Cabeceras HTTP y SEO: las que Googlebot sí lee

Cabeceras HTTP y SEO: las que Googlebot sí lee

Cuando Googlebot pide una URL, la respuesta que recibe no es solo el HTML que ve un visitante. Antes del cuerpo viaja un bloque de cabeceras que el navegador nunca muestra en pantalla, pero que el rastreador sí procesa: decide si Google entiende de qué tipo de recurso se trata, si le conviene descargar la página entera otra vez o puede confiar en la copia que ya tiene, y qué versión debe consolidar cuando conviven varias URLs casi idénticas. Nada de esto se ve con "ver código fuente"; solo aparece inspeccionando la respuesta cruda del servidor.

Este artículo se centra en cinco cabeceras: Content-Type, Cache-Control, Vary, el par Last-Modified/ETag con sus respuestas 304, y Link rel="canonical" enviado como cabecera en vez de en el <head>. No repite lo que ya cubre una cabecera de indexación como X-Robots-Tag, ni entra en la arquitectura interna de una CDN. El foco es más estrecho: las peticiones condicionales y Vary, que determinan si Google recibe la versión correcta de una página y si tu servidor gasta presupuesto de rastreo descargando contenido que no cambió.

Content-Type: la cabecera que decide cómo se interpreta la respuesta

Antes de mirar el contenido, Google mira el tipo de recurso que declara Content-Type: la diferencia entre tratar una respuesta como HTML indexable, un PDF, una imagen o un JSON que no tiene sentido mostrar en resultados. Si el valor declarado no coincide con lo que realmente se envía, el rastreador puede interpretar mal el recurso aunque el contenido esté bien formado.

El caso más frecuente aparece en endpoints dinámicos que devuelven HTML declarado como text/plain, o datos marcados como text/html cuando deberían ser application/json. También importa el charset (charset=utf-8): si falta o no coincide con la codificación real, los acentos pueden mostrarse mal en el snippet aunque la página se vea correcta en el navegador, que aplica sus propias heurísticas.

  • Verifica que cada tipo de recurso (HTML, PDF, imagen, feed) declare el Content-Type que le corresponde, no un valor genérico heredado del servidor.
  • Un endpoint que a veces sirve JSON y a veces HTML debe declarar el tipo correcto en cada petición según lo que realmente devuelve.

Last-Modified y ETag: cómo sabe Google que una página no cambió sin descargarla entera

Cuando Googlebot ya rastreó una URL, puede enviar después una petición condicional en vez de pedir el documento completo: añade If-Modified-Since (la fecha recibida en Last-Modified) o If-None-Match (el valor recibido en ETag). Si el contenido no cambió, el servidor responde con 304 Not Modified y cuerpo vacío, sin reenviar la página.

Esto no es un detalle de infraestructura ajeno al SEO: cada 304 que el servidor emite en vez de un 200 completo es una petición más barata de procesar, lo que a escala de un sitio grande influye en cuántas URLs puede permitirse rastrear Google dentro de la misma ventana de presupuesto. Un sitio que siempre responde 200 completo, aunque el contenido sea idéntico al de la última visita, gasta ese presupuesto en trabajo que no aporta nada nuevo.

Last-Modified y ETag no son intercambiables. Last-Modified es una fecha con resolución de segundos: dos versiones guardadas en el mismo segundo no se distinguen con ella. ETag es un identificador que el servidor genera a partir del contenido, "fuerte" (idéntico byte a byte) o "débil" (con el prefijo W/). Con ambas presentes, el servidor debería priorizar If-None-Match, más preciso que una fecha.

El error más común es generar Last-Modified o ETag a partir de algo que cambia en cada petición aunque el contenido visible sea el mismo: un timestamp de renderizado, o un ETag distinto en cada servidor de un balanceador de carga. El resultado es que el servidor nunca emite un 304, porque desde su punto de vista el contenido "cambió" siempre, aunque para Google no haya cambiado nada.

  • Comprueba si tu servidor responde 304 al reenviar la misma petición con el If-None-Match o If-Modified-Since que recibiste la primera vez.
  • Si usas balanceo de carga, asegúrate de que el ETag se genera igual en todas las instancias para el mismo contenido.

Cache-Control: quién puede guardar una copia de tu página y durante cuánto tiempo

Cache-Control le dice a cualquier intermediario -navegador, proxy, cache delante del servidor- si puede guardar la respuesta y por cuánto tiempo servirla sin preguntarle al origen. Los valores relevantes son public (cualquier cache la guarda), private (solo el navegador del usuario), no-cache (se guarda, pero debe revalidarse antes de reutilizarse) y no-store (no se guarda en ningún sitio). max-age=N indica cuántos segundos es válida la copia.

Para el rastreo, el efecto es que Cache-Control determina cuánto tiempo sigue circulando una versión desactualizada en las capas intermedias por las que pasa el tráfico real, incluido el que llega desde un resultado de búsqueda. Si publicas un cambio de precio con Cache-Control: public, max-age=86400, una cache intermedia puede seguir sirviendo la versión anterior ese día completo, aunque Google ya haya vuelto a rastrear la página.

El caso contrario también ocurre: páginas estables llevan no-store por herencia de configuración, y cada petición golpea el origen desde cero, aumentando el tiempo de respuesta bajo carga -una señal que sí se mide-. La recomendación es que max-age refleje la frecuencia real de cambio: minutos para stock cambiante, días para un artículo ya publicado. must-revalidate exige comprobar con el origen al caducar; stale-while-revalidate sirve la copia caducada mientras se pide una nueva en segundo plano.

  • Ajusta max-age al ritmo real de cambio del contenido: no uses el mismo valor para una portada de ofertas y para un artículo estático.
  • Revisa si alguna capa intermedia está sobrescribiendo el Cache-Control que envía tu código de origen.

Vary: la cabecera que puede hacer que Google vea una versión distinta a la que esperas

Vary le dice a cualquier cache que la respuesta de una URL puede variar según otra cabecera de la petición, y que debe guardar una copia por combinación de valores, no una sola compartida. El uso más razonable es Vary: Accept-Encoding, porque el servidor responde comprimido con gzip o brotli según lo que el cliente acepte.

El problema aparece con usos menos disciplinados. Vary: User-Agent es tentador para servir una plantilla distinta a móvil y escritorio desde la misma URL, pero el universo de cadenas de User-Agent es enorme: muchas caches dejan de guardar nada, o agrupan de forma imprecisa y sirven a Googlebot una variante pensada para otro cliente. Suele ser más fiable resolverlo con diseño responsive que depender de que toda la cadena de caches respete Vary correctamente.

En sitios multilingües, Vary: Accept-Language tiene una implicación que rara vez se documenta: si una URL puede devolver español o inglés según esa cabecera, pero alguna cache intermedia no la respeta de forma consistente, Google puede rastrearla una vez, guardar la versión que le tocó, y no volver a comprobar si existe otra. Combinado con hreflang esto produce una discrepancia: las etiquetas apuntan a esa URL como versión inglesa, pero lo indexado es la respuesta en español de una cache que ignoró el Vary.

Vary: Cookie es la combinación más peligrosa con caching agresivo: si una cache guarda una respuesta que dependía de una cookie de sesión y la sirve después sin comprobarla bien, contenido de un usuario puede mostrarse a otro. Una página que dependa de Vary: Cookie no debería estar en cache compartida; debería marcarse private.

  • Limita Vary a las cabeceras que realmente cambian el contenido, no a cualquiera presente por costumbre.
  • Con Vary: Accept-Language, comprueba con una petición manual que recibes contenido distinto y no una copia de otro idioma.
  • Evita Vary: User-Agent para contenido por dispositivo; resuélvelo con diseño responsive, no con una cabecera que las caches interpretan de forma desigual.

Link rel="canonical" como cabecera HTTP, no solo en el <head>

La forma más conocida de declarar una URL canónica es la etiqueta <link rel="canonical" href="..."> dentro del <head>. Pero esa etiqueta solo existe si el recurso es HTML y tiene un <head> donde ponerla. Para un PDF, una imagen o cualquier recurso no-HTML, la única forma de declarar canonical es la cabecera HTTP Link: Link: <https://ejemplo.com/informe.pdf>; rel="canonical".

Esto es útil cuando el mismo PDF es accesible desde varias URLs: una del propio dominio y otra reenviada por un gestor documental, o una con parámetros de tracking que apunta al mismo archivo. Sin la cabecera Link, Google no tiene señal de canonicalización sobre ese recurso más allá de los enlaces internos.

En páginas HTML, la cabecera Link puede coexistir con la etiqueta del <head>, y no hace falta duplicarla si ambas coinciden. Importa cuando no coinciden: si un CDN añade una cabecera Link con una URL distinta a la que declara el propio HTML -algo que ocurre cuando esa capa se configura de forma genérica- se envían dos señales contradictorias sobre la misma URL.

  • Usa la cabecera Link con rel="canonical" para cualquier recurso no-HTML accesible desde más de una URL.
  • Si tu CDN añade cabeceras Link automáticamente, verifica que el valor coincide con el canonical real de cada página.

Cómo auditar estas cabeceras en la práctica

La herramienta más directa es curl, porque muestra lo que el servidor devuelve sin que el navegador reescriba nada. curl -I https://tudominio.com/pagina muestra solo las cabeceras, sin descargar el cuerpo. Para probar una petición condicional, se guarda el ETag o Last-Modified de esa respuesta y se repite añadiendo -H "If-None-Match: valor"; si el servidor está bien configurado, debería devolver 304.

Para comprobar Vary, se repite la petición cambiando el valor de la cabecera que declara (-H "Accept-Language: en" frente a es) y se compara si el contenido realmente cambia, o si una recibe una copia cacheada que no le corresponde.

En el navegador, la pestaña Red del inspector muestra las cabeceras de cada petición, incluyendo si vino de cache local o de red; conviene desactivarla mientras se depura. En Search Console, la inspección de URL muestra, en «Ver página rastreada», el código de respuesta y las cabeceras recibidas en el último rastreo. Para confirmar si realmente se emiten 304 al rastreador, la fuente de verdad son los logs del servidor, filtrando el user-agent de Googlebot a una misma URL durante varios días.

  • curl -I para ver cabeceras sin descargar el cuerpo.
  • curl con -H "If-None-Match: ..." para comprobar si el servidor responde 304.
  • Repetir la petición cambiando la cabecera que Vary declara, para confirmar que el contenido cambia como debería.
  • Logs del servidor por user-agent de Googlebot para ver el patrón real de 200 frente a 304 en el tiempo.

Errores que rompen el comportamiento esperado

La mayoría de los problemas con estas cabeceras no vienen de no usarlas, sino de usarlas de forma inconsistente entre capas: el código de la aplicación envía un valor, una capa intermedia lo modifica sin que nadie lo note, y lo que llega a Googlebot no es lo que el desarrollador cree que envía. Auditar con curl contra la URL pública, no contra el origen en red interna, es lo que revela el problema real. Algunos patrones concretos vale la pena revisarlos en cualquier sitio:

  • Last-Modified que cambia en cada petición por generarse desde la hora de renderizado, no la de edición real: el servidor nunca emite 304.
  • ETag distinto por servidor detrás de un balanceador sin lógica compartida: la misma petición recibe un ETag diferente solo porque la atendió otra instancia.
  • Cache-Control: no-store en contenido en realidad estable, lo que multiplica la carga en el origen sin ganar frescura.
  • Vary: * combinado con cache compartida, que en la práctica anula cualquier beneficio de esa cache.
  • Cabeceras Link con rel=canonical añadidas por una CDN con un valor genérico que no coincide con el canonical real del HTML.
  • Confiar en que el servidor soporta peticiones condicionales sin haberlo probado: muchas configuraciones por defecto ignoran If-None-Match y devuelven 200 siempre.

Preguntas frecuentes

¿Cache-Control afecta directamente el ranking de una página?

No es un factor de ranking en sí mismo, pero influye en el tiempo de respuesta bajo carga y en si los usuarios que llegan desde un resultado de búsqueda ven una versión desactualizada durante un tiempo. Ambos efectos sí se relacionan con señales que Google mide, como la velocidad y la experiencia de la página.

¿Qué diferencia hay entre ETag y Last-Modified para efectos de rastreo?

Last-Modified es una fecha con resolución de segundos; ETag es un identificador que el servidor genera a partir del contenido y puede ser mucho más preciso. Cuando ambas están presentes, un servidor bien configurado debería priorizar If-None-Match sobre If-Modified-Since si el cliente envía las dos.

¿Necesito la cabecera Link rel=canonical si ya tengo la etiqueta en el <head>?

Para páginas HTML normales, la etiqueta en el <head> suele ser suficiente y no hace falta duplicarla como cabecera. La cabecera Link es imprescindible únicamente para recursos que no son HTML, como PDFs o imágenes, donde no existe un <head> donde colocarla.

¿Es buena idea usar Vary: User-Agent para servir una versión distinta a móvil y a escritorio?

En la práctica es una fuente frecuente de problemas, porque el número de cadenas de User-Agent distintas es enorme y muchas caches lo gestionan mal, ya sea fragmentando demasiado o agrupando de forma imprecisa. Suele ser más fiable resolver la adaptación por dispositivo con diseño responsive en la misma URL.

¿Cómo compruebo si mi servidor realmente responde con 304 Not Modified?

Haz una petición con curl -I a la URL, guarda el ETag o Last-Modified de la respuesta, y repítela añadiendo -H "If-None-Match: ese-valor" o -H "If-Modified-Since: esa-fecha". Si el contenido no cambió, la segunda respuesta debería ser un 304 con el cuerpo vacío en lugar de un 200 completo.

Todos los artículos