seo-watch: vigila la indexación en Google (ventana 5-60 días, huella sin la antigüedad)
This commit is contained in:
+79
-5
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user