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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user