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

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