Markdown per gli agenti: servire Accept: text/markdown
Un agente chiede la tua pagina e riceve HTML. Poi ne butta via quasi tutto: il menu, il footer, il banner dei cookie, gli script, gli attributi di classe, le icone SVG. Tiene qualche paragrafo di testo.
La versione che voleva ce l'hai già. Quasi nessuno la serve.
Quanto costa il markup
Confronta quello che invii con quello che sopravvive:
curl -s https://esempio.com/about | wc -c
curl -s -H "Accept: text/markdown" https://esempio.com/about | wc -c
Se i due numeri coincidono, servi una sola rappresentazione a tutti e la pulizia la fa l'agente. Quel lavoro non è gratis. Tutto ciò che sta dentro <head>, ogni <div> contenitore, ogni attributo data- e ogni frammento di analytics entra nella finestra di contesto prima che qualcosa decida che era rumore. In una pagina il cui contenuto reale è un titolo e otto paragrafi, la maggior parte dei byte è impalcatura.
Il gemello in Markdown conserva i livelli dei titoli, i link, gli elenchi, le tabelle e i blocchi di codice. Perde navigazione, stile e script, di cui non sarebbe mai stato citato nulla.
Come funziona la negoziazione con Accept
Il client dichiara cosa sa leggere:
GET /about HTTP/1.1
Host: esempio.com
Accept: text/markdown
Il server sceglie una rappresentazione per lo stesso URL:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Un browser invia Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 e riceve HTML. Un client che chiede text/markdown riceve Markdown. L'URL canonico non cambia, quindi resta un solo indirizzo da linkare e una sola pagina da condividere.
Vary: Accept, ovvero come un browser finisce per scaricare un file di testo
Il Vary non è opzionale ed è proprio la parte che tutti saltano.
Ogni cache tra te e chi legge (la tua CDN, un reverse proxy, l'archivio del browser stesso) indicizza le risposte per URL. Se servi due corpi diversi sullo stesso URL senza dirlo, la prima risposta messa in cache vale per tutti. Un agente chiede /about con Accept: text/markdown, la tua CDN salva il Markdown, e la persona successiva che apre /about si trova una pagina di asterischi o una finestra di download.
Vary: Accept
Quella riga dice alla cache di includere l'header Accept nella chiave, così HTML e Markdown vengono conservati separatamente. Mandala su entrambe le risposte, non solo su quella in Markdown.
C'è un costo reale: gli header Accept variano parecchio da un client all'altro, quindi variare su di essi frammenta la cache. È un argomento onesto a favore dell'alternativa che segue.
Il gemello .md
L'opzione più semplice è un secondo URL. /about serve HTML, /about.md serve lo stesso contenuto in Markdown. Niente negoziazione, niente Vary, niente che possa andare storto in una cache. È un URL normale, quindi si può linkare, salvare nei preferiti, mettere nella sitemap e scaricare da qualsiasi client, compreso uno che non manda alcun header Accept utilizzabile.
Poi dichiara che esiste. Nell'HTML:
<link rel="alternate" type="text/markdown" href="https://esempio.com/about.md">
E negli header della risposta, per chi fa un HEAD e non legge mai il corpo:
Link: <https://esempio.com/about.md>; rel="alternate"; type="text/markdown"
Farle entrambe costa una riga ciascuna e copre i client che guardano in un posto solo.
Controlla il tuo
# 1. Lo stesso URL risponde in modo diverso?
curl -s -H "Accept: text/markdown" https://esempio.com/about | head -20
# 2. La risposta HTML ammette che il gemello esiste?
curl -sI https://esempio.com/about | grep -i "vary\|link"
# 3. L'URL .md funziona da solo?
curl -sI https://esempio.com/about.md | grep -i "content-type\|vary"
Il secondo comando è quello che scopre il guasto più comune. Un sito può avere un gemello .md perfetto che non troverà mai nessuno, perché nessun header e nessun tag lo indicano.
Cosa facciamo qui
Ogni pagina di questo sito ha un gemello in Markdown, e packages/web/src/lib/content-negotiation.js è tutto qui: una novantina di righe.
Una mappa fissa elenca i percorsi statici e i loro gemelli: / → /index.md, /about → /about.md, ogni pagina degli strumenti e così via. I percorsi che portano un identificativo o uno slug (i report su /r/<uuid>, le guide, gli articoli del blog come questo) vengono riconosciuti per pattern e ricevono <percorso>.md.
Il controllo sulla richiesta è volutamente grezzo: se l'header Accept contiene la stringa text/markdown, si invia Markdown. Nessun ordinamento per valori di q. Un browser quella stringa non la manda mai, quindi in pratica i due casi restano ben separati, ma un client che elenca text/markdown con priorità bassa riceve Markdown lo stesso.
Ogni risposta in Markdown esce con questi header:
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 dichiara cosa si può fare con il contenuto, non solo se lo si può scaricare. X-Markdown-Tokens è un indicatore di dimensione: oggi porta il numero di caratteri del corpo, non un conteggio reale di token.
Le risposte HTML di una pagina con gemello ricevono Vary: Accept e l'header Link, e nel <head> finisce il tag rel="alternate" preso dalla stessa mappa.
Quest'ultimo dettaglio ci ha morso. La mappa alimentava il template, ma le rotte riconosciute per pattern nella mappa non c'erano, così ogni report di analisi serviva il suo gemello in Markdown e lo annunciava nell'header Link, mentre il suo HTML non ne diceva niente. Il gemello esisteva ed era invisibile a chiunque leggesse il markup.
È una convenzione, non uno standard
Accept: text/markdown è normalissima negoziazione del contenuto HTTP, e quella è uno standard. Usarla per il Markdown è una convenzione nata dalla proposta llms.txt, e nessun crawler è obbligato a chiederlo. La posizione onesta è che il gemello .md è la parte che funziona di sicuro, perché è un URL come tutti gli altri e chiunque può scaricarlo, mentre la negoziazione costa un header e rende con i client che invece chiedono. Nessuna delle due ti farà male, e per sapere quale viene usata si leggono i log, non gli annunci.