seo_watch/seo_link: dos fallos que se manifestaban callándose

1) El corpus se pedía con --limit 100. Con 40 posts en EN y 29 en ES sobraba,
   pero a 2 por semana 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, que es peor que no mirar. Pasa a --limit all
   y, sobre todo, se comprueba el número contra meta.pagination.total: eso es
   lo que convierte un fallo mudo en uno que se oye. En seo_link el truncado
   era aún peor, porque haría rechazar como inexistente un destino que sí
   está.

2) check_sitemap daba VERDE cuando curl fallaba: 0 URLs descargadas son 0
   problemas encontrados, y el informe decía « sin hallazgos». O sea que
   justo el rato en que el sitio está caído era cuando este check se callaba.
   Un sitemap vacío pasa a ser hallazgo con mensaje propio; en un blog con
   29-40 artículos nunca lo está legítimamente.

Probado con casos que DEBEN fallar: corpus recortado a 3 de 40 (revienta y lo
dice), corpus completo (pasa limpio), y sitemap vacío (hallazgo, informe claro
y notificaría). Y en real contra los dos blogs: EN 40 posts / 33 URLs, ES 29 /
31, ambos sin hallazgos.

No toca seo_rules.py, así que el vendor-sync con ResearchOwl sigue en verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-21 16:01:28 +00:00
co-authored by Claude Opus 4.8
parent 48fbe4302f
commit d381c0225c
2 changed files with 44 additions and 7 deletions
+30 -4
View File
@@ -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"<loc>(.*?)</loc>", 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: