---
title: Rendre mon blog lisible par les agents IA
description: J'ai ajouté des versions Markdown, un index pour agents et des contrôles
  de build. Pas pour une astuce GEO, mais pour rendre la source moins ambiguë.
canonical: https://djangodevreng.nl/fr/blog/rendre-mon-blog-lisible-par-les-agents-ia/
pubDate: '2026-08-18T00:00:00Z'
category: build-logs
---


Un agent pouvait déjà lire mon blog. Le contenu est en HTML, comme sur presque tous les blogs. Les routes Markdown directes et les alternates HTML existaient déjà elles aussi. Mais cela ne veut pas dire qu'un blog a une entrée complète pour les machines. Un agent qui veut utiliser un article comme source ne devrait pas d'abord retirer la navigation, la mise en page et les scripts, puis deviner où commence le contenu principal.

Dans cette modification, j'ai complété ces éléments existants comme un seul contrat : un index central pour toutes les langues, une fraîcheur cohérente dans le sitemap et des contrôles sur le site construit. Pas comme une astuce GEO. Je veux qu'un agent trouve la même source qu'un lecteur, avec une URL canonique claire et une date.

Cela correspond à la raison pour laquelle je construis ce site et [l'Arena](/fr/blog/pourquoi-ce-blog-et-arena/) : je ne publie pas seulement une conclusion, mais aussi le chemin qui y mène. Pour les benchmarks, cela signifie les commandes, les runs et les erreurs. Pour un blog, cela signifie que la source elle-même ne doit pas être inutilement ambiguë pour le logiciel qui doit la récupérer. La même logique sous-tend le [guide de décision IA locale](/fr/local-ai/) : rendre l'arbitrage explicite d'abord, la conclusion ensuite.

## Le HTML convient aux lecteurs, mais ce n'est pas le chemin le plus court pour un agent

Un navigateur a besoin de HTML. Un lecteur voit un en-tête, la navigation, des images, des blocs de code et le reste de la page comme prévu. Un agent cherche généralement autre chose : le texte, le titre, la date, l'URL qu'il peut citer et, idéalement, une manière de découvrir les articles avant de récupérer chaque page.

Le [guide de Vercel sur la documentation pour les agents IA](https://vercel.com/kb/guide/make-your-documentation-readable-by-ai-agents) distingue discovery, retrieval et tool access. Pour ce blog, retrieval était le premier gain concret : un agent doit pouvoir récupérer du Markdown sans devoir d'abord extraire la page HTML.

Cela ne rend pas un agent plus intelligent et ne garantit pas des réponses correctes. (Note: Le gain est plus modeste et concret : le même contenu, un format explicite, une URL canonique et des métadonnées de fraîcheur.)

Je n'utilise pas ici de content negotiation ni de détection par user-agent. Le site est statique, et la route explicite est simple : la page HTML est sur `/blog/slug/`, la variante lisible par machine sur `/blog/slug.md`. (Note: Cela simplifie le cache et le débogage par rapport à une URL qui renvoie une représentation différente selon les headers ou le user-agent.)

## Chaque article reçoit une variante Markdown depuis la même collection de contenu

La version Markdown n'est pas une deuxième copie à maintenir manuellement. Astro lit la même collection de contenu qui construit la page HTML et en génère un endpoint statique.

Les articles néerlandais utilisent `src/pages/blog/[...slug].md.ts`. Les articles anglais et français suivent la même structure sous `src/pages/[locale]/blog/[...slug].md.ts`. L'endpoint ajoute le frontmatter dont un agent a besoin pour situer le contenu :

```ts
const fmLines = [
  "---",
  `title: "${escape(post.data.title)}"`,
  `description: "${escape(post.data.description)}"`,
  `canonical: "${canonical}"`,
  `pubDate: "${post.data.pubDate.toISOString()}"`,
  post.data.updatedDate
    ? `updatedDate: "${post.data.updatedDate.toISOString()}"`
    : null,
  `category: "${post.data.category}"`,
  "---",
].filter(Boolean);

return new Response(fmLines.join("\n") + "\n" + post.body, {
  headers: {
    "Content-Type": "text/markdown; charset=utf-8",
    "Cache-Control": "public, max-age=3600",
  },
});
```

Le choix important est `canonical`. La page HTML reste la page publique et canonique. La route Markdown est un format alternatif qui renvoie vers cette page. Je ne veux pas créer deux versions concurrentes d'un article, seulement deux représentations utiles de la même source.

C'est aussi pourquoi les URL tiennent compte de la langue. Un article néerlandais reçoit `/blog/slug.md`, un article anglais reçoit `/en/blog/slug.md`. La structure correspond à l'URL de la page qu'un humain ouvre.

## La page HTML pointe explicitement vers le Markdown

Une URL `.md` n'aide vraiment que si un agent peut la trouver. Le composant SEO partagé ajoute donc un lien alternate dans le `<head>` de chaque article :

```astro
<link
  rel="alternate"
  type="text/markdown"
  href={new URL(mdUrl, Astro.site).toString()}
/>
```

`BlogPost.astro` déduit `mdUrl` de l'identifiant de l'article et de la langue. Un détail est important : le contenu néerlandais vit à la racine, alors que le contenu anglais et français a un préfixe de locale.

```ts
const mdUrl = id
  ? lang === DEFAULT_LOCALE
    ? `/blog/${id}.md`
    : `/${lang}/blog/${id.replace(`${lang}/`, "")}.md`
  : undefined;
```

Le guide Vercel couvre aussi `Accept: text/markdown`, un en-tête `Vary: Accept` et la détection automatique d'agents. Ces options sont utiles quand une seule URL doit servir dynamiquement du HTML ou du Markdown. Je ne voulais pas de variantes de cache, de règles user-agent ou de bascule cachée ici. Une route `.md` explicite est simple, directement ouvrable et testable.

## Le `/llms.txt` racine devait connaître toute la collection

La documentation AI SDK utilise elle-même [`llms.txt` comme point d'entrée Markdown](https://ai-sdk.dev/docs/introduction) pour des outils comme Cursor, Windsurf, Copilot et Claude. C'est l'usage utile pour moi : un petit index qui permet à un agent de voir quel contenu existe avant de récupérer des pages individuelles.

La première version de mon `/llms.txt` racine avait un problème : elle n'incluait que les articles néerlandais. C'est incomplet sur un site avec des versions anglaises et françaises. La branche ajoute donc un helper pour toute la collection :

```ts
export async function getAllPosts() {
  return (await getCollection("blog")).filter(isVisible);
}

export const postMarkdownUrl = (post: BlogEntry) =>
  postUrl(post).replace(/\/$/, ".md");
```

L'index racine peut alors inclure chaque article visible, quelle que soit sa langue, avec un lien direct vers sa version Markdown :

```ts
const posts = (await getAllPosts()).sort(...);

`- [${p.data.title}](${url(postMarkdownUrl(p))}): ${p.data.description}`
```

Les routes existantes `/en/llms.txt` et `/fr/llms.txt` restent des entrées compactes par langue. Root `/llms.txt` est la carte de toute la collection. Les drafts ne s'y retrouvent pas par erreur, car `getAllPosts()` applique la même règle de visibilité que le site de production.

Pour un agent qui cherche [mon build-log sur un assistant 24/7 sur Raspberry Pi](/fr/blog/openclaw-sur-raspberry-pi/), cela signifie moins de devinettes : d'abord l'index, ensuite la bonne source Markdown.

## Le sitemap avait le même angle mort multilingue

Les routes Markdown existaient déjà. L'alternate HTML aussi. Le travail de cette branche consistait surtout à compléter ce qui semblait correct dans une seule langue.

Le code du sitemap lisait auparavant seulement les fichiers directement sous `src/content/blog/`. Il ne pouvait pas définir `lastmod` de la même manière pour les articles dans `en/` et `fr/`. C'est un bug de collection familier : le code fonctionne pour le dossier par défaut jusqu'à ce que le contenu arrive dans un sous-dossier.

La correction parcourt maintenant les fichiers Markdown et MDX de façon récursive :

```ts
async function blogFiles(dir) {
  const files = [];
  for (const entry of await readdir(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) files.push(...(await blogFiles(full)));
    else if (!entry.name.startsWith("_") && /\.(md|mdx)$/.test(entry.name)) {
      files.push(full);
    }
  }
  return files;
}
```

Pour chaque article, la configuration préfère `updatedDate`, sinon `pubDate`, puis associe le chemin du fichier à sa route publique. `/blog/.../`, `/en/blog/.../` et `/fr/blog/.../` utilisent désormais la même règle de fraîcheur.

Cette date n'est pas un bouton de ranking. Elle indique toutefois à un crawler ou à un agent quand une source a été modifiée de manière substantielle. Si je mets à jour [les leçons pratiques de trois rondes de quantization](/fr/blog/quantization-llms-locaux/), chaque couche doit exposer le même signal.

## Je teste la sortie, pas seulement le code source

La partie la plus importante n'est pas une route, mais `scripts/check-content.mjs`.

Le contrôle s'exécute sur `dist/`, donc sur ce qu'Astro a réellement construit. Il trouve chaque page avec `BlogPosting` JSON-LD et vérifie ensuite que cette page a exactement un alternate Markdown, que la route est correcte, que la sortie Markdown existe, que l'URL apparaît dans root `/llms.txt` et que la date du sitemap correspond au frontmatter.

Le noyau ressemble à ceci :

```ts
if (markdownAlternates.length !== 1) {
  err(`${page}: ${markdownAlternates.length} markdown-alternates (expected 1)`);
}

if (!linkResolves(markdownPath)) {
  err(`${page}: markdown endpoint missing at ${markdownPath}`);
}

if (!rootLlms.includes(`https://djangodevreng.nl${markdownPath}`)) {
  err(`${page}: missing from root /llms.txt`);
}
```

C'est la limite qui m'importe. Une revue de code peut confirmer qu'une route semble logique. Seule la sortie construite montre si le `<link>` est réellement dans le HTML, si le fichier `.md` statique existe et si le sitemap a obtenu la bonne date.

Google conseille de rendre les pages importantes trouvables par des liens internes contextuels et de garder des textes de lien descriptifs. Ses [recommandations sur les liens crawlables](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) ne sont pas spécifiques aux agents, mais la discipline est la même : un lien doit avoir une URL réelle et indiquer clairement sa destination. C'est pourquoi cet article ne reste pas isolé du reste du blog et renvoie vers les build-logs sous-jacents lorsque cela aide le lecteur.

## Ce que cette implémentation ne fait pas

Ce n'est pas une spécification complète d'agent-readiness.

Je n'ai pas construit de `sitemap.md`, de 404 Markdown, de serveur MCP ou de réécriture automatique selon le user-agent d'un agent. Je n'ai pas non plus mesuré combien d'agents utilisent les nouvelles routes, ni si cela change la visibilité dans les réponses IA. Ce serait une prochaine mesure, pas une affirmation que je peux déjà faire.

L'étape qui existe aujourd'hui est plus petite et plus utile : chaque article publié a une représentation explicite lisible par machine, l'index connaît toutes les langues, la fraîcheur suit le contenu et le build échoue lorsque ces contrats se cassent.

Si je reconstruisais cela, je commencerais exactement dans cet ordre. Une source pour HTML et Markdown. Ensuite les canonicals et les dates. Puis un index. Ensuite seulement des couches supplémentaires comme content negotiation ou MCP, lorsqu'un cas d'usage d'agent concret les demande.
