What it is
Content negotiation is a 30-year-old HTTP feature. Adapted for agents: send Accept: text/markdown, get back markdown. The markdown version is easier for LLMs to consume and typically uses 60–80% fewer tokens than the HTML. See Cloudflare's Markdown for Agents.
Why it matters
- Agents have context windows. Markdown fits more content per token.
- HTML-to-markdown conversion on the agent side is lossy and slow.
- Serving markdown natively lets the site curate what the agent sees (skip navigation, cookie banners, footer).
Remediation Prompt
I want to improve my site's agent readiness. Please implement the following fix for Markdown Content Negotiation across our codebase. Instructions: Please fix the Markdown Content Negotiation issue on my site so it is agent-ready.
How we test it
| Step | Method | URL | Accept | Notes |
|---|---|---|---|---|
| A | GET | / |
text/markdown |
If response content-type starts with text/markdown → pass |
| B | GET | /index.md |
*/* |
Fallback convention popularised by Cloudflare docs |
| C | GET | / |
text/markdown |
Also record if server returns HTML with Vary: Accept — a near-miss |
A pass via either A or B. We prefer A because it's the standards-aligned approach.
Pass Warn Fail Matrix
| Condition | Status | Score |
|---|---|---|
A returns content-type: text/markdown* and a non-empty body that parses as markdown |
pass | 1.0 |
B returns text/markdown* and parses as markdown |
warn | 0.75 |
Vary: Accept present but body is still HTML |
warn | 0.3 |
| Neither route returns markdown | fail | 0.0 |
We additionally try to detect token savings: compute tokens(html) vs tokens(markdown) using a cheap tokeniser approximation (chars/4). If markdown is ≥40% smaller, tag the evidence with goodReduction: true.
Sub Tests
| id | Weight | Pass when |
|---|---|---|
accept-negotiation |
0.6 | A passes |
index-md-fallback |
0.3 | B passes |
token-savings-observed |
0.1 | ≥40% reduction |
Remediation Prompt
I want my site to respond with Markdown when an AI agent sends Accept: text/markdown.
Implement this in one of two ways (either is fine, both is better):
1) Content negotiation on the original URL.
When a request includes Accept: text/markdown, return:
- Status 200
- Content-Type: text/markdown; charset=utf-8
- A hand-curated or cleanly-generated markdown version of the page (no nav chrome, no cookie banner, no ads)
- A Vary: Accept header
2) URL-suffix fallback.
Serve the same markdown at /index.md relative to the HTML page's URL
(e.g. /docs/foo/ → /docs/foo/index.md or /docs/foo.md).
For tooling:
- If I'm on Next.js, generate markdown from the same MDX source the page is built from. Strip shell components (navbar, footer).
- If I'm on a CMS, export the post body as markdown and serve it directly.
- Make sure the markdown path is in my sitemap alternative or at least in /llms.txt.
Do not include HTML in the markdown version. Do not include navigation menus. Do include the page title as H1, the author/date as short metadata, and the full body.
Implementation Examples
Next.js dynamic (route handler)
// src/app/[...slug]/route.ts — runs BEFORE the page if mime is markdown
export async function GET(req: Request, ctx: { params: { slug?: string[] } }) {
const accept = req.headers.get('accept') ?? '';
if (!accept.includes('text/markdown')) return; // let page render HTML
const md = await buildMarkdownFor(ctx.params.slug);
return new Response(md, { headers: { 'content-type': 'text/markdown; charset=utf-8', vary: 'Accept' } });
}Cloudflare Workers (Rules)
- Request Header Transform: on
ends_with(http.request.uri.path, "/index.md"), setAccept: text/markdown - URL Rewrite: strip
/index.mdand proxy to same-path - Response Header Transform: add
Vary: Accept
See the Cloudflare docs article for the exact recipe.
Static site
Pre-generate *.md alongside every *.html at build time. Serve with the right content-type.
Common Mistakes
- Content-Type is
text/plain— fails the check; must betext/markdown. - Keeping the navigation sidebar inside the markdown → defeats the token-saving purpose.
- Infinite loop: agent requests
/index.md, server serves HTML with a hidden "prefer markdown" note that links to/index.md/index.md. (Strip the directive from the markdown version.) - Missing
Vary: Accept— caches may serve the wrong representation to the next client.
References
Test Fixtures
pass-accept.json— homepage returns markdown on Acceptpass-index-md.json— /index.md route returns markdownwarn-vary-html.json—Vary: Acceptbut body is HTMLfail-html-only.json— always HTML