TechniqueCarnet — Tharsan.me

Rendre un site lisible par les agents IA : llms.txt, Markdown et API publique

Comment j'ai rendu mon site lisible par les agents IA : négociation Accept text/markdown, llms.txt, agents.md, API REST documentée en OpenAPI et 404 utiles.

Vishnukumar Tharsan· 7 min de lecture
  • agents-ia
  • llms-txt
  • next-js
  • seo
Croquis à l'encre : un petit robot lit un long parchemin qui se transforme en liste ordonnée à puces rouges.

Un agent qui visite votre site reçoit exactement ce que reçoit un navigateur : du HTML noyé dans de la navigation, des scripts et du décor. Il doit deviner où est le contenu. On peut faire beaucoup mieux, et pour un coût modeste : servir du Markdown à qui en demande, publier un index lisible par machine, et exposer les données structurées derrière une API documentée. J'ai implémenté les trois sur ce site.

Voici ce que j'ai livré, pourquoi, et ce que je ferais dans un autre projet.

Le problème : le HTML est un mauvais format d'échange

Prenez n'importe quelle page de portfolio. Le contenu utile — un titre, trois paragraphes, une liste de technologies — représente une fraction du document. Le reste est de la structure de présentation : en-tête, menu, pied de page, balises de style, données d'hydratation.

Un agent qui lit cette page doit séparer le signal du bruit à chaque requête. C'est du contexte gaspillé et une source d'erreurs. Or le contenu, lui, existe déjà sous une forme propre dans votre code : c'est la donnée avant qu'on la mette en page.

L'idée est donc simple : rendre cette donnée accessible directement, sans obliger personne à défaire le HTML.

1. La négociation de contenu : le même chemin, deux formats

HTTP sait faire ça depuis toujours. Le client envoie un en-tête Accept qui déclare les formats qu'il comprend ; le serveur choisit et répond en indiquant son choix.

Sur ce site, chaque page a une représentation Markdown à la même URL :

curl -H "Accept: text/markdown" https://www.tharsan.me/about

Et pour les clients qui ne maîtrisent pas leurs en-têtes, un suffixe explicite fait la même chose :

curl https://www.tharsan.me/about.md

Trois points comptent dans l'implémentation :

  • Vary: Accept est obligatoire. Sans lui, un cache intermédiaire sert la version Markdown à un navigateur, ou l'inverse. C'est le genre de bug qui n'apparaît qu'en production, derrière un CDN.
  • Content-Location désigne l'URL canonique de la variante (/about.md), et un en-tête Link avec rel="canonical" renvoie vers la version HTML. Chaque représentation sait donc nommer l'autre.
  • Un Accept incompatible mérite un vrai 406. Si un client demande exclusivement du JSON sur une page HTML, répondre du HTML est un mensonge poli. Répondre 406 avec la liste de ce qu'on produit est une information exploitable.

Le point d'implémentation le plus délicat n'est pas la négociation elle-même, mais ce qu'il ne faut surtout pas négocier. Dans l'App Router, les navigations internes émettent des requêtes React Server Components avec leurs propres en-têtes. Si votre proxy les traite comme des requêtes de document et les réécrit vers du Markdown, la navigation côté client cesse de fonctionner — le premier chargement va bien, les clics ne font plus rien. Je détecte ces requêtes en tout premier et je les laisse passer intactes, avant toute autre décision. Même chose pour les assets, les images Open Graph générées et les routes d'API.

Quant au contenu du Markdown : il ne contient que le contenu. Pas de navigation, pas de pied de page, pas de décor. Un titre, un résumé en citation, des sections, et une ligne finale qui donne les URL canoniques. C'est ce qu'un agent aurait extrait du HTML, sans avoir à le faire.

2. llms.txt : l'index d'entrée

llms.txt est à un agent ce que sitemap.xml est à un robot d'indexation : un point d'entrée unique qui dit ce qu'on trouve où. Le format est volontairement minimal — un titre H1, un résumé en citation, puis des sections de listes de liens annotés.

Le mien publie, dans l'ordre : quand utiliser ce site (les cas d'usage réels, en français et en anglais), comment l'appeler (Markdown, API, gestion des erreurs), la liste des pages avec leur URL .md, les articles, les ressources développeur, les endpoints de l'API, et une section optionnelle qu'un agent à court de contexte peut ignorer.

Deux variantes complètent le dispositif :

  • llms-full.txt concatène tout le contenu du site en un seul document Markdown. Un agent qui veut tout ingérer fait une requête au lieu de dix.
  • agents.md, servi à la racine, reprend la convention des dépôts de code : instructions d'usage, quotas, politique de versionnement. C'est le fichier qu'un agent bien élevé cherche en premier.

Un détail qui compte : j'ai ajouté des redirections permanentes depuis les URL que les agents essaient/docs, /api-docs, /developer, /llms.md. Elles mènent toutes au bon endroit. Deviner une URL plausible est un comportement d'agent parfaitement normal ; autant qu'il aboutisse.

3. L'API publique et sa spécification OpenAPI

Le Markdown est fait pour être lu. Pour être traité, il faut du JSON structuré.

L'API de ce site est en lecture seule, sans clé, avec CORS ouvert : profil, projets, expériences, stack technique. Les réponses suivent une enveloppe unique — { data, meta } en cas de succès, { error: { status, code, message, hint, docs } } en cas d'échec, y compris pour les 404, les méthodes non supportées et les dépassements de quota.

Ce champ hint est celui que je recommande le plus. Une erreur qui explique quoi faire ensuite évite à un agent d'essayer six variantes de la même requête.

La spécification OpenAPI 3.1 est publiée à /openapi.json, avec des operationId uniques et des schémas de réponse typés — c'est exactement ce que consomme un mécanisme d'appel de fonctions. Elle n'est pas maintenue à la main : elle est générée à partir des mêmes définitions que les routes, ce qui rend la dérive impossible par construction.

Le rate limiting suit le brouillon IETF RateLimit : chaque réponse annonce la politique et le quota restant, et un dépassement renvoie un 429 avec Retry-After. Là encore, l'objectif est qu'un client automatisé puisse s'adapter au lieu de marteler.

4. Les 404 aussi doivent être utiles

Une erreur est un moment où le client est perdu. C'est précisément là qu'il faut lui donner une carte.

Une URL inexistante renvoie sur ce site un vrai 404 — le bon code, jamais une page de contenu déguisée — mais dans le format que le client comprend : HTML pour un navigateur, JSON sous /api, et Markdown pour un client qui préfère text/markdown, avec la liste complète des pages disponibles et les liens vers llms.txt, agents.md et le sitemap.

Un agent qui se trompe d'URL repart donc avec la réponse à sa question suivante. Ces réponses portent Cache-Control: no-store et X-Robots-Tag: noindex : utiles, mais jamais indexées.

Est-ce que ça sert vraiment ?

Question légitime, réponse honnête : je ne peux pas vous montrer une courbe de trafic qui décolle. Ces conventions sont récentes, leur adoption est inégale, et llms.txt n'est pas un standard ratifié.

Ce que je peux dire, c'est ceci. Le coût est faible parce que le contenu existait déjà sous forme structurée — mes pages Markdown sont générées depuis les mêmes fichiers de configuration que les pages HTML, donc les deux ne peuvent pas diverger. Un test vérifie d'ailleurs que le registre des pages, le sitemap et les en-têtes restent synchronisés : ajouter une page sans sa représentation Markdown fait échouer la suite.

Et le bénéfice ne dépend pas que des agents. Un Accept correctement géré, des 404 honnêtes, un sitemap fiable et des données structurées : c'est de l'hygiène HTTP. Elle profite aux moteurs de recherche classiques exactement autant.

Mon pari est simple : le trafic vers les sites passera de plus en plus par des intermédiaires logiciels. Autant qu'ils lisent correctement.

Par où commencer sur votre projet

Dans l'ordre de rapport sur l'effort :

  1. Un llms.txt correct. Quelques dizaines de lignes, aucune infrastructure. Décrivez ce que fait le site, à qui il s'adresse, et listez vos pages importantes.
  2. Des 404 honnêtes qui renvoient le bon code et proposent une suite.
  3. La négociation Markdown, si votre contenu est déjà structuré. Si vos pages sont écrites à la main en JSX, commencez par extraire les données — c'est de toute façon un meilleur code.
  4. L'API et l'OpenAPI, en dernier, et seulement si vos données valent la peine d'être requêtées.

Le détail complet de ce qui tourne ici est sur la page développeurs, et l'ensemble des projets est ici. Si vous travaillez sur Next.js, mon retour d'expérience sur la migration depuis Remix et l'histoire de l'en-tête Vary qui disparaît sont dans la continuité de cet article. Une question, un projet ? Écrivez-moi.

À lire ensuite

02 / SUITE
← Tous les articles