hreflang: convertirlo en regla del auditor y en paso del remate

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.
This commit is contained in:
ChemaVX
2026-07-28 07:39:45 +00:00
parent bdcab16ad9
commit cffd174d4a
3 changed files with 68 additions and 1 deletions
+33
View File
@@ -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)}")