Servirle markdown a los agentes
Cuando ChatGPT, Claude o Perplexity visitan tu web para responder algo, reciben lo mismo que un navegador: navegación, footer, scripts y JSON-LD. El artículo es una fracción de eso, y el resto son tokens que el modelo gasta en descartar plantilla. Esta página documenta la capa que arregla eso en angelcruz.dev, con el código y las decisiones.
La misma URL, dos tamaños
Medido sobre /guia-mcp en producción. No es una estimación: son las dos respuestas que da el servidor ahora mismo según lo que pidas en la cabecera Accept.
Se comprueba en dos comandos:
curl -sI https://www.angelcruz.dev/guia-mcp
# content-type: text/html; charset=utf-8
curl -sI -H "Accept: text/markdown" https://www.angelcruz.dev/guia-mcp
# content-type: text/markdown; charset=utf-8
# vary: Accept
# x-markdown-tokens: 2350La cabecera x-markdown-tokens es propia y está para lo que parece: que quien consume el recurso sepa lo que le va a costar antes de descargarlo.
Cuatro capas
Ninguna depende de las otras: se pueden implementar por separado y en este orden, que va de la que más ahorra a la que menos.
Negociación de contenido
La misma URL devuelve HTML a un navegador y markdown a un agente, según la cabecera Accept. No hay una versión duplicada del contenido que se pueda desincronizar: el markdown se deriva del HTML canónico.
- Accept: text/markdown en cualquier ruta de contenido
- Sufijo .md para el agente que no manda cabeceras
- Cabecera Vary: Accept, para que las cachés no mezclen las dos
- Link rel=canonical apuntando siempre a la URL con HTML
Descubrimiento
Un agente que aterriza en cualquier página recibe en la cabecera HTTP Link el mapa entero del sitio. No tiene que adivinar rutas ni rastrear para encontrarlas.
- llms.txt y llms-full.txt (llmstxt.org)
- Sitemaps en XML y en markdown
- AGENTS.md, la guía para quien consume el sitio
- API catalog (RFC 9727) y OpenAPI 3.1
Identidad
Responde a «quién es este sitio y de qué habla» sin que el modelo lo tenga que inferir del texto. Cada entidad va con su evidencia citada y atribuida, no como una afirmación suelta.
- Organización, persona, servicios y taxonomía editorial
- Gemelo en HTML legible, no solo el JSON
- JSON-LD por entidad con la atribución en texto visible
Errores legibles
Un 404 pedido en markdown no devuelve la página de error en HTML. Devuelve Problem Details, con un código estable y una salida concreta.
- application/problem+json en vez de HTML
- Campo resolution: qué hacer para arreglarlo
- Las URLs de discovery en el propio cuerpo del error
Un 404 que dice qué hacer
Es la diferencia entre un agente que se rinde y uno que se corrige solo. Si pides una ruta que no existe con Accept: text/markdown, esto es lo que devuelve el sitio.
Lo habitual
La página 404 en HTML. El agente recibe 40 KB de plantilla y tiene que deducir que falló, sin ninguna pista de qué probar después.
Problem Details
{
"status": 404,
"code": "markdown_not_found",
"detail": "No existe una página en …",
"resolution": "Revisa la ruta contra
/llms.txt o /sitemap.md, que listan
todas las URLs publicadas."
}El campo resolution no es estándar: RFC 9457 permite extender el objeto, y ahí cabe la única información que de verdad desbloquea a quien llamó. Un code estable encima, para que se pueda ramificar sobre él sin parsear prosa.
Cinco decisiones
Las trampas que aparecen al implementar esto y que no salen en la documentación de nadie, porque solo se descubren cuando algo se rompe.
- 00
Por qué no un catch-all para el sufijo .md
Un rewrite de `/:path*.md` se tragaría `/sitemap.md` y `/AGENTS.md`, que son rutas reales con su propio handler. Las rutas literales se generan una a una desde el inventario, y el catch-all se deja solo en `fallback`, que Next evalúa después de archivos y páginas: ahí ya no puede interceptar nada real.
- 01
Por qué el handler se pide su propio HTML
El conversor genérico hace un self-fetch de la página canónica con `Accept: text/html` y la convierte. Suena a rodeo y evita el problema de fondo: mantener a mano una versión markdown de cada página es garantizar que se desincronice. Lo que se sirve siempre sale de lo que se publicó, y el `Accept` explícito evita que el rewrite se dispare contra sí mismo.
- 02
Por qué el rel del header apunta al RFC y no al $schema
El índice de Agent Skills Discovery trae un `$schema` que es un identificador opaco y ni siquiera resuelve. En un header `Link` eso manda al agente contra una pared, así que el `rel` es la URL del RFC, que sí abre y explica el formato.
- 03
Por qué search-index.json no lleva rel
El `rel` estándar que le tocaría es `search`, y los navegadores esperan un documento OpenSearch en XML detrás de ese valor. Declararlo sería mentir sobre el formato, así que la superficie se lista en llms.txt y en las tablas humanas, pero se queda fuera del header.
- 04
Por qué un solo inventario y no cinco listas
Las rutas y las superficies vivían duplicadas en los rewrites, el header, la tabla de AGENTS.md, el footer y el sitemap. Cinco sitios donde olvidarse de una. Ahora es un módulo de datos puro, sin imports de runtime, que se puede importar tanto desde `next.config` en build como desde los route handlers.
Todo lo que expone este sitio
Esta tabla se genera desde el mismo módulo que alimenta los rewrites y la cabecera Link, así que no puede quedarse desfasada respecto a lo que el servidor sirve de verdad.
- /llms.txt
Índice estilo llmstxt.org de cada página y post
- /llms-full.txt
Corpus completo con el cuerpo de cada post inline en markdown
- /sitemap.xml
Sitemap XML estándar (páginas estáticas y categorías)
- /sitemap-posts.xml
Sitemap XML solo de posts, con <lastmod>
- /sitemap.md
Versión markdown jerárquica del sitemap
- /AGENTS.md
Guía para agentes que consumen el sitio
- /feed.xml
RSS de los artículos del blog
- /til.xml
RSS solo de los TIL (notas breves), separado del de artículos
- /til.md
Todos los TIL en markdown, sin el chrome de la página
- /entitymap.json
Índice de entidades del sitio según EntityMap v1.0, con la evidencia citada y atribuida
- /entitymap.html
Gemelo legible del EntityMap, con JSON-LD por entidad y la atribución en texto visible
- /manifest.webmanifest
Web app manifest
- /.well-known/api-catalog
API catalog (RFC 9727)
- /openapi.json
Descripción OpenAPI 3.1 de los endpoints públicos de solo lectura
- /.well-known/agent-skills/index.json
Skills que publica el sitio (Agent Skills Discovery RFC v0.2.0), cada una con su digest sha256
- /search-index.json
Índice de todos los artículos indexables (título, slug, extracto recortado y categoría) en un solo JSON
- /robots.txt
Permite GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot y otros
Y en robots.txt, una declaración de Content Signals: search=yes, ai-input=yes, ai-train=no. Se permite buscar y responder con el contenido, y se pide no usarlo para entrenar. Es una declaración de intenciones, no una barrera técnica, y por eso conviene decirla explícitamente.
Lo que viene después de servir markdown
Esta página cubre la capa de lectura: que un agente pueda consumir el contenido barato. Estos dos artículos van al paso siguiente, que es que pueda encontrarte y actuar.
DNS-AID: descubrimiento a través de DNS
Todo lo de esta página asume que el agente ya llegó a una URL tuya. DNS-AID es la propuesta para el paso anterior: que te encuentre desde el propio dominio, sin descargar nada primero.
WebMCP: de leer tu web a usarla
La capa de lectura termina donde empieza esta: una API para que tu página registre herramientas que un agente del navegador puede llamar. Leer barato es el suelo, actuar es el techo.
¿Quieres esto en tu sitio?
Si te interesa montar esta capa en tu proyecto, o auditar la que ya tienes, escríbeme y lo vemos. Y si prefieres implementarlo tú, todo lo de arriba está enlazado a su especificación.