diff --git a/seo_link.py b/seo_link.py index b2a8418..52bfb53 100644 --- a/seo_link.py +++ b/seo_link.py @@ -62,14 +62,25 @@ def sh(cmd, timeout=240): def fetch_corpus(cli): - """Corpus completo (published + scheduled). A fichero: un PIPE se trunca.""" + """Corpus completo (published + scheduled). A fichero: un PIPE se trunca. + + `--limit all` + comprobación del total (ver seo_watch.fetch_posts). Aquí un + corpus truncado es peor todavía que en el vigilante: haría fallar el + «¿existe el destino?» y rechazaría un enlace perfectamente válido, o peor, + daría por inexistente un post que sí está.""" fd, path = tempfile.mkstemp(suffix=".json") os.close(fd) try: - sh(f"{cli} post list --limit 100 --formats lexical,mobiledoc,html --json > {path}") + sh(f"{cli} post list --limit all --formats lexical,mobiledoc,html --json > {path}") with open(path) as f: d = json.load(f) - return d.get("posts", d) if isinstance(d, dict) else d + posts = d.get("posts", d) if isinstance(d, dict) else d + total = ((d.get("meta") or {}).get("pagination") or {}).get("total") \ + if isinstance(d, dict) else None + if total is not None and len(posts) != total: + sys.exit(f"✗ corpus truncado: {len(posts)} de {total} posts — abortado " + f"sin escribir (el chequeo de destinos sería mentira)") + return posts finally: os.unlink(path) diff --git a/seo_watch.py b/seo_watch.py index c93fba6..9f2ff1b 100644 --- a/seo_watch.py +++ b/seo_watch.py @@ -82,14 +82,28 @@ def sh(cmd, timeout=180): def fetch_posts(): """Todos los posts (published + scheduled) con html. Salida a fichero: la - respuesta pesa cientos de KB y un PIPE se trunca a 64 KB.""" + respuesta pesa cientos de KB y un PIPE se trunca a 64 KB. + + `--limit all` y ADEMÁS se comprueba contra meta.pagination. Antes pedía + `--limit 100`: con 40 posts en EN y 29 en ES sobraba, pero a 2 posts por + semana el corpus habría cruzado los 100 hacia 2027 y se habría truncado EN + SILENCIO — y un corpus truncado no da error, da falsos «enlaces rotos» + contra los posts que faltan. Verificar el total es lo que convierte ese + fallo mudo en uno que se oye.""" fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_watch_") os.close(fd) try: - r = sh(f"{CLI} post list --limit 100 --formats html --json > {path}", timeout=240) + r = sh(f"{CLI} post list --limit all --formats html --json > {path}", timeout=240) with open(path) as f: data = json.load(f) - return data.get("posts", data) if isinstance(data, dict) else data + posts = data.get("posts", data) if isinstance(data, dict) else data + total = ((data.get("meta") or {}).get("pagination") or {}).get("total") \ + if isinstance(data, dict) else None + if total is not None and len(posts) != total: + raise RuntimeError( + f"corpus truncado: {len(posts)} de {total} posts. " + f"Los checks de enlaces darían falsos rotos — abortado.") + return posts finally: os.unlink(path) @@ -211,11 +225,20 @@ def check_link_aging(posts): def check_sitemap(): + """URLs del sitemap que no dan 200. + + Un sitemap VACÍO cuenta como hallazgo, no como éxito: si curl falla o el + sitio está caído mientras corre la revisión, se descargan 0 URLs, se + encuentran 0 problemas y el informe decía «✅ sin hallazgos». Justo el rato + en que el sitio está roto es cuando este check se callaba. El sitemap de + posts nunca está legítimamente vacío en un blog con 29-40 artículos.""" malos = [] urls = [] for part in ("posts", "pages"): r = sh(f"curl -s --max-time 25 {HOST}sitemap-{part}.xml") urls += re.findall(r"(.*?)", r.stdout) + if not urls: + return 0, [(f"{HOST}sitemap-posts.xml", "vacío/inaccesible")] with ThreadPoolExecutor(max_workers=8) as ex: for u, (code, _) in zip(urls, ex.map(lambda x: http_status(x), urls)): if code != 200: @@ -376,7 +399,10 @@ def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, regl if envejecido: L.append(f"🔵 {len(envejecido)} enlace(s) ENVEJECIDO(S) (ya existe mejor destino):") L += [f" {s[:26]}: «{a}» → /{t[:24]}/ ⇒ /{m[:26]}/" for s, a, t, m in envejecido[:6]] - if sm_malos: + if sm_malos and n_sitemap == 0: + L.append("🔴 sitemap VACÍO o inaccesible — no se ha podido comprobar " + "ninguna URL (¿sitio caído?, ¿curl sin salida?)") + elif sm_malos: L.append(f"🔴 sitemap: {len(sm_malos)} de {n_sitemap} URL(s) no dan 200:") L += [f" [{c}] {u.replace(HOST,'/')}" for u, c in sm_malos[:6]] if mayus: