From 67b55454c815d08700c9b5f6f1fde04b9b90c320 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Wed, 29 Jul 2026 17:37:51 +0000 Subject: [PATCH] =?UTF-8?q?estado:=20vigilar=20tambi=C3=A9n=20los=20archiv?= =?UTF-8?q?os=20de=20tag=20(H1=20y=20t=C3=ADtulo=20duplicado)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salieron al ordenar la taxonomía del EN, y son los dos visibles: 1. el H1 del archivo de tag sale del campo `name`, y en el EN los siete tags se llamaban COMO SU SLUG: /tag/military-cases/ le enseñaba «military-cases» al lector, con el correcto justo encima. El ES enseñaba «Casos Militares». Siete páginas indexadas con un slug de titular. 2. `uap` e `investigation` tenían el MISMO meta_title — dos archivos compitiendo en el SERP con el mismo titular. Los nombres no se inventaron: salían del meta_title que cada tag ya tenía, o sea de una decisión editorial que estaba en el campo equivocado. Slugs intactos: `--name` y `--new-slug` son opciones distintas, así que las siete URLs siguen dando 200 y no hace falta ninguna redirección. Renombrar era IMPOSIBLE hasta esta mañana: researchowl casaba los tags por NOMBRE, así que esto habría creado siete duplicados `-2`. Se pudo porque el commit 3d8cba6 los pasó a resolver por slug→id. El detector de «nombre == slug» exige un guion a propósito: `uap` llamándose «uap» no se lee como slug crudo y marcarlo sería un falso positivo. Hay test. Y un fallo del propio fichero de tests, encontrado al añadir estos: el bloque `if __name__ == "__main__"` estaba en MEDIO, así que los tests definidos después no entraban en globals() y no corrían nunca — pasaban en verde sin ejecutarse. Movido al final. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- seo_estado.py | 23 +++++++++++++++++++- test_seo_estado.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/seo_estado.py b/seo_estado.py index d41e8f9..3174296 100644 --- a/seo_estado.py +++ b/seo_estado.py @@ -123,7 +123,28 @@ def check_tags(site, cfg, posts): if vacios: out.append(f"tags SIN NINGÚN POST (archivo vacío en el sitemap): {vacios}") - # 3. Posts sin categoría. Incluye los PROGRAMADOS a propósito: los 9 de + # 3. El nombre del tag es lo que el TEMA pinta como H1 del archivo. En el EN + # los siete se llamaban como su slug, así que /tag/military-cases/ le + # enseñaba «military-cases» al lector (el ES enseñaba «Casos Militares»). + # Un slug crudo de H1 en una página indexada es un fallo visible. + feos = sorted(t["slug"] for t in publicos + if (t.get("name") or "") == t["slug"] and "-" in t["slug"]) + if feos: + out.append(f"tags cuyo NOMBRE es el slug (sale de H1 en el archivo): {feos}") + + # 4. Dos archivos de tag con el MISMO <title> compiten entre ellos en el + # SERP. Salió al renombrar: `uap` e `investigation` tenían los dos + # «UAP Investigation — The Exclusion Zone». + por_meta = {} + for t in publicos: + mt = (t.get("meta_title") or "").strip() + if mt: + por_meta.setdefault(mt, []).append(t["slug"]) + for mt, v in sorted(por_meta.items()): + if len(v) > 1: + out.append(f"mismo <title> en {sorted(v)}: «{mt[:52]}»") + + # 5. Posts sin categoría. Incluye los PROGRAMADOS a propósito: los 9 de # agosto salían sin ninguna y se habrían publicado así. for estado in ("published", "scheduled"): sin = sorted(p["slug"] for p in posts if p.get("status") == estado diff --git a/test_seo_estado.py b/test_seo_estado.py index 298b53a..9fe22a6 100644 --- a/test_seo_estado.py +++ b/test_seo_estado.py @@ -140,6 +140,59 @@ def test_las_reglas_de_post_que_no_aplican_a_paginas_quedan_fuera(): assert not E._fuera_de_paginas(regla), regla + +# ───────── archivos de tag: lo que el lector ve y lo que ve Google ───────── + +def test_detecta_el_nombre_de_tag_que_es_el_slug(): + """El H1 del archivo sale del `name`. En el EN los siete se llamaban como + su slug: /tag/military-cases/ enseñaba «military-cases» al lector.""" + orig = E.sh + E.sh = _tags_falsos([ + {"slug": "military-cases", "name": "military-cases", "visibility": "public"}, + ]) + try: + salida = E.check_tags("en", E.SITES["en"], [ + {"slug": "p", "status": "published", + "tags": [{"slug": "military-cases", "visibility": "public"}]}]) + finally: + E.sh = orig + assert any("NOMBRE es el slug" in l for l in salida), salida + + +def test_un_acronimo_de_una_palabra_no_cuenta_como_slug_crudo(): + """`uap` llamándose «uap» no es el mismo problema que «military-cases»: + sin guion no se lee como slug. Exigir el guion evita el falso positivo.""" + orig = E.sh + E.sh = _tags_falsos([{"slug": "uap", "name": "uap", "visibility": "public"}]) + try: + salida = E.check_tags("en", E.SITES["en"], [ + {"slug": "p", "status": "published", + "tags": [{"slug": "uap", "visibility": "public"}]}]) + finally: + E.sh = orig + assert not any("NOMBRE es el slug" in l for l in salida), salida + + +def test_detecta_dos_archivos_de_tag_con_el_mismo_title(): + """Salió al renombrar: `uap` e `investigation` compartían meta_title, o sea + dos páginas indexadas compitiendo con el mismo titular.""" + orig = E.sh + E.sh = _tags_falsos([ + {"slug": "uap", "name": "UAP", "visibility": "public", + "meta_title": "UAP Investigation — The Exclusion Zone"}, + {"slug": "investigation", "name": "Investigation", "visibility": "public", + "meta_title": "UAP Investigation — The Exclusion Zone"}, + ]) + try: + salida = E.check_tags("en", E.SITES["en"], [ + {"slug": "p", "status": "published", "tags": [ + {"slug": "uap", "visibility": "public"}, + {"slug": "investigation", "visibility": "public"}]}]) + finally: + E.sh = orig + assert any("mismo <title>" in l for l in salida), salida + + if __name__ == "__main__": fallos = 0 for nombre, fn in sorted(globals().items()):