diff --git a/src/generator/generator.py b/src/generator/generator.py index 301b815..a0fab1b 100644 --- a/src/generator/generator.py +++ b/src/generator/generator.py @@ -385,6 +385,14 @@ def _seo_dryrun_message(ghost: "GhostPublisher", post: dict, seo: dict, inserted ) +def _norm_title(t: str) -> str: + """Normaliza un título para compararlo con lo que Ghost almacenó: colapsa + whitespace y trunca a 255 chars (límite de posts.title en Ghost — un H1 + largo del LLM puede volver truncado/normalizado, y la igualdad exacta + fallaría justo en los casos que el guard debe cubrir).""" + return " ".join((t or "").split())[:255] + + def _parse_ghost_ts(ts: str) -> float | None: """created_at de la Admin API (ISO 8601, p.ej. 2026-07-04T20:26:00.000+00:00 o sufijo Z) → epoch. None si no parsea.""" @@ -438,7 +446,9 @@ class GhostPublisher: (p.ej. decode roto), el draft existe aunque el caller viera una excepción. Con `since` (epoch) solo casan drafts creados en este intento — un draft viejo del mismo título de una run anterior NO cuenta - como "ya publicado". Best-effort: None si no hay match o si falla.""" + como "ya publicado". La comparación usa _norm_title (whitespace + colapsado + truncado a 255) para sobrevivir la normalización de Ghost. + Best-effort: None si no hay match o si falla.""" import aiohttp as _aio token = self._make_token() url = ( @@ -458,8 +468,9 @@ class GhostPublisher: if resp.status != 200: return None data = await resp.json() + wanted = _norm_title(title) for p in data.get("posts", []): - if p.get("title") != title: + if _norm_title(p.get("title") or "") != wanted: continue if since is not None: created = _parse_ghost_ts(p.get("created_at") or "")