seo-watch: vigila la indexación en Google (ventana 5-60 días, huella sin la antigüedad)

This commit is contained in:
ChemaVX
2026-08-31 18:07:39 +00:00
parent 1a9012db56
commit 1f9be7ed2a
3 changed files with 300 additions and 19 deletions
+79 -5
View File
@@ -31,6 +31,7 @@ Diseño (lecciones del 2026-07-18, ver memoria project-en-seo-recovery):
- Nada de esto modifica Ghost. Las correcciones las decide un humano.
"""
import argparse
import datetime as dt
import json
import os
import re
@@ -247,6 +248,70 @@ def check_sitemap():
return len(urls), malos
# ---------- indexación ----------
# Ventana de vigilancia. Por debajo de GRACIA no hay noticia: Google tarda días
# incluso cuando todo va bien, y avisar el día 1 convierte el vigilante en ruido.
# Por encima de VENTANA tampoco: un post de hace meses sin indexar ya no es un
# incidente que se arregle pidiendo indexación, es una conversación sobre la
# autoridad del dominio, y repetirlo cada mañana no la adelanta.
DIAS_GRACIA = 5
DIAS_VENTANA = 60
def check_indexacion(posts, site):
"""Publicados recientes que Google todavía NO ha indexado.
El resto del vigilante es estructuralmente ciego a esto: mide enlaces,
títulos, reglas y sitemap, y un artículo impecable en las cuatro cosas puede
llevar semanas sin que Google sepa que existe. En agosto de 2026 fueron SEIS
del blog EN a la vez, uno de ellos con ocho días publicado, y el marcador
semanal no podía cantarlo porque mide impresiones — y lo que Google no
conoce no tiene impresiones que medir.
Devuelve [(slug, dias, estado)] con SOLO los no indexados, del más viejo al
más nuevo. Si la comprobación no se puede hacer devuelve un único
("", 0, "no se pudo comprobar: …"): un chequeo que no corre tiene que verse,
no dejar el informe limpio por accidente.
"""
import seo_gsc as G
ahora = dt.datetime.now(dt.timezone.utc)
cand = []
for p in posts:
if p.get("status") != "published" or not p.get("published_at"):
continue
try:
pub = dt.datetime.fromisoformat(p["published_at"].replace("Z", "+00:00"))
except ValueError:
continue
dias = (ahora - pub).days
if DIAS_GRACIA <= dias <= DIAS_VENTANA:
cand.append((p["slug"], dias))
if not cand:
return []
# Orden estable. El desempate por slug no es cosmético: con dos posts del
# mismo día el orden dependería del corpus y la huella cambiaría sola,
# reenviando hallazgos viejos como si fueran nuevos.
cand.sort(key=lambda x: (-x[1], x[0]))
try:
tok = G.token(G.carga_credencial())
except Exception as e:
return [("", 0, f"no se pudo comprobar: {type(e).__name__}")]
fuera = []
for slug, dias in cand:
try:
d = G.inspecciona_url(tok, site, slug)
except Exception as e:
fuera.append((slug, dias, f"error: {type(e).__name__}"))
continue
icono, txt = G.etiqueta_indexacion(d)
if icono != "":
fuera.append((slug, dias, txt))
return fuera
# Encabezados en Title Case inglés: el generador del blog ES lo cuela a ratos
# (el 2026-07-21 había 219 en 23 de 29 posts, pero otros del mismo mes salieron
# limpios — es una fuga intermitente, no deuda cerrada). Estas dos listas son
@@ -431,8 +496,12 @@ def check_rules(posts, site):
# ---------- salida ----------
def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, reglas, mayus,
titulos, estado):
titulos, estado, indexa):
L = []
if indexa:
L.append(f"🕳 {len(indexa)} publicado(s) que Google NO ha indexado:")
L += [f" {s[:32]}{e} ({d} días)" if s else f"{e}"
for s, d, e in indexa[:6]]
if roto:
L.append(f"🔴 {len(roto)} enlace(s) interno(s) ROTO(S):")
L += [f" {s[:34]}{u.replace(HOST,'/')}" for s, u in roto[:6]]
@@ -468,9 +537,13 @@ def build_report(roto, nocanon, prematuro, envejecido, n_sitemap, sm_malos, regl
def fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus, titulos,
estado):
estado, indexa):
return json.dumps({
"estado": {k: sorted(v) for k, v in estado.items()},
# Sin los días A PROPÓSITO: la antigüedad sube cada mañana, así que
# meterla aquí haría cambiar la huella sola y el vigilante repetiría el
# mismo hallazgo a diario hasta que dejaras de leerlo.
"indexa": sorted(f"{s}|{e}" for s, _, e in indexa),
"roto": sorted(f"{s}|{u}" for s, u in roto),
"nocanon": sorted(f"{s}|{u}" for s, u in nocanon),
"prematuro": sorted(f"{s}|{t}" for s, t, _, _ in prematuro),
@@ -508,16 +581,17 @@ def run_site(site):
# programados). Van aquí y no en un timer aparte porque comparten lo que
# hace útil a este vigilante: una sola huella por sitio y un solo aviso.
estado = E.revisa(site, posts)
indexa = check_indexacion(posts, site)
body = build_report(roto, nocanon, prematuro, envejecido, n_sm, sm_malos, reglas, mayus,
titulos, estado)
titulos, estado, indexa)
pub = sum(1 for p in posts if p.get("status") == "published")
header = (f"── [{site.upper()}] {cfg['host']}\n"
f" {pub} publicados, {len(posts)} totales, sitemap {n_sm} URLs")
fp = fingerprint(roto, nocanon, prematuro, envejecido, sm_malos, reglas, mayus, titulos,
estado)
estado, indexa)
hallazgos = bool(roto or nocanon or prematuro or envejecido or sm_malos or mayus
or titulos or estado)
or titulos or estado or indexa)
return header + "\n" + body, fp, hallazgos