From cffd174d4a13e2da302e676a66841117f5b0a83b Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Tue, 28 Jul 2026 07:39:45 +0000 Subject: [PATCH] hreflang: convertirlo en regla del auditor y en paso del remate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hasta ahora el hreflang era un estado —puesto una vez a mano— y no una regla. Un slug que cambiara habría dejado el enlace apuntando al artículo viejo sin que nada avisara. El auditor gana dos reglas: hreflang.missing (pareja declarada sin bloque) y hreflang.stale (bloque que ya no coincide con la pareja). Viven en seo_hreflang y no en seo_rules porque necesitan hreflang-parejas.md, que es una decisión editorial curada; el motor sigue sin saber que hay parejas. Probadas en los cinco estados, no solo en el feliz: dispara sin bloque, dispara con el bloque apuntando a un slug viejo, y se calla cuando está correcto, cuando el post no es de ninguna pareja y cuando aún es borrador. Y el remate gana el paso 3c: buscar el espejo en el otro idioma y dejarlo en el informe. Con la advertencia explícita de decidir por TÍTULO y no por vector, y las dos reglas duras — solo espejos publicados, y uno a uno. --- remate_prompt.md | 26 ++++++++++++++++++++++++++ seo_audit.py | 10 +++++++++- seo_hreflang.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/remate_prompt.md b/remate_prompt.md index 8036e36..70357de 100644 --- a/remate_prompt.md +++ b/remate_prompt.md @@ -75,6 +75,29 @@ CHECKLIST — en este orden (el SEO se deriva del texto FINAL, nunca antes): (compruébalo: descarga el origen y busca la frase). Si ningún candidato pasa de 0,55 de similitud, dilo en vez de inventar un enlace forzado. +3c) ESPEJO — ¿existe este mismo caso en el otro idioma? + + La casa publica en dos idiomas y dos dominios. Sin `hreflang`, Google no + sabe que las dos piezas del mismo caso son la misma pieza y las trata como + dos sitios sueltos compitiendo. Hasta el 2026-07-28 no había NI UNO. + + Busca el espejo en el OTRO sitio (si $site es «es», mira el EN, y al revés). + El criterio que decide es el TÍTULO — año y nombre propio del caso —, NO el + parecido semántico: los dos artículos de Immaculate Constellation puntúan + 0,774 entre sí y uno sin ninguna relación puntúa 0,778. Si te fías del + vector, emparejarás mal. + + python3 ~/seo-tools/seo_semantic.py afines --site + + Reglas duras: solo vale un espejo **publicado** (a uno programado el enlace + sería un 404), y es **uno a uno** — si dos artículos tuyos apuntarían al + mismo, no es espejo de ninguno. + + Si lo encuentras, NO lo apliques: añadir el bloque toca los DOS posts, uno + de ellos del otro sitio. Deja en el informe la línea lista para + `hreflang-parejas.md` y el comando. Si no hay espejo, dilo — es lo normal + en artículos que solo existen en un idioma. + 4) METAS + ALT — con seo_finish.py: - `python3 ~/seo-tools/seo_finish.py prep --site $site` descarga la imagen destacada a ~/.cache/seo-finish/. @@ -105,4 +128,7 @@ anchas, <3000 caracteres): verificado en la prosa del origen. Va aparte de los enlaces del paso 3 y no se puede omitir: es lo único del checklist que decide si Google llega a encontrar el artículo. +- ESPEJO: el slug del artículo equivalente en el otro idioma, o «no existe». + Si existe y está publicado, la línea para `hreflang-parejas.md` y el + recordatorio de rematar con `seo_hreflang.py aplicar`. - Cierra recordando que sigue en borrador, listo para publicar. diff --git a/seo_audit.py b/seo_audit.py index 7d98a03..7cc13e5 100755 --- a/seo_audit.py +++ b/seo_audit.py @@ -27,6 +27,7 @@ from collections import Counter import seo_rules as R import seo_exceptions as X +import seo_hreflang as HL def partition(post, violations, site="en"): @@ -102,9 +103,16 @@ def main(): # el ES contaría cero enlaces internos en los 31 posts. R.usar_sitio(args.site) + # Las parejas espejo son una decisión editorial curada, no una regla del + # motor: el auditor las inyecta y seo_rules sigue sin saber que existen. + try: + parejas = HL.lee_parejas() + except OSError: + parejas = [] + results = [] # (post, actionable, accepted, info, score) for p in posts: - v = R.check_post(p) + v = R.check_post(p) + HL.violaciones(p, args.site, parejas) actionable, accepted, info = partition(p, v, args.site) # score ranks worst-first on ACTIONABLE severity only — accepted waivers # and INFO don't push a post up the list. diff --git a/seo_hreflang.py b/seo_hreflang.py index c906016..c6e5f89 100644 --- a/seo_hreflang.py +++ b/seo_hreflang.py @@ -129,6 +129,39 @@ def cambios(): return plan, avisos, parejas +def violaciones(post, site, parejas=None): + """Regla para el auditor: una pareja declarada que NO lo tiene puesto. + + Vive aquí y no en seo_rules porque necesita conocer hreflang-parejas.md, que + es una decisión editorial curada a mano y no una regla del motor. El auditor + la inyecta; el motor sigue sin saber nada de parejas. + + No basta con que exista el bloque: se comprueba que las URLs sean las de ESTA + pareja. Si un slug cambia, el hreflang se queda apuntando al viejo y esto es + lo único que lo cazaría. + """ + import seo_rules as R + if parejas is None: + parejas = lee_parejas() + idx = {(a if site == "en" else b): (a, b) for a, b in parejas} + par = idx.get(post.get("slug")) + if not par: + return [] + if post.get("status") != "published": + return [] + cabeza = post.get("codeinjection_head") or "" + esperado = bloque(*par) + if esperado in cabeza: + return [] + falta = INICIO not in cabeza + return [R.Violation( + "hreflang.missing" if falta else "hreflang.stale", R.MED, + ("sin hreflang y tiene espejo declarado" if falta else + "el hreflang no coincide con la pareja declarada (¿cambió un slug?)") + + f" — pareja: {par[1] if site == 'en' else par[0]}", + "python3 ~/seo-tools/seo_hreflang.py aplicar")] + + def cmd_plan(a): plan, avisos, parejas = cambios() print(f"{len(parejas)} parejas marcadas SI en {os.path.basename(PAREJAS)}")