diff --git a/seo_rules.py b/seo_rules.py index d697291..6653762 100644 --- a/seo_rules.py +++ b/seo_rules.py @@ -270,6 +270,19 @@ def _sig_tokens(title, slug): if len(t) >= 3 and t not in TOPIC_STOPWORDS and not _YEAR_RE.fullmatch(t)} +def _canoniza_a(post): + """Slug al que este post declara canónico, o None si no declara ninguno. + + Ghost guarda una URL completa; aquí solo interesa el último segmento, que es + lo único comparable con el slug de otro post del mismo sitio. + """ + url = _s(post.get("canonical_url")) + if not url: + return None + resto = url.split("?")[0].split("#")[0].rstrip("/") + return resto.rsplit("/", 1)[-1] or None + + def _hook(title): return _s(title).split(":")[0].strip().lower() @@ -290,10 +303,19 @@ def topic_collision(post, corpus): c_hook = _hook(post.get("title")) c_slug_toks = _tokens(_s(post.get("slug")).replace("-", " ")) + c_canon = _canoniza_a(post) + for other in corpus: if other.get("id") == post.get("id"): continue o_title, o_slug = _s(other.get("title")), _s(other.get("slug")) + # Un par consolidado NO es una colisión: es la solución a una colisión. + # Cuando uno de los dos declara al otro como canónico, Google ya sabe + # cuál manda y Ghost excluye al secundario del sitemap. Marcarlo sería + # pedir que se arregle algo que está arreglado — y el aviso, al no poder + # resolverse nunca, enseña a ignorar al validador. + if c_canon == o_slug or _canoniza_a(other) == _s(post.get("slug")): + continue o_years = _case_years(o_title, o_slug) o_sig = _sig_tokens(o_title, o_slug) diff --git a/seo_validate.py b/seo_validate.py index 8d68b60..b0f1b5b 100644 --- a/seo_validate.py +++ b/seo_validate.py @@ -50,8 +50,10 @@ def fetch_corpus(): fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_corpus_") os.close(fd) try: + # canonical_url hace falta para NO marcar como colisión un par que ya + # está consolidado a propósito (ver topic_collision en seo_rules.py). cmd = (f"ghst-en --json post list --limit all " - f"--fields id,title,slug,status > {path}") + f"--fields id,title,slug,status,canonical_url > {path}") r = subprocess.run(cmd, shell=True, stderr=subprocess.PIPE, text=True) if r.returncode != 0: print(f" [warn] corpus fetch failed — topic collision NOT checked:\n{r.stderr}",