Tu web responde en HTML. Los agentes de IA quieren Markdown

Cómo servir la misma URL en HTML para navegadores y en Markdown para agentes de IA. Implementación real en Next.js, con el código, los dos fallos que nos costaron tiempo y cómo comprobarlo con curl.

Publicado el 14 de septiembre de 2026

Puntos clave

  • La negociación de contenido consiste en servir la misma URL en dos formatos según lo que pida quien la visita: HTML para un navegador, Markdown para un agente.
  • No es una cabecera suelta. Hay dos fallos que solo aparecen al montarlo: un bucle infinito de peticiones y un CDN sirviendo Markdown a personas.
  • Requiere control del servidor. En Wix o Squarespace no se puede hacer, y esa es precisamente la parte interesante.
  • Esta web lo tiene funcionando. Al final del artículo está el comando para comprobarlo tú mismo.

Qué es la negociación de contenido

La negociación de contenido es una idea vieja de HTTP: el cliente dice en qué formato quiere la respuesta, con la cabecera Accept, y el servidor decide qué representación le devuelve. La URL no cambia. Lo que cambia es lo que viaja por el cable.

Durante veinte años esto se usó sobre todo para elegir idioma o para servir JSON en lugar de HTML a una API. Lo nuevo es el tercer cliente que ha aparecido: los agentes de IA que leen páginas para responder a alguien que les ha preguntado algo.

Por qué importa ahora y no hace dos años

Cuando un modelo lee tu web, no ve una página. Ve un montón de HTML donde la mayor parte no es contenido: son etiquetas de maquetación, scripts, estilos, iconos SVG inline, atributos de accesibilidad. Todo eso ocupa contexto, que es el recurso escaso del agente y por el que alguien paga.

Si la misma página se le entrega en Markdown, el contenido es el mismo y el volumen es una fracción. Le sale más barato leerte, y le resulta más fácil citarte bien. Es un incentivo pequeño y poco romántico, pero es el que mueve esto.

Misma URL, dos representaciones

La implementación tiene dos piezas. La primera intercepta la petición antes de que llegue a la página. En Next.js eso vive en el proxy (lo que en versiones anteriores se llamaba middleware):

const accept = request.headers.get('accept') ?? ''
const pideMarkdown = /\btext\/markdown\b/i.test(accept)

Fíjate en un detalle que parece menor y no lo es: se comprueba text/markdown de forma explícita. No vale aceptar */*, que es lo que manda cualquier curl por defecto y también muchos navegadores. Si lo dieras por bueno, empezarías a servir Markdown a personas sin querer.

La segunda pieza es una ruta que coge el HTML de la propia página y lo convierte. Es decir: la web se pide a sí misma la versión HTML y la traduce. Suena rodado, y lo es, pero tiene una ventaja grande frente a generar el Markdown por separado: no hay dos fuentes que puedan divergir. Si cambias la página, la versión en Markdown cambia con ella, porque sale de ahí.

El bucle infinito que nadie te cuenta

En cuanto montas eso, te encuentras con el primer problema de verdad. La ruta de conversión pide el HTML de la página. Esa petición vuelve a pasar por el proxy. El proxy mira la cabecera Accept, ve que pide Markdown y la manda otra vez a la ruta de conversión. Y así hasta que algo se cae.

La solución es una cabecera interna que marca las peticiones que hace el sistema a sí mismo:

const esPeticionInterna = request.headers.get('x-markdown-passthrough') === '1'

if (pideMarkdown && !esPeticionInterna) {
  // convertir
}

Cuando la ruta de conversión pide el HTML, envía esa cabecera. El proxy la ve, entiende que esa petición es suya y la deja pasar a la página normal. Es una línea, pero sin ella no hay sistema.

El fallo de la portada

El segundo problema fue más difícil de ver, porque el sistema funcionaba: devolvía Markdown correctamente formado, con buena pinta. El detalle era que todas las páginas devolvían el contenido de la portada.

La causa: al reescribir una petición desde el proxy, el query string no llega de forma fiable a la ruta de destino. Se pasaba la ruta pedida como parámetro en la URL, y ese parámetro se perdía por el camino, así que la ruta de conversión usaba su valor por defecto, que era /.

El arreglo es mandar la ruta también en una cabecera, y que la cabecera tenga prioridad:

const ruta =
  request.headers.get('x-markdown-ruta') ??
  request.nextUrl.searchParams.get('ruta') ??
  '/'

Lo cuento porque es el tipo de fallo que no aparece en ningún tutorial: no da error, no rompe nada, y solo lo detectas si pruebas más de una página. Si montas esto, prueba tres URLs distintas antes de darlo por bueno.

Vary: Accept, o cómo un CDN le sirve Markdown a una persona

Una misma URL que devuelve dos cosas distintas es una trampa para cualquier cache. Imagina que un agente pide la página, recibe Markdown, y el CDN se lo guarda. La siguiente persona que entra desde un navegador recibe ese Markdown cacheado, en texto plano, sin diseño.

La cabecera que evita eso es Vary, que le dice a la cache que la respuesta depende del valor de Accept y que tiene que guardar una copia por cada variante:

response.headers.append('Vary', 'Accept')

Hay que declararlo siempre, también en las respuestas HTML normales, no solo cuando devuelves Markdown. Si solo lo pones en una rama, la otra sigue siendo cacheable sin distinción y el problema persiste.

Qué se le quita al HTML antes de convertirlo

La conversión no es volcar el HTML tal cual. Hay partes que no aportan nada a quien lee y sí ocupan contexto. En nuestro caso se eliminan script, style, noscript, template e iframe, y se añade una regla específica para quitar los SVG inline, que son iconos decorativos y ocupan bastante.

También se envía una cabecera con una estimación del tamaño en tokens:

'x-markdown-tokens': String(estimarTokens(markdown))

Es una aproximación deliberada, con la regla de bolsillo de unos cuatro caracteres por token. No pretende ser exacta: sirve para que el agente sepa cuánto contexto le va a costar leer la página antes de leerla. Es opcional, pero es barata y educada.

El segundo nivel: agent skills

Servir Markdown resuelve el cómo lee un agente. Queda el qué sabe que hay. Para eso existe un directorio /.well-known/agent-skills/ con un índice y un fichero por capacidad: cómo contactar, qué servicios hay, dónde está el blog.

La parte que merece la pena copiar es que el índice lleva un digest SHA-256 por fichero. Eso permite a un agente verificar que lo que lee es lo que el índice dice que es. Y tiene una consecuencia práctica: el índice no se puede editar a mano, porque si cambias un fichero y olvidas el digest, el índice miente y un agente que lo verifique descartará la capacidad entera. Por eso se genera con un script, y la descripción de cada capacidad se extrae del propio fichero en lugar de escribirse dos veces.

Por qué no se puede hacer en Wix

Aquí está lo que hace que esto sea interesante y no solo un juguete técnico.

Cloudflare ofrece la conversión a Markdown como una casilla en su panel. Es cómodo, pero exige que tu tráfico pase por su proxy y un plan de pago. Si tu web va directa por otro proveedor, como es nuestro caso, la conversión la tienes que hacer tú.

Y si tu web está en Wix, Squarespace o un constructor parecido, sencillamente no puedes: no controlas el servidor, no puedes interceptar una petición antes de que se sirva la página, no puedes añadir cabeceras propias. Puedes tener una web preciosa y estar en el escalón de abajo sin posibilidad de subir.

Esa es la diferencia real entre una web que un agente puede leer bien y una que no, y no se arregla con contenido ni con plugins.

Compruébalo tú mismo

Lo mejor de este sistema es que no hay que creerse nada. Esta misma página responde en Markdown si se lo pides:

curl -H "Accept: text/markdown" https://genjoprojects.com/es/servicios/seo-local-ia

Y sin esa cabecera, la misma URL te devuelve el HTML de siempre. Si quieres ver las cabeceras que acompañan a la respuesta, incluida la estimación de tokens:

curl -I -H "Accept: text/markdown" https://genjoprojects.com/es

Puedes hacer lo mismo contra tu propia web. Si te devuelve HTML, estás en el escalón de abajo. Que sea un problema o no depende de cuánta gente vaya a preguntarle a un asistente por lo que tú haces, y eso es una apuesta que cada uno hace por su cuenta.

Preguntas frecuentes

¿Esto mejora mi posicionamiento en Google?

No directamente, y desconfía de quien te diga lo contrario. Google indexa el HTML. Esto sirve para los asistentes de IA que leen páginas para responder preguntas, que es un canal distinto y hoy más pequeño. Se hace apostando a que crezca, no porque mueva el ranking mañana.

¿No basta con tener un sitemap y buen HTML semántico?

Ayuda, y es el paso previo. La diferencia es el coste de lectura: un HTML semántico sigue llevando scripts, estilos e iconos que el agente tiene que atravesar. El Markdown le entrega lo mismo sin el envoltorio.

¿Puede romper algo para los visitantes normales?

Puede, si te saltas dos cosas. Si aceptas */* como petición de Markdown, acabarás sirviéndoselo a navegadores. Y si no declaras Vary: Accept, una cache puede guardar la versión Markdown y entregársela a una persona. Con esas dos bien puestas, el visitante normal no nota nada.

¿Cuánto cuesta montarlo?

Poco, si tu web es un proyecto que controláis y alguien puede tocar el código. Son dos ficheros y una librería de conversión. Lo caro no es escribirlo: es darse cuenta de los dos fallos que cuento arriba, que es justo lo que este artículo te ahorra.