Toda la caja miraba los enlaces en un solo sentido — a quién enlazo yo — y
esa es la pregunta del lector, no la de Google. Dos posts de EN publicados
el 21-jul seguían siendo «URL is unknown to Google» una semana después:
tenían enlaces entrantes, pero solo desde artículos con 8 y 12 impresiones
de por vida, páginas que Google casi no visita.
seo_semantic gana dos subcomandos: `padrinos <slug>`, que ordena candidatos
por tráfico REAL del origen (Search Console) en vez de por similitud, y
`descubrimiento`, que lista los publicados a los que no llega ni un enlace
desde el tercio fuerte del sitio. Salen 21 de 32 en EN y 21 de 31 en ES.
Ambos descartan los posts con canonical_url propio: Ghost los excluye del
sitemap por consolidados, así que apadrinar desde ahí no sirve de nada — el
de Grusch de mayo tiene 1.728 impresiones y habría encabezado la lista.
seo_gsc gana `inspecciona`, que pregunta el veredicto a Google en vez de
deducirlo. Es lo que separa «posiciona mal» de «no lo ha visto nunca», que
son problemas opuestos y se arreglan al revés.
Y el checklist del remate incorpora el paso 3b: el informe tiene que traer
el comando de enlace entrante listo para copiar. No lo aplica el agente
porque eso es editar otro post publicado, y eso sigue prohibido.
⚠️ Anotado en el docstring: contar enlaces internos desde el HTML público da
«cero huérfanos», porque el tema mete navegación y feed de relacionados que
parecen enlaces del cuerpo. La verdad está en el html del CLI de Ghost.
1) remate-watch ya no elige los enlaces internos a ojo. El paso 3 del prompt
pregunta primero a seo_semantic (afines), que le da los publicados más
parecidos y le marca los que el borrador YA enlaza. Se le dice
explícitamente que es una sugerencia y no una orden, y que si nada pasa de
~0,55 lo diga en el informe en vez de forzar un enlace malo: un enlace
forzado es peor que ninguno.
Para que eso funcione, seo_semantic sabe ahora trabajar con un post que NO
está en el corpus: `ghst post list` sin filtro devuelve solo publicados y
programados, así que el borrador que remate-watch está preparando no
aparecía. Se baja suelto por slug y se embebe al vuelo. Verificado por
equivalencia: los vecinos de un post embebido al vuelo son idénticos, hasta
el cuarto decimal, a los del mismo post cacheado.
2) canibal-watch: a diario a las 07:00 UTC (una hora después de que publique
la cola de agosto). Telegram SOLO si hay algo nuevo.
Solo avisa de pares que además NO se enlazan. Un par muy parecido pero
cosido con un enlace ya le dice a Google cuál manda; avisar de él es ruido.
Los enlazados se guardan igual en el estado, porque hacen falta para
detectar que MÁS TARDE pierdan el enlace (una edición que se lleve por
delante la jerarquía). Con el filtro, el primer aviso baja de 19 líneas a 5
accionables.
Probado por el camino real, arrancándolo con systemctl y no desde una
shell con el PATH bueno: primera pasada avisa (5 pares), segunda se calla,
y con una regresión simulada en el estado la detecta.
No escribe en el blog. Coser o fusionar lo decide Jose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Embebe los 80 artículos de los dos blogs con bge-m3 (ya cargado en ollama,
100% GPU) y sobre los vectores contesta dos cosas: a qué artículos publicados
conviene enlazar desde uno nuevo, y qué pares compiten por la misma búsqueda.
Nunca escribe en el blog: imprime. Quien escribe el enlace es seo_link.py,
y publicar lo decide Jose.
Decisiones que no son de adorno:
- NUNCA se cruzan los dos sitios. bge-m3 es multilingüe y da 0,966 entre un
artículo y su traducción (medido): cruzarlos marcaría todo el espejo ES↔EN
como canibalización, justo lo contrario de la verdad.
- Endpoint de ollama por la ClusterIP del Service, no por la IP del pod (que
cambia en cada reinicio) ni por el Ingress (está tras Authentik). El host la
alcanza por kube-proxy, sin NodePort ni tocar la red.
- Sin keep_alive en las peticiones: bge-m3 es de producción (lo usa
researchowl) y mandar keep_alive:0 lo desalojaría, cobrándole a otro la
recarga.
- Umbral 0,78 calibrado contra el corpus real, no a ojo: el reparto no es
igual en los dos sitios (ES mediana 0,58 / p99 0,75; EN 0,64 / 0,80).
Absoluto y no percentil, porque un umbral relativo siempre encuentra "el 1%
más parecido" aunque no haya problema, y un vigilante que siempre avisa no
sirve.
- Se mira qué enlaces EXISTEN ya en el cuerpo, y en qué DIRECCIÓN. Sugerir un
enlace que ya está es ruido; y decir "se enlazan entre sí" cuando el enlace
va en un solo sentido es mentir sobre lo único accionable.
Caché en ~/.local/state/seo-semantic/, re-embebe solo lo que cambia (hash del
texto). Corpus vía fetch_corpus de seo_link, que ya trae la guarda de corpus
truncado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>