Quick start
The smallest working setup.
One URL. Two representations. One correct Vary header.
Every recipe on this site is a variation of the three steps below.
1. Detect the Accept header
When a request comes in, read Accept. If it asks for
text/markdown before text/html, you'll serve Markdown.
Otherwise, you'll serve HTML.
Don't substring-match. Parse the header, sort by q-value, break ties by specificity. Or use a library — every stack has one. See the Accept parsing guide for the gotchas.
2. Respond with the matching representation
You need two things in the response:
Content-Typematching what you served (text/markdown; charset=utf-8ortext/html; charset=utf-8)Vary: Acceptso caches key on the request header
If you can't satisfy the request at all (the client specifically asked for something you don't do), return 406 Not Acceptable instead of silently falling back.
3. Verify with curl
curl -sI -H "Accept: text/markdown" https://yoursite.com/article
You should see Content-Type: text/markdown and Vary: Accept. Swap the Accept header to text/html and you should get HTML back from the same URL.
Or use the probe on the home page to check any URL.
What's next
Pick your stack: Nginx, Caddy, WordPress, Laravel, Rails, Cloudflare Workers, Next.js, Astro, Apache, SvelteKit, Nuxt/Nitro, Express, Django. Each recipe is the three steps above translated into that stack's idioms, usually in under 20 lines of config or code.
If your site sits behind Cloudflare, you may not need to do any of this yourself — see Markdown for Agents.