The accept-markdown header
Accept: text/markdown is an HTTP content-negotiation header that signals an AI agent prefers clean markdown over HTML. Implementing it correctly means a Content-Type match, a Vary: Accept header, honoring q-values, and 406 on unsatisfiable requests.
What the header is
Accept: text/markdown is an RFC 9110 content-negotiation header. A client tells the server "I can handle markdown, and I prefer it". The server then picks the best representation it can serve.
Example request:
GET /docs/api HTTP/1.1
Accept: text/markdown;q=1.0, text/html;q=0.8, */*;q=0.1
User-Agent: Cursor/1.5
Example response:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Content-Length: 4137
Correct implementation: the four things
1. Serve the right Content-Type
If the client sends Accept: text/markdown and you have a markdown representation, return Content-Type: text/markdown; charset=utf-8.
2. Set Vary: Accept {#vary}
Without it, downstream caches will mix HTML and markdown responses under the same cache key. This is the single most common implementation bug.
3. Honor q-values {#q-values}
Accept: text/html;q=0.9, text/markdown;q=1.0 means "prefer markdown but accept html". Serve markdown.
4. 406 on unsatisfiable Accept {#406}
If a client sends Accept: application/vnd.pandoc and you can't produce that format, returning 406 Not Acceptable is the spec-compliant response. Rarely required, but it's the mark of a serious implementation.
Cloudflare Workers example
export default {
async fetch(req: Request) {
const accept = req.headers.get('accept') || ''
const prefersMarkdown = /text\/markdown/.test(accept)
&& !/text\/html/.test(accept.split(',')[0])
const pageUrl = new URL(req.url)
const htmlRes = await fetch(pageUrl.toString())
if (!prefersMarkdown) {
const res = new Response(htmlRes.body, htmlRes)
res.headers.set('Vary', 'Accept')
return res
}
const html = await htmlRes.text()
const markdown = convertHtmlToMarkdown(html) // e.g. mdream
return new Response(markdown, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Vary': 'Accept',
},
})
},
}
Vercel middleware example
import { NextResponse } from 'next/server'
export function middleware(req: Request) {
const accept = req.headers.get('accept') || ''
const wantsMd = accept.includes('text/markdown')
const res = wantsMd
? NextResponse.rewrite(new URL(`/api/render-md?u=${req.url}`, req.url))
: NextResponse.next()
res.headers.set('Vary', 'Accept')
return res
}