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.
# 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_filesfalls through to.htmlif no.mdexists — degrade gracefully rather than 404ing the Markdown-speakers.add_headerin a nestedlocationblock 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
locationblocks and return406from 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/htmlstill matches the markdown branch even though the client explicitly rejected Markdown.Accept: text/html, text/markdown;q=0.1still 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 for the algorithm.
The lazy option
If your site is behind Cloudflare, skip all of this and flip on Markdown for Agents. It does the negotiation at the edge with no origin changes.