# Content Negotiation

> Serve Markdown to AI agents via Accept: text/markdown, send Vary: Accept, avoid relabeled HTML, and advertise rel=alternate fallbacks.

Cette vérification représente 4 % du score.

*Ces guides de correction sont rédigés en anglais.*

<a id="not-supported"></a>

## 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
```

---

<a id="http-406"></a>

## 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.

---

<a id="no-alternate"></a>

## 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.

---

<a id="mislabeled-content-type"></a>

## 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>
>
> .

---

<a id="empty-body"></a>

## 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
```

---

<a id="missing-vary"></a>

## 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).

---

<a id="not-smaller"></a>

## 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.

---

<a id="fetch-error"></a>

## 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.

---

<a id="no-frontmatter"></a>

## The Markdown has no frontmatter

You negotiate correctly and return real Markdown, and the response arrives with no provenance. Stripping the HTML also stripped the canonical link, the title tag and the metadata, so an agent that quotes this page cannot say where the quote came from.

```
---
title: How content negotiation works
canonical_url: https://example.com/guides/content-negotiation
last_updated: 2026-09-01
description: Serving Markdown to agents from the same URL as the HTML.
---

# How content negotiation works
...
```

`title` and `canonical_url` are the two that matter: together they make the response attributable. Add `last_updated` where you have it, since an agent has no other way to judge whether what it is reading is current.

---

<a id="empty-frontmatter"></a>

## The frontmatter carries nothing useful

A YAML block is present but holds none of `title`, `canonical_url`, `url`, `last_updated` or `description`. Build metadata, template names and layout keys are not wrong to include, but they answer a question nobody asked.

This is normally a generator emitting its own front matter verbatim. Map the fields the page already has (its title, its canonical URL, its modified date) into the block, and keep whatever else you were emitting alongside them.

---

Représentation Markdown de https://axrush.com/guides/content-negotiation
