El ES nunca se había auditado: no había wrapper hasta hoy (ghst-es). Corre
ambos por defecto y emite un informe con una sección por sitio.
Detalles del diseño:
* canónico invertido entre sitios (EN www, ES apex) → SITES parametriza url,
cli y host.
* seo_rules.SITE_HOST se reasigna en runtime desde use_site(): ese fichero se
vendoriza byte a byte en ResearchOwl y la CI lo verifica, así que no se
puede parametrizar en origen.
* huella de estado por sitio (dict), para que un cambio en uno no reabra la
notificación del otro por accidente.
* un sitio caído no tumba el informe del otro: run_site va en try/except y
el error se reporta como hallazgo de ese sitio.
Primera pasada real: EN 0 hallazgos; ES 152 violaciones del auditor (28 sin
alt-text — todos —, 27 con enlazado flojo, 21 sin OG/Twitter). Ninguna dispara
Telegram: las violaciones de reglas son inventario, no regresión — solo avisan
los enlaces rotos/no canónicos/prematuros/envejecidos y el sitemap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El blog ES no tenía forma de llegar desde las herramientas: solo existía el
wrapper ghst-en. Creado ghst-es (gemelo, namespace zona-exclusion, token
efímero del pod, nada en disco) y parametrizado seo_finish.
Ojo al canónico, que va al revés en cada sitio: EN es www, ES es el apex.
seo_rules.SITE_HOST está fijado a EN y NO se parametriza ahí a propósito: ese
fichero se vendoriza byte a byte dentro de ResearchOwl y la CI lo verifica.
Se reasigna en tiempo de ejecución desde use_site(), que deja intacto el
contrato compartido.
Los ficheros de trabajo (imagen convertida, volcado de texto) y los backups
llevan ahora prefijo de sitio, para que un mismo slug en ambos idiomas no se
pise.
Verificado contra los dos sitios: ES detecta las 8 violaciones del borrador
nuevo (todas por dryrun, que ya no escribe SEO) y EN sigue dando 0 en el
artículo belga publicado hoy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primer artículo pasado por el flujo nuevo (generar → revisión editorial →
imagen → remate). 1 enlace (→ gimbal-gofast, por la referencia a los vídeos
de la Navy); la prosa no nombra ningún otro caso del corpus: la oleada belga
se narra con sus propias fuentes (SOBEPS, De Brouwer, el memo de la DIA).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El corpus crece 2 posts/semana, así que un enlace apunta al mejor destino que
existía el día en que se escribió — y envejece. Caso real que motivó el check
(2026-07-18): levelland enlazaba el anchor 'Project Blue Book' al artículo de
AARO porque cuando se generó (30-jun) el de Blue Book no existía (1-jul).
No fue un fallo del generador: fue el mejor destino disponible, un día antes
de que apareciera el correcto.
Heurística deliberadamente conservadora, por la misma razón que el resto del
job: un vigilante desatendido que grita en falso se acaba ignorando.
* solo marca si el anchor contiene TODOS los tokens distintivos de otro post
* no marca si el anchor ya casa con el destino actual (el enlace está bien)
* ignora posts con <2 tokens distintivos — p.ej. roswell-1947 se reduce a
{roswell}, que casa con demasiadas cosas
Test de regresión hecho contra los datos reales pre y post arreglo: caza
levelland antes, 0 hallazgos después (incluido el falso positivo de yellow-sea,
cuyo anchor menciona 'roswell' pero apunta correctamente al post del vídeo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El enlace llevaba el anchor 'Project Blue Book' pero apuntaba al artículo de
AARO. No era un error del generador: Levelland se creó el 30-jun y el artículo
de Blue Book no existía hasta el 1-jul, así que el autofill enlazó al destino
más cercano disponible. Al día siguiente quedó obsoleto.
Arreglado: el enlace ahora apunta a project-blue-book (anchor y destino
coinciden) y se ha desenlazado el duplicado que se añadió antes hoy sobre la
segunda mención. Queda 1 enlace, de ahí el waiver: la prosa no nombra ninguna
otra entidad con artículo propio (lo demás es Hynek y 'ball lightning').
Blog completo (30 publicados + 9 programados): 0 violaciones reales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Revisión preventiva de la cola programada: 4 posts traían enlazado flojo.
Dos tenían enlace honesto y se aplicó (Project Blue Book, publicado el 1-jul,
así que no hay riesgo de enlace prematuro):
- shag-harbour (17-ago): '…led to the end of the U.S. Air Force's Project
Blue Book' → project-blue-book
- levelland (20-ago): '…led to the closing of Project Blue Book' → idem
Trampa evitada en levelland: su PRIMERA mención de 'Project Blue Book' ya era
el anchor de un enlace a AARO. Insertar ahí habría anidado <a> dentro de <a>.
Se enlazó la segunda mención, que estaba libre; verificado que no hay anidados.
Los otros dos van a waiver por autocontenidos:
- westall: no nombra ningún otro caso del corpus.
- colares: solo cita Varginha (ya enlazado) y 'Operation Saucer', que es su
propia operación y no tiene artículo.
Cola programada: 0 violaciones reales en los 9 posts. El auditor no saltará
según se vayan publicando.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El SEO es DERIVADO del artículo. Hoy ResearchOwl lo genera al crear el
borrador, pero la revisión editorial cambia el texto después → el SEO queda
obsoleto y hay que rehacerlo a mano en cada post. La solución no es parchear
sino re-derivarlo del texto FINAL, y ese es el hueco que cubre esta tool.
Además el alt-text SOLO puede escribirse aquí: cuando autofill.py genera el
SEO la imagen destacada aún no se ha elegido (de ahí su
'feature_image_alt': "" # human adds later). Ese paso humano no ocurría
nunca — los 9 posts publicados entre el 29-jun y el 16-jul salieron sin alt.
prep <slug> paquete de revisión: descarga la imagen destacada y la
convierte a PNG (Ghost sirve webp), vuelca el texto final,
lista el SEO actual con longitudes y las violaciones. READ-ONLY.
apply <slug> aplica un patch vía ghst-en, con backup previo, y RE-VALIDA
después (si sigue incumpliendo, lo dice).
Guardas por límites duros, para convertir en error explicado lo que Ghost
devuelve como 422 mudo:
* feature_image_alt > 191 car. (límite de Ghost, descubierto a base de 422)
* meta_description > 145 / meta_title > 60 (seo_rules)
El criterio (redactar la meta, describir la imagen) lo pone quien invoca —
humano o Claude vía hermes. El script hace lo mecánico y lo verifica.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra las 3 incidencias restantes de internal_links.too_few. Ninguno de los
tres nombra en su prosa una entidad con artículo propio (criterio de siempre:
entidad nombrada en el texto, no afinidad temática):
- tassili-najjer: arqueología sahariana, autocontenida.
- missing-scientists: trata de la cadena mediática, no de casos UAP.
- grusch-capitol-june-9 (adelanto): además lleva canonical_url a la
investigación completa, así que sus señales se consolidan allí; forzar
enlaces en un preview superado no aporta.
El cuarto caso (roswell-341) SÍ tenía enlace honesto y se aplicó:
'the Air Force's official Roswell explanation' → roswell-1947-ufo-incident.
Nota técnica: ese post usa nodos lexical nativos (no bloques HTML como
Wilson-Davis), así que hubo que partir el nodo de texto e insertar un nodo
'link' con el mismo esquema que el enlace preexistente.
Con esto el auditor queda a CERO violaciones accionables (venía de 20), lo que
hace que cualquier aviso futuro de seo_watch signifique algo de verdad.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chequeos deterministas sobre el sitio EN, READ-ONLY, con aviso a Telegram
solo cuando hay hallazgos Y algo cambia respecto a la ejecución anterior
(un informe semanal de 'todo bien' se acaba ignorando):
1. enlaces internos rotos (destino inexistente)
2. enlaces internos no canónicos (apex/http/sin barra → salto 301 evitable)
3. enlaces PREMATUROS — destino programado para DESPUÉS del post que lo
enlaza. Caso real detectado el 2026-07-18: el post Wilson-Davis (3-ago)
enlazaba a MJ-12 (13-ago) → habría sido 404 durante 10 días justo en la
ventana de rastreo del post recién publicado.
4. sitemap: toda URL listada debe dar 200 directo
5. violaciones accionables del motor de reglas (respeta seo_exceptions)
Por qué no lleva Gemini: el 2026-07-18 se midió con datos reales — de 6
sugerencias de enlazado interno, 0 cumplían el estándar 'entidad nombrada en
la prosa', y no vio las 3 entidades que había servidas en una sola frase.
Es fiable extrayendo (12/12 slugs correctos, cero alucinaciones) pero no
juzgando. Se deja fuera del job hasta tener un uso donde aporte de verdad.
Unidades systemd incluidas: timer semanal (lunes 05:30 UTC, antes de la
publicación de las 06:00). Instaladas en /etc/systemd/system/ y habilitadas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tehran-1976-iranian-f4-ufo-incident: 0 enlaces, prosa autocontenida — solo
nombra a Klass, Jafari y el informe de la DIA; ningún caso con artículo
propio. Los candidatos temáticos (Malmstrom, Levelland, Manises) no
aparecen en el texto, así que enlazarlos sería forzado. Revisado 2026-07-18.
- chiles-military-...-cefaas: waiver que ya estaba en el working tree sin
commitear (1 enlace → pursue-release-3; caso chileno autocontenido).
Efecto en el auditor: internal_links accionables 9 → 7 (el otro punto lo
cierra el post Wilson-Davis, que pasa a 2 enlaces reales).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos posts sobre el mismo caso se canibalizan en la SERP (2026-07-10: se
publicó un segundo Kecksburg con otro ya programado, y había un título
casi gemelo del de Malmstrom en la cola). Nueva función pura
topic_collision(post, corpus) en seo_rules.py — corpus-aware, fuera de
RULES — que dispara por: caso+año compartidos (años < 2020; los de la
era de noticias no son identidad de caso), hooks de título ≥70%
similares (calibrado: el par nuclear da 0.742, el par legítimo más
cercano 0.65), o slugs ≥50% solapados. seo_validate.py obtiene el corpus
via ghst-en (--limit all) y lo reporta como grupo topic_collision;
--no-collision lo omite para follow-ups intencionados.
Calibrado contra los 36 posts reales: dispara solo el duplicado
Kecksburg (HIGH) y el par Grusch preview/evento (MED, intencionado).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Versioning ~/seo-tools as critical SEO infrastructure (was home-dir only).
Canonical home of seo_rules.py — ResearchOwl will vendor a copy with a CI diff
guard against this repo. No secrets; Ghost access is via the ghst-en wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>