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:
@@ -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 <slug-tuyo> --site <el-otro>
|
||||
|
||||
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 <slug> --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.
|
||||
|
||||
+9
-1
@@ -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.
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user