La teva web respon en HTML. Els agents d’IA volen Markdown

Com servir la mateixa URL en HTML per a navegadors i en Markdown per a agents d’IA. Implementació real en Next.js, amb el codi, els dos errors que ens van costar temps i com comprovar-ho amb curl.

Publicat el 14 de setembre del 2026

Punts clau

  • La negociació de contingut consisteix a servir la mateixa URL en dos formats segons què demani qui la visita: HTML per a un navegador, Markdown per a un agent.
  • No és una capçalera solta. Hi ha dos errors que només apareixen en muntar-ho: un bucle infinit de peticions i un CDN servint Markdown a persones.
  • Requereix control del servidor. A Wix o Squarespace no es pot fer, i aquesta és precisament la part interessant.
  • Aquesta web ho té funcionant. Al final de l’article hi ha l’ordre per comprovar-ho tu mateix.

Què és la negociació de contingut

La negociació de contingut és una idea vella d’HTTP: el client diu en quin format vol la resposta, amb la capçalera Accept, i el servidor decideix quina representació li torna. La URL no canvia. El que canvia és el que viatja pel cable.

Durant vint anys això es va fer servir sobretot per triar idioma o per servir JSON en lloc d’HTML a una API. El que és nou és el tercer client que ha aparegut: els agents d’IA que llegeixen pàgines per respondre a algú que els ha preguntat alguna cosa.

Per què importa ara i no fa dos anys

Quan un model llegeix la teva web, no veu una pàgina. Veu un munt d’HTML on la major part no és contingut: són etiquetes de maquetació, scripts, estils, icones SVG inline, atributs d’accessibilitat. Tot això ocupa context, que és el recurs escàs de l’agent i pel qual algú paga.

Si la mateixa pàgina se li lliura en Markdown, el contingut és el mateix i el volum és una fracció. Li surt més barat llegir-te, i li resulta més fàcil citar-te bé. És un incentiu petit i poc romàntic, però és el que mou això.

Mateixa URL, dues representacions

La implementació té dues peces. La primera intercepta la petició abans que arribi a la pàgina. A Next.js això viu al proxy (el que en versions anteriors s’anomenava middleware):

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

Fixa’t en un detall que sembla menor i no ho és: es comprova text/markdown de manera explícita. No val acceptar */*, que és el que envia qualsevol curl per defecte i també molts navegadors. Si ho donessis per bo, començaries a servir Markdown a persones sense voler.

La segona peça és una ruta que agafa l’HTML de la pàgina mateixa i el converteix. És a dir: la web es demana a si mateixa la versió HTML i la tradueix. Sona rodat, i ho és, però té un avantatge gran respecte a generar el Markdown per separat: no hi ha dues fonts que puguin divergir. Si canvies la pàgina, la versió en Markdown canvia amb ella, perquè en surt.

El bucle infinit que ningú no t’explica

Tan bon punt muntes això, et trobes amb el primer problema de debò. La ruta de conversió demana l’HTML de la pàgina. Aquesta petició torna a passar pel proxy. El proxy mira la capçalera Accept, veu que demana Markdown i la torna a enviar a la ruta de conversió. I així fins que alguna cosa cau.

La solució és una capçalera interna que marca les peticions que el sistema es fa a si mateix:

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

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

Quan la ruta de conversió demana l’HTML, envia aquesta capçalera. El proxy la veu, entén que aquella petició és seva i la deixa passar a la pàgina normal. És una línia, però sense ella no hi ha sistema.

L’error de la portada

El segon problema va ser més difícil de veure, perquè el sistema funcionava: tornava Markdown ben format, amb bona pinta. El detall era que totes les pàgines tornaven el contingut de la portada.

La causa: en reescriure una petició des del proxy, el query string no arriba de manera fiable a la ruta de destí. Es passava la ruta demanada com a paràmetre a la URL, i aquell paràmetre es perdia pel camí, així que la ruta de conversió feia servir el seu valor per defecte, que era /.

L’arranjament és enviar la ruta també en una capçalera, i que la capçalera tingui prioritat:

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

Ho explico perquè és el tipus d’error que no apareix en cap tutorial: no dona error, no trenca res, i només el detectes si proves més d’una pàgina. Si muntes això, prova tres URL diferents abans de donar-ho per bo.

Vary: Accept, o com un CDN serveix Markdown a una persona

Una mateixa URL que torna dues coses diferents és un parany per a qualsevol cache. Imagina que un agent demana la pàgina, rep Markdown, i el CDN se’l guarda. La següent persona que hi entra des d’un navegador rep aquell Markdown cachejat, en text pla, sense disseny.

La capçalera que ho evita és Vary, que diu a la cache que la resposta depèn del valor d’Accept i que ha de guardar una còpia per cada variant:

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

Cal declarar-ho sempre, també a les respostes HTML normals, no només quan tornes Markdown. Si només ho poses en una branca, l’altra continua sent cachejable sense distinció i el problema persisteix.

Què se li treu a l’HTML abans de convertir-lo

La conversió no és abocar l’HTML tal qual. Hi ha parts que no aporten res a qui llegeix i sí que ocupen context. En el nostre cas s’eliminen script, style, noscript, template i iframe, i s’afegeix una regla específica per treure els SVG inline, que són icones decoratives i ocupen força.

També s’envia una capçalera amb una estimació de la mida en tokens:

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

És una aproximació deliberada, amb la regla de butxaca d’uns quatre caràcters per token. No pretén ser exacta: serveix perquè l’agent sàpiga quant context li costarà llegir la pàgina abans de llegir-la. És opcional, però és barata i educada.

El segon nivell: agent skills

Servir Markdown resol el com llegeix un agent. Queda el què sap que hi ha. Per això existeix un directori /.well-known/agent-skills/ amb un índex i un fitxer per capacitat: com contactar, quins serveis hi ha, on és el blog.

La part que val la pena copiar és que l’índex porta un digest SHA-256 per fitxer. Això permet a un agent verificar que el que llegeix és el que l’índex diu que és. I té una conseqüència pràctica: l’índex no es pot editar a mà, perquè si canvies un fitxer i oblides el digest, l’índex menteix i un agent que ho verifiqui descartarà la capacitat sencera. Per això es genera amb un script, i la descripció de cada capacitat s’extreu del fitxer mateix en lloc d’escriure’s dues vegades.

Per què no es pot fer a Wix

Aquí hi ha el que fa que això sigui interessant i no només una joguina tècnica.

Cloudflare ofereix la conversió a Markdown com una casella al seu panell. És còmode, però exigeix que el teu trànsit passi pel seu proxy i un pla de pagament. Si la teva web va directa per un altre proveïdor, com és el nostre cas, la conversió l’has de fer tu.

I si la teva web és a Wix, Squarespace o un constructor semblant, senzillament no pots: no controles el servidor, no pots interceptar una petició abans que se serveixi la pàgina, no pots afegir capçaleres pròpies. Pots tenir una web preciosa i ser a l’esglaó de sota sense possibilitat de pujar.

Aquesta és la diferència real entre una web que un agent pot llegir bé i una que no, i no s’arregla amb contingut ni amb plugins.

Comprova-ho tu mateix

El millor d’aquest sistema és que no cal creure’s res. Aquesta mateixa pàgina respon en Markdown si l’hi demanes:

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

I sense aquesta capçalera, la mateixa URL et torna l’HTML de sempre. Si vols veure les capçaleres que acompanyen la resposta, inclosa l’estimació de tokens:

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

Pots fer el mateix contra la teva pròpia web. Si et torna HTML, ets a l’esglaó de sota. Que això sigui un problema o no depèn de quanta gent preguntarà a un assistent pel que tu fas, i aquesta és una aposta que cadascú fa pel seu compte.

Preguntes freqüents

Això millora el meu posicionament a Google?

No directament, i desconfia de qui et digui el contrari. Google indexa l’HTML. Això serveix per als assistents d’IA que llegeixen pàgines per respondre preguntes, que és un canal diferent i avui més petit. Es fa apostant que creixi, no perquè mogui el rànquing demà.

No n’hi ha prou amb tenir un sitemap i bon HTML semàntic?

Ajuda, i és el pas previ. La diferència és el cost de lectura: un HTML semàntic continua portant scripts, estils i icones que l’agent ha de travessar. El Markdown li lliura el mateix sense l’embolcall.

Pot trencar alguna cosa per als visitants normals?

Pot, si et saltes dues coses. Si acceptes */* com a petició de Markdown, acabaràs servint-lo a navegadors. I si no declares Vary: Accept, una cache pot guardar la versió Markdown i lliurar-la a una persona. Amb aquestes dues ben posades, el visitant normal no nota res.

Quant costa muntar-ho?

Poc, si la teva web és un projecte que controleu i algú pot tocar el codi. Són dos fitxers i una llibreria de conversió. El car no és escriure-ho: és adonar-se dels dos errors que explico a dalt, que és justament el que aquest article t’estalvia.