---
title: Mijn blog leesbaar maken voor AI-agents
description: Ik breidde de Markdown-routes van mijn blog uit met een index voor alle
  talen, consistente sitemapdatums en controles op de gebouwde site.
canonical: https://djangodevreng.nl/blog/mijn-blog-leesbaar-voor-ai-agents/
pubDate: '2026-08-18T00:00:00Z'
category: build-logs
updatedDate: '2026-09-13T00:00:00Z'
---


Mijn blog kon al door een agent gelezen worden. De inhoud staat gewoon in HTML, net als bijna iedere andere blog. De directe Markdown-routes en HTML-alternates bestonden ook al. Maar dat is iets anders dan een blog een volledige, machine-leesbare ingang geven. Een agent die een post als bron wil gebruiken, moet niet eerst navigatie, layout en scripts wegfilteren en gokken waar de hoofdtekst begint.

In deze wijziging heb ik die bestaande onderdelen als één contract afgemaakt: een centrale index voor alle talen, consistente freshness in de sitemap en checks op de gebouwde site. Ik wil dat een agent dezelfde bron kan vinden als een lezer, met een duidelijke canonieke URL en een datum erbij.

Dat past bij waarom ik deze site en [de Arena](/blog/waarom-deze-blog-en-arena/) bouw: de werkwijze bij de resultaten publiceren. Voor benchmarks betekent dat commands, runs en fouten. Voor een blog betekent het dat de bron zelf niet onnodig vaag is voor de software die hem moet ophalen. Diezelfde gedachte zit achter de [Local AI-beslisgids](/local-ai/): eerst de afweging expliciet maken, dan pas de conclusie.

## HTML is prima voor mensen, maar niet de kortste route voor een agent

Een browser heeft HTML nodig. Een lezer ziet een header, navigatie, afbeeldingen, codeblokken en de rest van de pagina precies zoals bedoeld. Een agent heeft daar meestal weinig aan. Die zoekt de tekst, de titel, de datum, de URL waarnaar hij kan verwijzen en liefst een manier om vooraf te ontdekken welke posts er zijn.

De [Vercel-gids over documentatie voor AI-agents](https://vercel.com/kb/guide/make-your-documentation-readable-by-ai-agents) splitst dat op in discovery, retrieval en tool access. Voor deze blog was retrieval de eerste praktische winst: een agent moet Markdown kunnen ophalen zonder de HTML-pagina zelf te hoeven uitpakken.

De bron wordt er niet slimmer van en een agent krijgt er geen garantie op correcte antwoorden door. (Noot: De winst is kleiner en concreter: dezelfde inhoud, een expliciet formaat, een canonieke URL en freshness-metadata.)

Ik gebruik hier geen content negotiation of user-agent-detectie. De site is statisch en de expliciete route is eenvoudig: de HTML-pagina staat op `/blog/slug/`, de machine-leesbare variant op `/blog/slug.md`. (Noot: Dit maakt caching en debugging eenvoudiger dan één URL die, afhankelijk van headers of user-agent, een andere representatie teruggeeft.)

## Iedere post krijgt een Markdown-variant uit dezelfde content collection

De Markdown-versie is geen tweede kopie die ik met de hand moet bijwerken. Astro leest dezelfde content collection die ook de HTML-pagina bouwt en genereert daar een statische endpoint van.

Voor Nederlandse posts gebeurt dat in `src/pages/blog/[...slug].md.ts`. De Engelse en Franse varianten hebben dezelfde opbouw onder `src/pages/[locale]/blog/[...slug].md.ts`. De endpoint voegt frontmatter toe die een agent nodig heeft om de inhoud te plaatsen:

```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",
  },
});
```

De belangrijke keuze zit in `canonical`. De HTML-pagina blijft de publieke, canonieke pagina. De Markdown-route is een alternatief formaat dat terugwijst naar die pagina. Ik wil geen twee concurrerende versies van een post maken, alleen twee bruikbare representaties van dezelfde bron.

Dat is ook waarom de URL's taalbewust zijn. Een Nederlandse post krijgt `/blog/slug.md`; een Engelse post krijgt `/en/blog/slug.md`. De URL-structuur blijft gelijk aan die van de pagina die een mens opent.

## De HTML-pagina wijst expliciet naar Markdown

Een `.md`-URL helpt pas echt als een agent hem kan vinden. Daarom zet de gedeelde SEO-component een alternate link in de `<head>` van iedere blogpost:

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

`BlogPost.astro` leidt `mdUrl` af van de post-ID en de taal. Daar zit een klein detail dat makkelijk fout gaat: Nederlandse content staat op de root, Engelse en Franse content onder een locale-prefix.

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

De Vercel-gids beschrijft ook `Accept: text/markdown`, een `Vary: Accept`-header en automatische agent-detectie. Dat zijn nuttige opties wanneer je dezelfde URL dynamisch als HTML of Markdown wilt serveren. Ik wilde hier geen cache-varianten, user-agentregels of verborgen omschakelingen. Een expliciete `.md`-route kan ik direct openen en testen.

## Root `/llms.txt` moest de volledige collectie kennen

De AI SDK-documentatie gebruikt zelf [`llms.txt` als Markdown-ingang](https://ai-sdk.dev/docs/introduction) voor onder meer Cursor, Windsurf, Copilot en Claude. Dat is voor mij de nuttige toepassing: een kleine index waarmee een agent ziet welke inhoud er bestaat voordat hij losse pagina's gaat ophalen.

De eerste versie van mijn root `/llms.txt` had een probleem: hij nam alleen Nederlandse posts op. Dat is onvolledig op een site met Engelse en Franse versies. De branch voegt daarom een collectiebrede helper toe:

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

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

Daarmee kan de root-index alle zichtbare posts opnemen, ongeacht taal, met een directe link naar hun Markdown-versie:

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

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

De bestaande `/en/llms.txt`- en `/fr/llms.txt`-routes blijven compacte taalingangen. Root `/llms.txt` is de collectiebrede kaart. Drafts komen niet per ongeluk mee, want `getAllPosts()` gebruikt dezelfde zichtbaarheidregel als de rest van de productie-site.

Voor een agent die bijvoorbeeld [mijn build-log over een 24/7 assistent op een Raspberry Pi](/blog/openclaw-op-raspberry-pi/) zoekt, betekent dat minder raden: eerst de index, daarna de juiste Markdown-bron.

## De sitemap had hetzelfde meertalige gat

De Markdown-routes waren er al. De HTML-alternate ook. Het werk in deze branch zat vooral in compleet maken wat op één taal nog goed leek.

De sitemaplogica las eerder alleen bestanden direct onder `src/content/blog/`. Daardoor kon zij niet op dezelfde manier `lastmod` zetten voor posts in `en/` en `fr/`. Dat is een klassiek collectiefoutje: de code werkt voor de standaardmap, totdat content een submap krijgt.

De oplossing loopt nu recursief door Markdown- en MDX-bestanden:

```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;
}
```

Per post kiest de configuratie eerst `updatedDate` en anders `pubDate`, en vertaalt daarna het bestandspad naar de publieke route. Zo krijgt `/blog/.../`, `/en/blog/.../` en `/fr/blog/.../` dezelfde freshness-regel.

Die datum is geen rankingknop. Hij vertelt een crawler of agent wel wanneer een bron voor het laatst inhoudelijk is gewijzigd. Als ik bijvoorbeeld de [praktische lessen uit drie quantization-rondes](/blog/quantization-lokale-llms/) aanvul, moet die update op elke laag hetzelfde signaal opleveren.

## Ik test de output, niet alleen de broncode

In `scripts/check-content.mjs` controleer ik of de gebouwde site alle varianten correct aanbiedt.

De check loopt over `dist/`, dus over wat Astro daadwerkelijk heeft gebouwd. Hij zoekt iedere pagina met `BlogPosting` JSON-LD en controleert daarna of die pagina precies één Markdown-alternate heeft, of de route klopt, of de Markdown-output bestaat, of de URL in root `/llms.txt` staat en of de sitemapdatum overeenkomt met de frontmatter.

De kern ziet er zo uit:

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

if (!linkResolves(markdownPath)) {
  err(`${page}: markdown-endpoint ontbreekt op ${markdownPath}`);
}

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

Met deze controle zie ik of de `<link>` in de gebouwde HTML staat, de statische `.md`-file bestaat en de sitemap de juiste datum heeft. Ik controleer dus ook het resultaat van de build.

Google adviseert om belangrijke pagina's via contextuele interne links vindbaar te maken en linkteksten beschrijvend te houden. De [richtlijnen voor crawlbare links](https://developers.google.com/search/docs/crawling-indexing/links-crawlable) zijn niet speciaal voor agents, maar de discipline is dezelfde: een link moet een echte URL hebben en duidelijk maken waar hij heen gaat. Daarom staat deze post niet los van de rest van de blog en verwijs ik naar de onderliggende build-logs waar dat inhoudelijk helpt.

## Wat deze implementatie niet doet

Dit is geen volledige agent-readiness-specificatie.

Ik bouwde geen `sitemap.md`, geen Markdown-404's, geen MCP-server en geen automatische omzetting op basis van een agent user-agent. Ik heb ook niet gemeten hoeveel agents de nieuwe routes al gebruiken of of dit invloed heeft op zichtbaarheid in AI-antwoorden. Dat zou een volgende meting zijn, geen claim die ik nu al kan doen.

Het resultaat: iedere gepubliceerde post heeft een expliciete machine-leesbare representatie, de index kent alle talen, freshness loopt mee en de build faalt wanneer die contracten breken.

Als ik dit opnieuw zou bouwen, zou ik precies in die volgorde beginnen. Eerst dezelfde contentbron voor HTML en Markdown. Dan canonical en datums. Daarna een index. Pas daarna extra lagen zoals content negotiation of MCP, als er een concrete agent-use-case is die dat nodig maakt.
