# Nginx

Serve Markdown from the same URL as HTML using Nginx's `map` + `try_files`.

Nginx can content-negotiate without a downstream app. Pre-render each
page to both `.html` and `.md` at build time, then let Nginx pick.

```nginx
# Map the Accept header to a file extension we'll try first.
map $http_accept $preferred_ext {
    default                 ".html";
    "~*text/markdown"       ".md";
}

server {
    listen 443 ssl http2;
    server_name yoursite.com;
    root /var/www/yoursite;

    # Always tell caches the response depends on Accept.
    add_header Vary Accept always;

    location / {
        # Try the preferred extension at both the sibling-file layout
        # (/about.md) and the directory-index layout (/about/index.md),
        # then fall back to HTML. Without the second try, a request for
        # `/about/` with Accept: text/markdown silently falls through to
        # /about/index.html even when /about/index.md exists.
        try_files $uri$preferred_ext $uri/index$preferred_ext $uri.html $uri/index.html =404;
    }

    # Set Content-Type explicitly for .md (Nginx doesn't know it).
    location ~* \.md$ {
        default_type text/markdown;
        charset utf-8;
        add_header Vary Accept always;
    }
}
```

## Notes

- `try_files` falls through to `.html` if no `.md` exists — degrade
  gracefully rather than 404ing the Markdown-speakers.
- `add_header` in a nested `location` block replaces the outer one; the
  duplication is intentional, not a copy-paste error.
- For a true "no Markdown available, reject instead of falling back"
  setup, use two separate `location` blocks and return `406` from the
  Markdown branch when the file is missing.

## Caveat: this is pragmatic, not strict

The `map` directive substring-matches the Accept header. That's fine
for the common case — almost no agent sends `q=0` on `text/markdown`,
and q-value tiebreaks rarely matter for a two-type produces list. But
it technically means:

- `Accept: text/markdown;q=0, text/html` still matches the markdown
  branch even though the client explicitly rejected Markdown.
- `Accept: text/html, text/markdown;q=0.1` still prefers markdown even
  though HTML has higher priority.

For strict handling, reverse-proxy the `@markdown` branch to a small
application that parses Accept properly. See
[Accept parsing & q-values](/guides/accept-parsing) for the algorithm.

## The lazy option

If your site is behind Cloudflare, skip all of this and flip on
[Markdown for Agents](/guides/cloudflare-markdown-for-agents). It does
the negotiation at the edge with no origin changes.