Back to articles

Pon una auditoría SEO en tu pipeline de despliegue: API, códigos de salida y recetas CI

SEOReport Team·
apici-cdautomatizacióngithub-actionsseo-tecnicodevops

Una receta funcional para auditar tu sitio desde CI: tres endpoints REST, los campos de puntuación que una puerta de construcción puede leer, y la diferencia entre una regresión real y un blip de infraestructura.

Las regresiones técnicas que cuestan más visibilidad son invisibles en una revisión de código. Una etiqueta canonical que comienza a apuntar al host incorrecto, un bloque hreflang que pierde su autorreferencia, un encabezado Content-Security-Policy eliminado de una configuración de reverse-proxy, una ruta que mueve su contenido detrás de la hidratación — cada uno de esos se envía en un diff que parece correcto y pasa todas las pruebas que tienes. Aparecen semanas después, en un gráfico de tráfico, mucho después del commit que los causó se desplaza fuera de la vista.

La solución es estructural: ejecutar la auditoría en el despliegue, no el día que alguien lo recuerda. Cada informe SEOReport está disponible sobre REST, la respuesta lleva un bloque de puntuación numérica, y un script shell puede convertir ese bloque en un código de salida. Ese es todo el mecanismo.

Cuatro clases de regresión que solo una máquina detecta a tiempo

Cuatro modos de fallo sobreviven especialmente bien a la revisión humana, y conviene nombrarlos con precisión, porque son lo que una compuerta de despliegue protege realmente.

Hreflang. Un clúster depende de la autorreferencia, los enlaces de retorno, un x-default sensato, URL bien formadas y alternativas indexables que se canonicalizan a sí mismas. Una migración CMS que reescribe URL patrones puede romper enlaces de retorno en todos los locales a la vez, y ningún locale parece roto en aislamiento.

Canonicalización. Un canonical tiene que existir, apuntar a un destino indexable que no redirija y permanecer en el propio host del sitio. La peor versión de esta falla es un canonical de staging enviado a producción — una página que silenciosamente nombra un host que no quieres indexar.

Encabezados de seguridad. Content-Security-Policy, HSTS, Referrer-Policy, X-Frame-Options, X-Content-Type-Options, y Permissions-Policy. Estos viven en la configuración de infraestructura, no en el código de la aplicación, lo que explica por qué desaparecen durante un cambio de proxy o configuración de edge y nadie se da cuenta.

Paridad de renderizado. Una página puede entregar una versión en su respuesta HTML y otra después de ejecutar sus scripts: un título distinto, encabezados ausentes, un canonical inyectado tarde o texto que solo existe tras la hidratación. Cuando nuestros datos de render-parity alcanzaron un veredicto, la mayoría de los sitios servían una página materialmente más delgada a cualquier cosa que lea HTML como entregado. Un ssr: false en un archivo de configuración basta para mover un sitio a ese grupo.

Los cuatro son determinísticos, los cuatro son baratos de comprobar, y los cuatro son el tipo de cosa que una persona solo inspecciona cuando ya está sospechosa.

Tres puntos finales y un bucle

La superficie API que necesita una canalización es pequeña. El acceso a la API está incluido en los planes de pago y se autentica con un token bearer: emita una clave desde el dashboard y envíela como Authorization: Bearer sr__live_your_api_key. La misma clave abre REST y MCP; la referencia completa vive en la página de desarrolladores.

graph TD A[Despliegue completo] --> B["POST /api/v1/reports"] B --> C["GET /api/v1/reports/:id/ready"] C -->|"ready: false"| C C -->|"error.code present"| D[Señal de infraestructura, salir 75] C -->|"ready: true"| E["GET /api/v1/reports/:id"] E --> F{¿Bloque de puntuación dentro del presupuesto?} F -->|Sí| G[salir 0] F -->|No| H[salir 1, la construcción falla]

Enviar un informe es un POST:

bash
JOB_ID=$(curl -sS -X POST https://seoreport.dev/api/v1/reports \
-H "Authorization: Bearer $SEOREPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "forceRerun": true}' \
| jq -r '.report.jobId')

Dos cosas sobre ese cuerpo importan en CI específicamente.

forceRerun existe porque el API reutiliza una instantánea existente cuando hay una disponible — sensato para una persona que hace clic en un botón, incorrecto para una puerta de despliegue, que de otro modo calificaría la versión anterior. La respuesta te indica qué ocurrió en submission.reusedSnapshot. Las ejecuciones frescas forzadas son una capacidad de cuenta paga; una clave sin ese derecho recibe un 403 nombrando la razón en lugar de devolver silenciosamente datos obsoletos.

El alcance de la auditoría se infiere del camino URL. Un origen simple ejecuta una auditoría completa del sitio; un URL con un camino audita esa página en aislamiento. Así, un pipeline puede bloquear la página de inicio en cada despliegue y agregar la ruta específica que tocó el cambio, sin ningún parámetro extra.

Luego poll. GET /api/v1/reports/:id/ready es el punto final de estado barato — devuelve ready, status, stage, pollAfterMs, y, cuando una ejecución falla, un objeto error:

bash
for _ in $(seq 1 60); do
READY=$(curl -sS -H "Authorization: Bearer $SEOREPORT_API_KEY" \
"https://seoreport.dev/api/v1/reports/$JOB_ID/ready")
CODE=$(echo "$READY" | jq -r '.error.code // empty')
if [ -n "$CODE" ]; then
echo "audit did not complete: $CODE"
exit 75
fi
[ "$(echo "$READY" | jq -r '.ready')" = "true" ] && break
sleep 5
done

Ese exit 75 es deliberado, y es la parte que la mayoría de las integraciones interpreta mal.

Diferenciar una regresión de una ejecución que nunca ocurrió

error.code lleva una razón legible por máquina y un booleano retryable. DNS_FAILURE, CRAWLER_BLOCKED, REDIRECT_LOOP, TLS_ERROR, NO_CONTENT, y STALE_ENGINE_VERSION están marcados como no reintentos, porque reintentar produce la misma respuesta. La mayoría de esa lista describe una condición en el sitio, y varias entradas — un bucle de redirección, un error TLS, una regla de bot que ahora bloquea rastreadores — son defectos de despliegue genuinos que valen la pena fallar una compilación. Los códigos fuera de ese conjunto son transitorios y valen otro intento.

Un gate que combina "el sitio regresó" y "la auditoría no pudo ejecutarse" en la misma compilación roja se desactiva dentro de un mes. Manténgalos separados en sus códigos de salida: 1 para un veredicto que pediste que el gate aplique, 75 — el convencional EX_TEMPFAIL — para una ejecución que no produjo ningún veredicto. La mayoría de los sistemas CI pueden configurarse para reintentar el segundo y avisar a un humano el primero.

Leyendo el bloque de puntuación

Una vez que ready es verdadero, GET /api/v1/reports/:id devuelve el objeto de informe. El bloque score es la parte que lee un gate:

bash
REPORT=$(curl -sS -H "Authorization: Bearer $SEOREPORT_API_KEY" \
"https://seoreport.dev/api/v1/reports/$JOB_ID")
echo "$REPORT" | jq '.report.score
| {overall, totalChecks, passedChecks, failedChecks,
warnChecks, inconclusiveChecks, failingCriticalChecks}'

Tres de esos campos llevan la mayor parte de la señal.

failingCriticalChecks es el conteo de fallos que el motor clasifica como críticos — los que pueden eliminar páginas de un índice o ocultar contenido a un rastreador por completo. Para un primer gate, este es el único número que necesitas, y > 0 es un umbral defensible el día uno.

overall es la puntuación compuesta, útil como una rampa: almacena el valor del despliegue anterior y falla cuando el nuevo cae por más de una tolerancia que elijas. Esto captura la erosión lenta que ninguna verificación individual señala.

domainScores desglosa la puntuación por seo, ai, performance, security, y brand, cada uno con su propio conteo pass, fail, y warn. Los pisos por dominio permiten que equipos diferentes tengan presupuestos distintos — la puntuación de seguridad es un gate significativo para quien controla la configuración de borde, independiente de lo que el equipo de contenido envíe.

inconclusiveChecks merece su propia regla: nunca gatear sobre él. Un informe indica inconclusivo cuando el motor no pudo llegar a un veredicto defensible, y tratar eso como fallo entrena a todos a ignorar la puerta.

El bloque de puntuación forma parte del resumen del informe, por lo que una compuerta de compilación puede leerlo sin descargar los hallazgos completos. Cuando quiera la carga completa para archivarla, GET /api/v1/reports/:id/result devuelve los resultados completos, y GET /api/v1/reports/:id/download?format=json devuelve el mismo objeto como un artefacto descargable — vale la pena adjuntarlo a la construcción para que la diferencia entre dos despliegues sea inspeccionable más tarde.

Conectándolo a un flujo de trabajo

Nada de lo anterior es específico del proveedor CI. En GitHub Actions la puerta completa es un paso que llama al script que ya tienes:

yaml
- name: SEO audit gate
env:
SEOREPORT_API_KEY: ${{ secrets.SEOREPORT_API_KEY }}
AUDIT_URL: https://example.com
run: ./scripts/seo-gate.sh

Donde seo-gate.sh envía, consulta y termina con la decisión:

bash
CRITICAL=$(echo "$REPORT" | jq -r '.report.score.failingCriticalChecks // 0')
SECURITY=$(echo "$REPORT" | jq -r \
'.report.score.domainScores[] | select(.domain == "security") | .score')
if [ "$CRITICAL" -gt 0 ]; then
echo "::error::$CRITICAL critical checks failing"
exit 1
fi
echo "clean — security domain at $SECURITY"

Ejecútalo después de que el despliegue termine en lugar de contra un entorno de vista previa. Una vista previa URL suele estar detrás de autenticación básica o un desafío de bot, y una auditoría que no puede obtener la página informa CRAWLER_BLOCKED en lugar de una puntuación. Post-despliegue contra el origen real es más simple y más cercano a lo que experimenta un rastreador.

Dos formas adyacentes valen la pena conocer. Si tu orquestación ya vive en una plataforma de raspado, nuestro actor Apify ejecuta el mismo motor en un horario sin ninguna de esta pegamento. Y para agentes, la misma clave portador autentica una conexión MCP en https://seoreport.dev/mcp, donde las herramientas de informe se anuncian en connect — un agente investigando una caída de tráfico puede ejecutar la auditoría y leer los hallazgos él mismo, lo cual está documentado junto con la superficie REST en la página de desarrolladores.

Lo que la puerta no cubre

Una puerta de despliegue responde a una pregunta: ¿introdujo esta versión una regresión. Es silenciosa sobre todo lo que cambia sin un despliegue — un certificado que expira, una configuración CDN editada en un panel, un script de terceros que comienza a bloquear la representación, la migración de un competidor que cambia con qué tu canónico colisiona. El monitoreo por dominio es el complemento siempre activo a la puerta: el monitoreo SEO API vuelve a auditar un dominio registrado cada semana con la misma cuenta y la misma clave.

Comienza con la versión estrecha. Un punto final, un campo, failingCriticalChecks > 0, y un honesto exit 75 cuando la auditoría no pudo ejecutarse en absoluto. Una puerta que dispara dos veces al año y es confiable ambas veces vale más que una completa que todos aprendieron a saltar.

Obtenga el diagnóstico completo de su sitio

Un informe basado en evidencia y un plan de acción priorizado, con un plan de créditos mensuales.