Serving Your Blog as Markdown So AI Agents Can Actually Read It
Last updated: 31.05.2026
Serving Your Blog as Markdown So AI Agents Can Actually Read It
Audio Narration

The post explains how to serve clean Markdown versions of blog content so AI agents can consume it without HTML noise like navigation, scripts, and cookie banners. Instead of parsing full HTML pages, agents can request Markdown either by appending .md to post URLs or by sending an Accept: text/markdown header. In Next.js, two rewrite rules in next.config.ts route both patterns to an internal /md/posts/[slug] handler. That route fetches posts from Sanity with sanityFetch, converts them to Markdown, and returns a text/markdown response with short caching headers for freshness. The buildPostMarkdown function constructs a complete Markdown document: H1 title, canonical URL, optional hero image, auto-generated summary, and the main body converted from Sanity Portable Text via @portabletext/markdown. Code blocks stored in Sanity as custom _type: "code" objects are rendered as fenced code blocks with language tags preserved. A /posts.md index provides a machine-readable sitemap listing all posts with metadata and links, enabling agents to discover content before fetching individual Markdown posts. The .md suffix is ideal for sharing, while the Accept header method suits tools and AI clients that control HTTP headers.
Why AI Agents Struggle with HTML
HTML is for browsers. When an AI agent fetches a blog post it gets navigation bars, scripts, cookie banners, and footer links tangled around the actual content. Markdown gives agents clean, structured text they can parse and reason over without stripping noise. Two patterns make this work: append .md to any post URL, or send an Accept: text/markdown header on a normal request.
Wiring It Up in next.config.ts
Next.js rewrites handle both patterns in beforeFiles. The first rule maps the .md URL suffix to the internal route. The second matches the same destination when the Accept header contains text/markdown:
1rewrites: async () => ({
2 beforeFiles: [
3 // Explicit .md URL
4 { source: '/posts/:slug', destination: '/md/posts/:slug' },
5 // Content negotiation via Accept header
6 {
7 source: '/posts/:slug',
8 destination: '/md/posts/:slug',
9 has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
10 },
11 ],
12})1rewrites: async () => ({
2 beforeFiles: [
3 // Explicit .md URL
4 { source: '/posts/:slug', destination: '/md/posts/:slug' },
5 // Content negotiation via Accept header
6 {
7 source: '/posts/:slug',
8 destination: '/md/posts/:slug',
9 has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }],
10 },
11 ],
12})The Route Handler
The internal /md/posts/[slug] route fetches the post from Sanity via sanityFetch, converts it to Markdown, and returns it with the correct Content-Type and a 60-second cache:
1export async function GET(request, { params }) {
2 const data = await sanityFetch({ query: postQuery, params: await params })
3 if (!data) return new Response('Not found', { status: 404 })
4
5 const markdown = buildPostMarkdown(
6 data,
7 process.env.NEXT_PUBLIC_SITE_URL
8 )
9 return new Response(markdown, {
10 headers: {
11 'Content-Type': 'text/markdown; charset=utf-8',
12 'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
13 },
14 })
15}1export async function GET(request, { params }) {
2 const data = await sanityFetch({ query: postQuery, params: await params })
3 if (!data) return new Response('Not found', { status: 404 })
4
5 const markdown = buildPostMarkdown(
6 data,
7 process.env.NEXT_PUBLIC_SITE_URL
8 )
9 return new Response(markdown, {
10 headers: {
11 'Content-Type': 'text/markdown; charset=utf-8',
12 'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
13 },
14 })
15}Building the Markdown Output
buildPostMarkdown assembles the full document: title as H1, canonical URL, hero image if present, auto-generated summary, body converted from Portable Text via @portabletext/markdown, and an optional image gallery.
1export function buildPostMarkdown(post, baseUrl) {
2 const parts = []
3 parts.push(`# ${post.title}`)
4 parts.push(`**URL:** ${baseUrl}/${post.path}`)
5 if (post.mainImage) {
6 parts.push(``)
7 }
8 if (post.autoSummary) parts.push(`**Summary:** ${post.autoSummary}`)
9 if (post.body) parts.push(convertToMarkdown(post.body, baseUrl))
10 return parts.join('\n')
11}1export function buildPostMarkdown(post, baseUrl) {
2 const parts = []
3 parts.push(`# ${post.title}`)
4 parts.push(`**URL:** ${baseUrl}/${post.path}`)
5 if (post.mainImage) {
6 parts.push(``)
7 }
8 if (post.autoSummary) parts.push(`**Summary:** ${post.autoSummary}`)
9 if (post.body) parts.push(convertToMarkdown(post.body, baseUrl))
10 return parts.join('\n')
11}Code Blocks in Portable Text
Sanity stores code snippets as custom blocks with _type: "code". Here is a real example from the TypeScript Pro Essentials post on this blog, as it lives in Portable Text:
1{
2 "_type": "code",
3 "language": "ts",
4 "code": "type StrictOmit<T, K extends keyof T> = Omit<T, K>;"
5}1{
2 "_type": "code",
3 "language": "ts",
4 "code": "type StrictOmit<T, K extends keyof T> = Omit<T, K>;"
5}@portabletext/markdown handles this block type and renders it as a fenced code block with the language tag preserved — exactly what an AI agent or syntax highlighter expects:
1```ts
2type StrictOmit<T, K extends keyof T> = Omit<T, K>;
3```1```ts
2type StrictOmit<T, K extends keyof T> = Omit<T, K>;
3```The Posts Index
/posts.md lists all published posts with titles, dates, authors, summaries, and links. It works as a machine-readable sitemap — an agent can fetch it first to discover what is available, then follow links to individual posts as Markdown.
Practical Usage
1# .md URL suffix
2curl https://andreskristensen.blog/posts/reading-time-sanity-blog.md
3
4# Accept header (content negotiation)
5curl -H 'Accept: text/markdown' https://andreskristensen.blog/posts/reading-time-sanity-blog
6
7# Posts index — discover all posts
8curl https://andreskristensen.blog/posts.md1# .md URL suffix
2curl https://andreskristensen.blog/posts/reading-time-sanity-blog.md
3
4# Accept header (content negotiation)
5curl -H 'Accept: text/markdown' https://andreskristensen.blog/posts/reading-time-sanity-blog
6
7# Posts index — discover all posts
8curl https://andreskristensen.blog/posts.mdThe .md URL approach is simpler for linking and sharing. The Accept header approach works with tools that fetch standard URLs but can override headers — useful for MCP servers and AI clients.
Inspired by Sanity's field guide on serving content to agents.

6 Claude Power Prompts That Change How You Build
Most people use Claude as a smarter search engine, but it can be a disciplined collaborator if you structure your prompts. The article outlines six patterns to unlock this mode. First, launch subagents to split work: one checks brand tone, another reviews code, a third drafts copy, each operating independently to avoid context bleed. Second, write a spec before coding, clarifying purpose, scope, and key decisions while changes are still cheap. Third, have Claude interview you first to surface hidden assumptions, co-create a spec, and get approval before implementation. Fourth, define how success will be verified upfront, then actually run that check afterward, especially for sensitive systems like payments or auth. Fifth, convert productive sessions into a reusable SKILL.md, including gotchas so lessons persist beyond the chat. Sixth, once a workflow is proven through manual runs, automate it via scripts, cron jobs, or CI. All six follow a meta-pattern: plan → build → verify → learn → automate, shifting you from just prompting Claude to designing how it works with you.
Read post
How I Used My Own Blog to Research My Presentation About It
The author needed a fast way to collect 14 blog posts for a presentation without manually copy-pasting from the browser. Fortunately, the blog already exposed each post as clean markdown at /md/posts/[slug], originally built for AI tools. This endpoint strips away HTML, navigation, and cookie banners, returning only the core content, which the author then fed into Claude for rapid reading and summarisation. Before working with individual posts, the author used /llms.txt, a structured, machine-readable index of the site similar to robots.txt but designed for AI discovery. Claude used this index to understand the site’s structure and locate relevant posts. For the presentation itself, the author used Slidev, a markdown-based slide tool, allowing Claude to generate and refine slides directly in markdown. The experience highlighted a “meta” benefit: infrastructure built for AI and other developers ended up being most useful to the author, demonstrating that investing in AI-friendly content formats can pay off immediately for the site owner.
Read post