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:
co-authored by
Claude Opus 4.8
parent
48fbe4302f
commit
d381c0225c
+14
-3
@@ -62,14 +62,25 @@ def sh(cmd, timeout=240):
|
|||||||
|
|
||||||
|
|
||||||
def fetch_corpus(cli):
|
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")
|
fd, path = tempfile.mkstemp(suffix=".json")
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
try:
|
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:
|
with open(path) as f:
|
||||||
d = json.load(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:
|
finally:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
|
|||||||
+30
-4
@@ -82,14 +82,28 @@ def sh(cmd, timeout=180):
|
|||||||
|
|
||||||
def fetch_posts():
|
def fetch_posts():
|
||||||
"""Todos los posts (published + scheduled) con html. Salida a fichero: la
|
"""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_")
|
fd, path = tempfile.mkstemp(suffix=".json", prefix="seo_watch_")
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
try:
|
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:
|
with open(path) as f:
|
||||||
data = json.load(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:
|
finally:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
@@ -211,11 +225,20 @@ def check_link_aging(posts):
|
|||||||
|
|
||||||
|
|
||||||
def check_sitemap():
|
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 = []
|
malos = []
|
||||||
urls = []
|
urls = []
|
||||||
for part in ("posts", "pages"):
|
for part in ("posts", "pages"):
|
||||||
r = sh(f"curl -s --max-time 25 {HOST}sitemap-{part}.xml")
|
r = sh(f"curl -s --max-time 25 {HOST}sitemap-{part}.xml")
|
||||||
urls += re.findall(r"<loc>(.*?)</loc>", r.stdout)
|
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:
|
with ThreadPoolExecutor(max_workers=8) as ex:
|
||||||
for u, (code, _) in zip(urls, ex.map(lambda x: http_status(x), urls)):
|
for u, (code, _) in zip(urls, ex.map(lambda x: http_status(x), urls)):
|
||||||
if code != 200:
|
if code != 200:
|
||||||
@@ -376,7 +399,10 @@ def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, regl
|
|||||||
if envejecido:
|
if envejecido:
|
||||||
L.append(f"🔵 {len(envejecido)} enlace(s) ENVEJECIDO(S) (ya existe mejor destino):")
|
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]]
|
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.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]]
|
L += [f" [{c}] {u.replace(HOST,'/')}" for u, c in sm_malos[:6]]
|
||||||
if mayus:
|
if mayus:
|
||||||
|
|||||||
Reference in New Issue
Block a user