Markdown pour les agents : servir Accept: text/markdown
Un agent demande votre page et reçoit du HTML. Puis il en jette la plus grande partie : le menu, le pied de page, le bandeau cookies, les scripts, les attributs de classe, les icônes SVG. Il garde quelques paragraphes de texte.
La version qu'il voulait, vous l'avez déjà. Presque personne ne la sert.
Ce que coûte le balisage
Comparez ce que vous envoyez avec ce qui survit :
curl -s https://exemple.com/about | wc -c
curl -s -H "Accept: text/markdown" https://exemple.com/about | wc -c
Si les deux nombres sont identiques, vous servez une seule représentation à tout le monde et c'est l'agent qui fait le ménage. Ce travail n'est pas gratuit. Tout ce qui se trouve dans le <head>, chaque <div> conteneur, chaque attribut data- et chaque bout de code analytique entre dans la fenêtre de contexte avant que quoi que ce soit ne décide que c'était du bruit. Sur une page dont le contenu réel est un titre et huit paragraphes, la majorité des octets ne sont que de l'échafaudage.
Le jumeau Markdown conserve les niveaux de titre, les liens, les listes, les tableaux et les blocs de code. Il perd la navigation, la mise en forme et les scripts, dont rien n'allait jamais être cité.
Comment fonctionne la négociation avec Accept
Le client indique ce qu'il sait lire :
GET /about HTTP/1.1
Host: exemple.com
Accept: text/markdown
Le serveur choisit une représentation pour la même URL :
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Un navigateur envoie Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 et reçoit du HTML. Un client qui demande text/markdown reçoit du Markdown. L'URL canonique ne change pas : il reste une seule adresse à lier et une seule page à partager.
Vary: Accept, ou comment un navigateur finit par télécharger un fichier texte
Le Vary n'est pas optionnel, et c'est précisément la partie que tout le monde saute.
Chaque cache entre vous et le lecteur (votre CDN, un proxy inverse, le stockage du navigateur lui-même) indexe les réponses par URL. Si vous servez deux corps différents sur une même URL sans le signaler, la première réponse mise en cache vaudra pour tout le monde. Un agent demande /about avec Accept: text/markdown, votre CDN garde le Markdown, et la personne suivante qui ouvre /about tombe sur une page d'astérisques ou sur une invite de téléchargement.
Vary: Accept
Cette ligne dit au cache d'inclure l'en-tête Accept dans sa clé, de sorte que le HTML et le Markdown soient stockés séparément. Envoyez-la sur les deux réponses, pas seulement sur celle en Markdown.
Il y a un coût réel : les en-têtes Accept varient beaucoup d'un client à l'autre, donc varier dessus fragmente votre cache. C'est un argument honnête en faveur de l'alternative qui suit.
Le jumeau .md
L'option la plus simple est une deuxième URL. /about sert le HTML, /about.md sert le même contenu en Markdown. Pas de négociation, pas de Vary, rien qui puisse mal tourner dans un cache. C'est une URL ordinaire, donc on peut la lier, la mettre en favori, la placer dans un sitemap et la récupérer avec n'importe quel client, y compris un client qui n'envoie aucun en-tête Accept exploitable.
Ensuite, annoncez son existence. Dans le HTML :
<link rel="alternate" type="text/markdown" href="https://exemple.com/about.md">
Et dans les en-têtes de réponse, pour ce qui fait un HEAD sans jamais analyser le corps :
Link: <https://exemple.com/about.md>; rel="alternate"; type="text/markdown"
Faire les deux coûte une ligne chacune et couvre les clients qui ne regardent qu'à un seul endroit.
Vérifier le vôtre
# 1. La même URL répond-elle différemment ?
curl -s -H "Accept: text/markdown" https://exemple.com/about | head -20
# 2. Le HTML admet-il que le jumeau existe ?
curl -sI https://exemple.com/about | grep -i "vary\|link"
# 3. L'URL .md fonctionne-t-elle toute seule ?
curl -sI https://exemple.com/about.md | grep -i "content-type\|vary"
La deuxième commande est celle qui attrape la panne habituelle. Un site peut avoir un jumeau .md parfait que personne ne trouvera jamais, parce qu'aucun en-tête et aucune balise ne pointent vers lui.
Ce que nous faisons ici
Chaque page de ce site a un jumeau Markdown, et packages/web/src/lib/content-negotiation.js contient tout le mécanisme : environ quatre-vingt-dix lignes.
Une table fixe liste les chemins statiques et leurs jumeaux : / → /index.md, /about → /about.md, chaque page d'outil, et ainsi de suite. Les chemins qui portent un identifiant ou un slug (les rapports en /r/<uuid>, les guides, les billets de blog comme celui-ci) sont reconnus par motif et reçoivent <chemin>.md.
Le test sur la requête est volontairement grossier : si l'en-tête Accept contient la chaîne text/markdown, on envoie du Markdown. Aucun classement par valeurs de q. Un navigateur n'envoie jamais cette chaîne, donc en pratique la séparation est nette, mais un client qui liste text/markdown en basse priorité recevra quand même du Markdown.
Chaque réponse Markdown part avec ceci :
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 déclare ce qu'on a le droit de faire du contenu, et pas seulement s'il peut être récupéré. X-Markdown-Tokens est un repère de taille : il transporte aujourd'hui le nombre de caractères du corps, pas un vrai comptage de tokens.
Les réponses HTML d'une page qui a un jumeau reçoivent Vary: Accept et l'en-tête Link, et le <head> reçoit la balise rel="alternate" issue de la même table.
Ce dernier détail nous a mordus. La table alimentait le gabarit, mais les routes reconnues par motif n'étaient pas dans la table : chaque rapport d'analyse servait donc son jumeau Markdown et l'annonçait dans l'en-tête Link, pendant que son HTML n'en disait rien. Le jumeau existait et restait invisible pour tout ce qui lisait le balisage.
C'est une convention, pas un standard
Accept: text/markdown est de la négociation de contenu HTTP tout à fait ordinaire, et cela, c'est un standard. L'utiliser pour du Markdown est une convention née de la proposition llms.txt, et aucun robot n'est tenu de la demander. La position honnête : le jumeau .md est la partie qui marche à coup sûr, parce que c'est une URL comme une autre et que n'importe quoi peut la récupérer, tandis que la négociation coûte un en-tête et paie avec les clients qui, eux, demandent. Ni l'une ni l'autre ne vous fera de mal, et pour savoir laquelle sert, on lit ses logs plutôt que les annonces.