Markdown para agentes: servir Accept: text/markdown
Un agente pide tu página y recibe HTML. Después tira casi todo: el menú, el pie, el aviso de cookies, los scripts, los atributos de clase, los iconos SVG. Se queda con unos párrafos de texto.
Esa versión que quería ya la tienes. Casi nadie la sirve.
Lo que cuesta el marcado
Compara lo que envías con lo que sobrevive:
curl -s https://ejemplo.com/about | wc -c
curl -s -H "Accept: text/markdown" https://ejemplo.com/about | wc -c
Si los dos números coinciden, sirves una sola versión a todo el mundo y el trabajo de limpiar lo hace el agente. Ese trabajo se paga. Todo lo que hay dentro del <head>, cada <div> contenedor, cada atributo data- y cada trozo de analítica entra en la ventana de contexto antes de que nada decida que era ruido. En una página cuyo contenido real es un titular y ocho párrafos, la mayoría de los bytes son andamio.
El gemelo en Markdown conserva los niveles de encabezado, los enlaces, las listas, las tablas y los bloques de código. Pierde navegación, estilos y scripts, que nunca iban a acabar citados en ninguna respuesta.
Cómo funciona la negociación con Accept
El cliente dice qué sabe leer:
GET /about HTTP/1.1
Host: ejemplo.com
Accept: text/markdown
Y el servidor elige una representación para esa misma URL:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Un navegador manda Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 y recibe HTML. Un cliente que pide text/markdown recibe Markdown. La URL canónica no cambia, así que sigues teniendo una sola dirección que enlazar y una sola página que compartir.
Vary: Accept, o cómo acaba un navegador descargándose un fichero de texto
El Vary no es opcional y es justo la parte que se salta la gente.
Cualquier caché que haya entre tú y quien lee (tu CDN, un proxy inverso, el propio almacén del navegador) indexa las respuestas por URL. Si sirves dos cuerpos distintos en una misma URL sin avisar, la primera respuesta que se cachee vale para todos. Un agente pide /about con Accept: text/markdown, tu CDN guarda el Markdown y la siguiente persona que abra /about se encuentra una pantalla de asteriscos o una descarga.
Vary: Accept
Esa línea le dice a la caché que incluya la cabecera Accept en su clave, de manera que HTML y Markdown se guardan por separado. Mándala en las dos respuestas, no solo en la de Markdown.
Tiene un coste real: las cabeceras Accept varían mucho de un cliente a otro, así que variar por ellas fragmenta la caché. Es un argumento honesto a favor de la alternativa que viene ahora.
El gemelo .md
La opción sencilla es una segunda URL. /about sirve HTML y /about.md sirve lo mismo en Markdown. Sin negociación, sin Vary, sin nada que se pueda torcer en una caché. Es una URL normal, o sea que se puede enlazar, guardar en marcadores, meter en el sitemap y descargar desde cualquier cliente, incluso desde uno que no mande ninguna cabecera Accept aprovechable.
Luego dile al mundo que existe. En el HTML:
<link rel="alternate" type="text/markdown" href="https://ejemplo.com/about.md">
Y en las cabeceras de la respuesta, para lo que hace un HEAD y nunca llega a leer el cuerpo:
Link: <https://ejemplo.com/about.md>; rel="alternate"; type="text/markdown"
Poner las dos cuesta una línea cada una y cubre a los clientes que solo miran en un sitio.
Comprueba el tuyo
# 1. ¿Responde distinto la misma URL?
curl -s -H "Accept: text/markdown" https://ejemplo.com/about | head -20
# 2. ¿Admite el HTML que el gemelo existe?
curl -sI https://ejemplo.com/about | grep -i "vary\|link"
# 3. ¿Funciona la URL .md por su cuenta?
curl -sI https://ejemplo.com/about.md | grep -i "content-type\|vary"
El segundo comando es el que caza el fallo habitual. Una web puede tener un gemelo .md impecable que no va a encontrar nadie, porque ninguna cabecera y ninguna etiqueta apuntan a él.
Qué hacemos aquí
Todas las páginas de este sitio tienen gemelo en Markdown, y packages/web/src/lib/content-negotiation.js es todo el asunto: unas noventa líneas.
Un mapa fijo lista las rutas estáticas y sus gemelos, o sea / → /index.md, /about → /about.md, cada página de herramienta y así con todas. Las rutas que llevan un identificador o un slug (los informes en /r/<uuid>, las guías, las entradas de blog como esta) se reconocen por patrón y reciben <ruta>.md.
La comprobación de la petición es tosca a propósito: si la cabecera Accept contiene la cadena text/markdown, se envía Markdown. Sin ordenar por valores de q. Un navegador nunca manda esa cadena, así que en la práctica separa bien los dos casos, pero un cliente que liste text/markdown con prioridad baja también se lleva Markdown.
Cada respuesta en Markdown sale con esto:
Content-Type: text/markdown; charset=utf-8
Content-Signal: search=yes, ai-input=yes, ai-train=no
Vary: Accept
X-Markdown-Tokens: 4213
Cache-Control: public, max-age=3600
Content-Signal dice qué se puede hacer con el contenido, no solo si se puede descargar. X-Markdown-Tokens es una marca de tamaño: hoy lleva el número de caracteres del cuerpo, no un recuento real de tokens.
Las respuestas HTML de una página con gemelo llevan Vary: Accept y la cabecera Link, y el <head> recibe la etiqueta rel="alternate" sacada del mismo mapa.
Ese último detalle nos mordió. El mapa alimentaba la plantilla, pero las rutas reconocidas por patrón no estaban en el mapa, así que cada informe de análisis servía su gemelo en Markdown y lo anunciaba en la cabecera Link mientras su HTML no decía nada. El gemelo existía y era invisible para cualquiera que leyese el marcado.
Es una convención, no un estándar
Accept: text/markdown es negociación de contenido HTTP de toda la vida, y eso sí es estándar. Usarla para Markdown es una convención que salió de la propuesta de llms.txt, y ningún rastreador está obligado a pedirlo. Lo honesto es decir que el gemelo .md es la parte que funciona seguro, porque es una URL como otra cualquiera y la descarga quien sea, mientras que la negociación cuesta una cabecera y compensa con los clientes que sí preguntan. Ninguna de las dos te va a hacer daño, y para saber cuál se está usando se miran los logs, no los anuncios.