Implementación · Next.js

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.

El problema, en una cifra

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.

121 KB
text/htmlLo que recibe un navegador
9 KB
text/markdownLo que recibe un agente
92%
menosEl resto era plantilla

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: 2350

La 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.

Cómo está montado

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.

27 rutas

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
17 superficies

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
EntityMap v1.0

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
RFC 9457

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
La capa que casi nadie hace

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.

Lo que no se ve

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.

Inventario

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.

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.

Para seguir

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.

Siguiente paso

¿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.