All guides
Guide

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
}

Verify

Use our tester to replay Claude Code, Cursor, and Codex requests against your URL and confirm all four checks pass.

Spec references

Check this on your site

AI search visibility audit

One-click audit: llms.txt, Accept-header, robots.txt, sitemap.md, token savings.

Related guides