Skip to main content
All Guides

Content Negotiation (Markdown for Agents)

Informational in 3.x — runs and reports but does not affect your AX score yet (it will gain weight in v4.0). Agents like Claude Code, Cursor, and OpenCode request pages with Accept: text/markdown. Serving Markdown instead of HTML cuts their token usage by roughly 80% for the same content. This check probes your homepage with that header and validates the response.

Homepage does not serve Markdown via content negotiation

Your server returned HTML (or another type) when asked for Accept: text/markdown. Content negotiation is standard HTTP: same URL, different representation depending on the Accept header.

The zero-code options first:

  • Cloudflare — Markdown for Agents: enable it in the dashboard and Cloudflare converts your HTML to Markdown at the edge for any request carrying Accept: text/markdown.
  • Vercel: supports Markdown content negotiation for framework deployments — verify it is active for your project.
  • Docs platforms: Mintlify and Read the Docs already serve Markdown variants out of the box.

Implementing it yourself (Express example):

app.get('*', (req, res, next) => {
  if (req.accepts(['text/html', 'text/markdown']) === 'text/markdown') {
    res.set('Content-Type', 'text/markdown; charset=utf-8');
    res.set('Vary', 'Accept');
    return res.send(renderPageAsMarkdown(req.path));
  }
  next(); // fall through to the regular HTML handler
});

Verify with curl:

curl -s -H "Accept: text/markdown" https://your-site.com | head
# Expected: Markdown content, not <!doctype html>

curl -sI -H "Accept: text/markdown" https://your-site.com | grep -i -E 'content-type|vary'
# Expected: Content-Type: text/markdown; charset=utf-8
#           Vary: Accept

Server responds 406 Not Acceptable

Your server performs strict content negotiation and rejects Accept: text/markdown outright instead of falling back to HTML. This is worse than ignoring the header — agents get nothing at all.

Configure your negotiation layer to treat HTML as the default representation when the requested type is unavailable, rather than returning 406. In most frameworks this means listing text/html as the fallback in the negotiation config.


No Markdown alternate link fallback

If you cannot enable negotiation on the same URL, you can still get partial credit by advertising a Markdown version of the page in your <head>:

<link rel="alternate" type="text/markdown" href="/index.md" title="Markdown version">

Agents that understand the pattern fetch /index.md directly. This is weaker than negotiation — it costs the agent an extra request and an extra discovery step — which is why it scores 40 instead of 100.


Labeled text/markdown but the body is an HTML document

Your server sets Content-Type: text/markdown but returns the same HTML document. Relabeling is not converting — agents parse the body expecting Markdown and get markup soup. This usually means a misconfigured rewrite rule or a middleware that changes the header without transforming the payload.

Note
Inline HTML inside Markdown is fine and the check tolerates it. What gets flagged is a full HTML document: a body starting with <!doctype html>, <html>, or <head>.

Markdown response body is empty

The negotiation works — correct Content-Type — but the body is empty. Common causes: the Markdown renderer receives no content for the route, an edge function returns early, or a converter fails silently on your HTML structure. Test the exact response:

curl -s -H "Accept: text/markdown" https://your-site.com | wc -c
# Expected: > 0

Vary header does not include Accept

When one URL serves two representations, every cache between you and the client needs to know the response depends on the Accept header. Without Vary: Accept, a CDN may cache the Markdown variant and serve it to browsers — or cache the HTML and serve it to agents, silently undoing your negotiation.

# Nginx
add_header Vary "Accept" always;

# Express
res.set('Vary', 'Accept');

# Vercel (vercel.json)
{
  "headers": [{
    "source": "/(.*)",
    "headers": [{ "key": "Vary", "value": "Accept" }]
  }]
}
Caveat
Vary: Accept fragments your cache by every distinct Accept value browsers send. If hit ratio matters, normalize the header at the edge (e.g., a Cloudflare worker that collapses Accept to two canonical values: markdown vs HTML).

Markdown response is not smaller than the HTML representation

The whole point of serving Markdown is token efficiency — Cloudflare measured a typical page at ~80% fewer tokens. If your Markdown is as large as your HTML, the converter is probably including navigation menus, footers, cookie banners, or raw markup remnants. Strip boilerplate and emit only the main content. This finding is informational and does not affect the check score.


Could not fetch the homepage with Accept: text/markdown

The probe request failed at the network level — timeout, connection refused, or TLS failure. If the regular audit checks pass, this usually means a WAF rule rejects requests with unusual Accept headers. Reproduce with:

curl -v -H "Accept: text/markdown" https://your-site.com -o /dev/null

Check your WAF / bot-management logs for the blocked request and add an exception for the Accept: text/markdown pattern.