ghost: resolver los tags por ID, que mandarlos por nombre partía el ES en dos

ALLOWED_TAGS son SLUGS y Ghost casa los tags de un post por NOMBRE. Mandarlos
como {"name": slug} funcionaba en EN por casualidad — allí los tags se llaman
igual que su slug ("military-cases") — y en ES rompía: no hay ningún tag
llamado "casos-militares" (se llama "Casos Militares"), así que Ghost creaba
uno nuevo con ese nombre y, con el slug ya pillado, lo dejaba en
`casos-militares-2`.

Encontrado el 2026-07-29 al preparar el hub: 5 tags duplicados, 7 posts
repartidos entre dos archivos flacos cada uno, y los cinco `-2` ofrecidos a
Google en sitemap-tags.xml. Los datos ya están fusionados en Ghost; esto es
para que no vuelva.

Se resuelve el slug a ID contra la Admin API, que es lo único no ambiguo justo
en el punto donde falla la ambigüedad. Un slug que no exista se DESCARTA con
aviso en vez de crearse: la lista es cerrada, así que no existir significa que
está mal escrita, y crear el tag es exactamente el bug. Si no resuelve nada, o
si Ghost no contesta, se cae al comportamiento anterior antes que publicar un
post sin ninguna categoría.

7 tests nuevos, incluido uno para EN — donde el bug era invisible por la
coincidencia nombre==slug y el resolutor no debe depender de ella.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-29 16:22:14 +00:00
co-authored by Claude Opus 5
parent a43bbc752d
commit 3d8cba6567
2 changed files with 126 additions and 1 deletions
+42 -1
View File
@@ -480,6 +480,47 @@ class GhostPublisher:
return None
return await resp.json()
async def _resolve_tags(self, slugs: list[str]) -> list[dict]:
"""ALLOWED_TAGS son SLUGS; Ghost casa los tags de un post por NOMBRE.
Mandarlos como `{"name": slug}` funciona por casualidad en EN, donde los
tags se llaman igual que su slug ("military-cases"), y rompe en ES,
donde se llaman "Casos Militares": Ghost no encuentra ninguno con ese
nombre y CREA uno nuevo llamado "casos-militares", que como ya tiene el
slug pillado acaba en `casos-militares-2`. Detectado el 2026-07-29 con
5 tags duplicados y 7 posts repartidos entre dos archivos flacos, uno
de ellos ofrecido a Google en el sitemap.
Se resuelve el slug a ID contra Ghost, que es lo único no ambiguo. Un
slug que no exista se descarta con aviso en vez de crearse: la lista es
cerrada, así que no existir significa que está mal escrita, y crear el
tag es precisamente el bug. Si no se resuelve nada (o Ghost no
responde) se cae al comportamiento anterior antes que dejar el post sin
ninguna categoría.
"""
data = await self._admin_get("tags/?limit=all")
if not data:
logger.warning("Ghost tags no legibles: se mandan por nombre",
slugs=slugs)
return [{"name": t} for t in slugs]
por_slug = {t.get("slug"): t.get("id") for t in data.get("tags", [])}
resueltos, perdidos = [], []
for s in slugs:
tid = por_slug.get(s)
if tid:
resueltos.append({"id": tid})
else:
perdidos.append(s)
if perdidos:
logger.warning("Ghost: slugs de tag inexistentes, descartados",
slugs=perdidos, lang=self.lang)
if not resueltos:
defecto = _DEFAULT_TAG.get(self.lang, "investigation")
tid = por_slug.get(defecto)
return [{"id": tid}] if tid else [{"name": defecto}]
return resueltos
async def find_draft_by_title(self, title: str,
since: float | None = None) -> dict | None:
"""Busca entre los drafts recientes uno con título exacto.
@@ -549,7 +590,7 @@ class GhostPublisher:
"title": title,
"mobiledoc": mobiledoc,
"status": "draft", # NEVER "published" — draft only, always.
"tags": [{"name": t} for t in tag_names],
"tags": await self._resolve_tags(tag_names),
}
if seo:
post_obj.update({