From 6029bebae536cc12e2db51e37de9a9d8114071ca Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Tue, 21 Jul 2026 15:47:39 +0000 Subject: [PATCH] =?UTF-8?q?fix(seo):=20el=20can=C3=B3nico=20del=20ES=20es?= =?UTF-8?q?=20el=20APEX,=20no=20www?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cada enlace interno que el generador escribía en un borrador español apuntaba a www.zonadeexclusion.com y se comía un 301. El canónico de los dos blogs está INVERTIDO —EN es www, ES es el apex—, la misma inversión que provocó el 522 y de la que ya avisa el wrapper ghst-es. No es teórico: el enlace www suelto que apareció en los-villares salió de aquí. El corpus ES está limpio hoy, pero el próximo /generate con enlaces lo habría reintroducido. Nada río abajo lo paraba: seo_watch solo ve el enlace DESPUÉS de publicar, y ningún test cubría el host. Añado los dos tests que faltaban, uno por idioma, para fijar la inversión. El del ES falla contra el valor viejo con el href entero en el mensaje. Sin tocar rules.py, así que el vendor-sync sigue en verde. Queda documentado el matiz que este cambio NO arregla: rules.internal_links cuenta cero enlaces en ES bajo cualquiera de los dos hosts, porque ese módulo está clavado al host EN (se vendoriza byte a byte y la CI lo verifica). Inofensivo hoy — internal_links.too_few no bloquea en borrador. Co-Authored-By: Claude Opus 4.8 --- src/seo/autofill.py | 20 ++++++++++++++++---- tests/test_seo_autofill.py | 28 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/seo/autofill.py b/src/seo/autofill.py index 717599c..ccd397d 100644 --- a/src/seo/autofill.py +++ b/src/seo/autofill.py @@ -27,12 +27,24 @@ from src.seo import rules as R logger = structlog.get_logger(__name__) -# Canonical site host per language. The wrapped hrefs use the www host (the -# site's canonical form); rules.internal_links still counts them because its -# SITE_HOST ("theexclusionzone.com") is a substring of "www.theexclusionzone.com". +# Canonical site host per language — and the two are INVERTED, which is exactly +# the trap: EN canonicalizes on www, ES canonicalizes on the APEX. Same +# inversion that caused the 522 outage, and the same one ghst-es warns about. +# +# ES said "www." until 2026-07-21. Every internal link the generator wrote into +# a Spanish draft therefore pointed at a non-canonical host and ate a 301 — +# that is where the stray www link found in los-villares came from. Nothing +# downstream would have stopped it either: seo_watch only sees it after the +# post is published. +# +# Caveat, unchanged by this fix: rules.internal_links counts EN links because +# its SITE_HOST ("theexclusionzone.com") is a substring of the www form, but it +# counts ZERO for ES under either host, since that module is hardcoded to the +# EN host (it is vendored byte-for-byte and CI enforces the copy). Harmless +# today because internal_links.too_few is not a blocking rule at draft time. SITE_BY_LANG = { "en": "www.theexclusionzone.com", - "es": "www.zonadeexclusion.com", + "es": "zonadeexclusion.com", } # Tag allow-list — the model may ONLY pick from these; invented tags are dropped diff --git a/tests/test_seo_autofill.py b/tests/test_seo_autofill.py index da938f3..8ed2a4f 100644 --- a/tests/test_seo_autofill.py +++ b/tests/test_seo_autofill.py @@ -1,4 +1,5 @@ -from src.seo.autofill import ALLOWED_TAGS, DEFAULT_TAG, _coerce, _system_prompt +from src.seo.autofill import (ALLOWED_TAGS, DEFAULT_TAG, _coerce, _system_prompt, + insert_internal_links) BASE = { "meta_title": "t", @@ -81,3 +82,28 @@ def test_collision_note_is_markdown_safe(): def test_slugify_title(): assert _slugify_title("USS Russell 2019: The Pyramid UAP!") == "uss-russell-2019-the-pyramid-uap" + + +# --- canonical host per language ------------------------------------------- +# EN canonicalizes on www, ES on the APEX. They are INVERTED, and the ES entry +# said "www." until 2026-07-21, so every internal link written into a Spanish +# draft ate a 301. Nothing caught it: no test covered the host, and seo_watch +# only sees a link once the post is published. These two pin it. + +def test_internal_link_uses_es_apex_canonical(): + html = "

El caso de Manises sigue abierto.

" + out, pairs = insert_internal_links( + html, [{"phrase": "Manises", "slug": "manises-1979"}], + [{"slug": "manises-1979", "title": "Manises"}], "es") + assert 'href="https://zonadeexclusion.com/manises-1979/"' in out + assert "www.zonadeexclusion.com" not in out + assert len(pairs) == 1 + + +def test_internal_link_uses_en_www_canonical(): + html = "

The Roswell debris was recovered.

" + out, pairs = insert_internal_links( + html, [{"phrase": "Roswell", "slug": "roswell-1947"}], + [{"slug": "roswell-1947", "title": "Roswell"}], "en") + assert 'href="https://www.theexclusionzone.com/roswell-1947/"' in out + assert len(pairs) == 1