seo_gsc: el marcador — datos reales de Search Console
Toda la caja de herramientas miraba hacia dentro (enlaces, sitemap, reglas de metas): decía si el sitio está bien construido, no si alguien lo encuentra. Los únicos datos de resultado en la máquina eran una exportación manual del 2026-06-17, anterior al remate de metas del 23 de junio — o sea, cuarenta días optimizando sin saber el resultado. Cuenta de servicio en vez de OAuth, porque aquí no hay navegador (misma piedra que con Gemini). Sin librerías de Google: el JWT se firma con PyJWT y se canjea a mano, treinta líneas en lugar de media docena de dependencias. Lo que de verdad aporta es `informe`: la lista de páginas que POSICIONAN Y NO SE PINCHAN, con los clics que faltan si el CTR fuese el normal de su posición. Y `antes-despues`, escrito para responder de una vez si el remate del 23-jun sirvió: la API guarda 16 meses, así que el histórico está disponible desde el primer minuto y no hay que esperar semanas para comparar. Probado lo que se puede probar sin credencial: firma RS256 emitida y verificada con su pública, los dos caminos de fallo de la credencial (ausente y con permisos flojos), y la lógica de análisis contra datos sintéticos calcados del caso Nimitz real (pos 6,44 / 2.452 impresiones / 0,16% CTR), que sale marcada con +111 clics de hueco mientras la página sana no se marca. El timer queda instalado y PARADO: sin credencial fallaría cada lunes, y un timer que falla siempre enseña a ignorar los avisos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
# El marcador — Search Console (`seo_gsc.py`)
|
||||
|
||||
Lo que faltaba en toda la caja de herramientas: **datos de resultado**. El
|
||||
resto mira hacia dentro (enlaces, sitemap, reglas de metas); esto mira lo único
|
||||
que decide si el trabajo sirvió — cuánta gente nos encuentra y cuánta pincha.
|
||||
|
||||
Es de **solo lectura**. No escribe en los blogs ni en Search Console.
|
||||
|
||||
## Puesta en marcha (cuatro pasos de navegador, una sola vez)
|
||||
|
||||
La API es gratis. Hace falta una **cuenta de servicio**, no OAuth: en esta
|
||||
máquina no hay navegador y el flujo interactivo no funciona headless — la misma
|
||||
piedra con la que ya tropezamos en Gemini.
|
||||
|
||||
1. **Proyecto y API.** En <https://console.cloud.google.com> crea un proyecto
|
||||
(o reutiliza uno) y activa **Google Search Console API** en
|
||||
*APIs y servicios → Biblioteca*.
|
||||
|
||||
2. **Cuenta de servicio.** *IAM y administración → Cuentas de servicio → Crear*.
|
||||
No necesita ningún rol de IAM: el permiso que importa se da en el paso 4.
|
||||
Luego *Claves → Añadir clave → Crear nueva → JSON* y descarga el fichero.
|
||||
|
||||
3. **La clave, en su sitio.** En esta máquina:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/gsc
|
||||
mv ~/Descargas/EL-FICHERO.json ~/.config/gsc/service-account.json
|
||||
chmod 600 ~/.config/gsc/service-account.json
|
||||
```
|
||||
|
||||
La herramienta **se niega a arrancar** si el fichero es legible por otros.
|
||||
Está fuera de cualquier repo; no la metas en git.
|
||||
|
||||
4. **Permiso en Search Console.** Este es el paso que se olvida. Para **cada
|
||||
una de las dos propiedades** (`theexclusionzone.com` y
|
||||
`zonadeexclusion.com`): *Configuración → Usuarios y permisos → Añadir
|
||||
usuario* → pega el `client_email` de la credencial (algo como
|
||||
`nombre@proyecto.iam.gserviceaccount.com`) → permiso **Restringido**, que
|
||||
basta para leer.
|
||||
|
||||
⚠️ Si `zonadeexclusion.com` no está dada de alta en Search Console, hay que
|
||||
verificarla antes. Del blog ES no había ni una sola exportación, así que es
|
||||
probable que ni exista la propiedad.
|
||||
|
||||
5. **Arrancar el vigilante.** El timer se instala **parado a propósito**: sin
|
||||
credencial fallaría cada lunes, y un timer que falla siempre enseña a
|
||||
ignorar los avisos. Se enciende cuando el canario pasa:
|
||||
|
||||
```bash
|
||||
seo-gsc comprueba && sudo systemctl enable --now seo-gsc-watch.timer
|
||||
```
|
||||
|
||||
Comprobación:
|
||||
|
||||
```bash
|
||||
seo-gsc sitios # tienen que salir las dos propiedades
|
||||
seo-gsc comprueba # canario: recorre el camino real eslabón por eslabón
|
||||
```
|
||||
|
||||
Si el nombre que devuelve `sitios` no coincide con el configurado (puede ser
|
||||
`sc-domain:x.com` o `https://www.x.com/`, con barra final), corrige el
|
||||
diccionario `SITIOS` en `seo_gsc.py` con el nombre exacto.
|
||||
|
||||
## Uso diario
|
||||
|
||||
```bash
|
||||
seo-gsc pull # descarga y acumula en SQLite (idempotente)
|
||||
seo-gsc # sin argumentos = informe de 28 días
|
||||
seo-gsc informe --dias 7
|
||||
```
|
||||
|
||||
La primera descarga se trae **16 meses**, que es todo lo que Google guarda. Eso
|
||||
significa que el histórico está disponible desde el minuto uno: no hay que
|
||||
esperar semanas para tener con qué comparar.
|
||||
|
||||
### La pregunta pendiente
|
||||
|
||||
El remate de metas fue el **23 de junio** y los únicos datos que había eran del
|
||||
17. En cuanto haya credencial:
|
||||
|
||||
```bash
|
||||
seo-gsc antes-despues --corte 2026-06-23 --dias 28
|
||||
```
|
||||
|
||||
## Qué mira el informe
|
||||
|
||||
- **Movimiento** de clics, impresiones y CTR contra el periodo anterior.
|
||||
- **Páginas que posicionan y no se pinchan.** Es la lista que importa: si algo
|
||||
está en posición 6 con 2.400 impresiones y 0,16% de CTR, no tiene un problema
|
||||
de posicionamiento, tiene un problema de titular. Cada línea estima los clics
|
||||
que faltan si el CTR fuese el normal de esa posición.
|
||||
- **Consultas con impresiones y cero clics**, que son temas donde ya salimos y
|
||||
no convencemos.
|
||||
|
||||
El CTR "normal" sale de una curva de referencia del sector, no de una medición
|
||||
nuestra: **sirve para ordenar candidatas, no para prometer tráfico**. Solo se
|
||||
avisa por encima de 150 impresiones, porque por debajo el CTR es ruido.
|
||||
|
||||
## Automático
|
||||
|
||||
`seo-gsc-watch.timer` — lunes 06:15 UTC: descarga y manda el informe por
|
||||
Telegram, pero **solo si hay páginas desperdiciadas y el informe ha cambiado**
|
||||
desde el último aviso. Misma disciplina que `seo-watch`: si no hay nada que
|
||||
decir, se calla.
|
||||
|
||||
```bash
|
||||
systemctl status seo-gsc-watch.timer
|
||||
journalctl -u seo-gsc-watch -n 40
|
||||
```
|
||||
|
||||
## Notas de implementación
|
||||
|
||||
- **Sin librerías de Google.** El JWT se firma con PyJWT y se canjea a mano.
|
||||
Son treinta líneas y evita arrastrar media docena de dependencias para tres
|
||||
peticiones.
|
||||
- **Los datos tardan 2-3 días en consolidarse.** Todo termina 3 días atrás y se
|
||||
pide `dataState=final`; pedir "ayer" devuelve cifras que luego cambian solas.
|
||||
- **La posición se pondera por impresiones.** Promediar posiciones a pelo da el
|
||||
mismo peso a un día de 2 impresiones que a uno de 500.
|
||||
- **La API pagina de 25.000 en 25.000** y no avisa de que quedan más filas.
|
||||
- El almacén es SQLite en `~/.local/state/seo-gsc/gsc.db`; `pull` es idempotente
|
||||
y resolapa un día por si el último se consolidó después.
|
||||
Reference in New Issue
Block a user