# Caddy

Caddy's named matchers make content negotiation concise. Serve pre-rendered `.md` files to agents and HTML to browsers from the same URL.

Caddy has first-class support for matching on request headers, which
makes Accept-based routing a few lines of config. This recipe assumes
you've pre-rendered both `.html` and `.md` variants of each page at
build time.

## Caddyfile

```caddyfile
yoursite.com {
    root * /var/www/yoursite

    @markdown header Accept *text/markdown*

    handle @markdown {
        # `{path}index.md` (no slash) resolves both layouts:
        #   /about  → /about.md
        #   /about/ → /about/index.md
        #   /       → /index.md
        try_files {path}.md {path}index.md
        header Content-Type "text/markdown; charset=utf-8"
        header Vary Accept
        file_server
    }

    handle {
        header Vary Accept
        try_files {path} {path}.html {path}/index.html
        file_server
    }
}
```

## How it works

- `@markdown` is a named matcher. It matches requests where the Accept
  header contains `text/markdown` (the `*` on each side is a substring
  glob). This is the rare case where substring-matching Accept is
  acceptable — Caddy handles the header value as a plain string, and
  you're checking for presence, not precedence.
- `handle @markdown` — if that matches, `try_files` picks the first
  existing Markdown file (sibling or directory index), sets the right
  Content-Type and Vary, and serves it.
- `handle {}` (no matcher) — the fallback. Set Vary, try the path plus
  `.html` variants, serve HTML.
- Both branches set `Vary: Accept`, so CDNs downstream cache correctly.

## A more correct matcher

The simple header matcher above doesn't respect q-values. If a client
sends `Accept: text/markdown;q=0, text/html` (explicitly rejecting
Markdown), it would still match. For stricter handling, use a
`request_matcher` module or proxy to an app that parses Accept properly.

In practice, nobody sends `Accept: text/markdown;q=0` — clients only
use `q=0` when explicitly rejecting, which for Markdown is rare. The
substring match is pragmatic.

## If you don't pre-render

If your Markdown doesn't exist as a file, reverse-proxy the
`@markdown` branch to a small service that converts HTML on the fly,
or use [Cloudflare's Markdown for Agents](/guides/cloudflare-markdown-for-agents)
at the edge and skip Caddy negotiation entirely.

```caddyfile
handle @markdown {
    reverse_proxy localhost:3001 {
        header_up Accept "text/markdown"
    }
    header Vary Accept
}
```

## Verify

```bash
curl -sI -H "Accept: text/markdown" https://yoursite.com/article
# Content-Type: text/markdown; charset=utf-8
# Vary: Accept

curl -sI -H "Accept: text/html" https://yoursite.com/article
# Content-Type: text/html; charset=utf-8
# Vary: Accept
```