diff --git a/hreflang-parejas.md b/hreflang-parejas.md index 0bdc393..1193746 100644 --- a/hreflang-parejas.md +++ b/hreflang-parejas.md @@ -89,8 +89,20 @@ sim 0.881 222 sim 0.842 david grusch EN david-grusch-capitol-2026-whistleblower-uap-disclosure David Grusch at the Capitol: The Whistleblower Claims, the Congressional Obstruction, - ES el-testimonio-de-david-grusch-el-denunciante-que-cambio-la-conversacion-sobre-ovnis-en-el-congreso-de-ee-uu - El Testimonio de David Grusch: El Denunciante que Cambió la Conversación sobre OVNIs e + ES grusch-capitolio-9-junio-2026-divulgacion-uap + Grusch en el Capitolio: el día que la divulgación UAP pasó del testimonio a la exigenc + NOTA (2026-07-29, corregida): apuntaba a «el-testimonio-de-david-grusch», que trata la + comparecencia JURADA DE JULIO DE 2023 ante el Congreso. El EN trata el acto del 9 de + JUNIO DE 2026 en las escaleras del Capitolio. Mismo protagonista, tres años de + diferencia: declarados espejo, le decían a Google que dos artículos distintos son el + mismo. El espejo real es este, publicado un día después del acto (10-jun frente al + 9-jun) y con extensión pareja (1.481 frente a 1.647 palabras). + El ES de 2023 se queda SIN pareja: no hay artículo inglés sobre esa comparecencia. Y el + adelanto del EN (grusch-capitol-june-9-2026, «[Preview]», publicado el 31-may) tampoco + la lleva, porque está canonizado hacia el largo — y un canonizado no debe declarar + hreflang propio. + El fallo enseña dónde mirar: con casos históricos el año del título ya identifica el + hecho, pero con NOTICIAS el mismo protagonista vuelve, y el título no basta. decision: SI sim 0.838 aaro diff --git a/seo_hreflang.py b/seo_hreflang.py index c6e5f89..476445c 100644 --- a/seo_hreflang.py +++ b/seo_hreflang.py @@ -104,7 +104,39 @@ def trae(site, slugs): posts = json.load(open(path))["posts"] finally: os.unlink(path) - return {p["slug"]: p for p in posts if p["slug"] in slugs} + return {p["slug"]: p for p in posts if slugs is None or p["slug"] in slugs} + + +def desmarca(actual): + """Quita NUESTRO bloque y deja intacto lo demás. Devuelve None si no había.""" + actual = actual or "" + if INICIO not in actual or FIN not in actual: + return None + limpio = re.sub(re.escape(INICIO) + r".*?" + re.escape(FIN), "", actual, flags=re.S) + return re.sub(r"\n{3,}", "\n\n", limpio).strip() + + +def huerfanos(parejas): + """Posts que llevan nuestro bloque y YA NO están en ninguna pareja SI. + + Sin esto, repuntar una pareja deja media declaración viva. Pasó con Grusch + el 2026-07-29: el ES de la comparecencia de 2023 estaba emparejado por error + con el EN del acto de 2026, y al corregirlo el ES viejo se habría quedado + señalando a un artículo que ya no le señala a él. Un hreflang que no es + recíproco Google lo ignora, pero el que sobra sigue afirmando algo falso. + + Se busca en TODO el corpus, no solo en los slugs de las parejas: el que hay + que limpiar es justamente el que ha dejado de estar en la lista. + """ + fuera = [] + for site, dentro in (("en", {a for a, _ in parejas}), ("es", {b for _, b in parejas})): + for slug, p in trae(site, None).items(): + if slug in dentro: + continue + limpio = desmarca(p.get("codeinjection_head")) + if limpio is not None: + fuera.append((site, p, limpio)) + return fuera def cambios(): @@ -126,7 +158,7 @@ def cambios(): nuevo = fusiona(p.get("codeinjection_head"), blq) if nuevo != (p.get("codeinjection_head") or ""): plan.append((site, p, nuevo)) - return plan, avisos, parejas + return plan + huerfanos(parejas), avisos, parejas def violaciones(post, site, parejas=None):