Apache

Serve prebuilt Markdown and HTML from the same canonical URL in Apache using rewrite + proxy fallback for strict Accept handling.

Apache can do pragmatic negotiation in config, but strict q-value behavior (q=0, wildcard precedence, tie-breaking) is easier in a tiny app layer. This recipe gives both patterns.

Option A: static-first (pragmatic, not strict)

This is the minimal setup if your build emits both index.html and index.md. It is not RFC-9110-correct — Apache rewrite conditions can’t express q-value precedence, specificity tiebreaks, or text/html;q=0 rejections. It also doesn’t emit Link: rel="alternate" to advertise the Markdown sibling via RFC 8288. Use Option B if you want spec-correct behavior for q=0, mixed wildcards, or 406 on unsatisfiable Accept.

# .htaccess or vhost context
RewriteEngine On

# Skip real files/dirs and obvious assets.
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteCond %{REQUEST_URI} \.(?:css|js|mjs|map|png|jpe?g|webp|gif|svg|avif|ico|woff2?|ttf|otf|eot|xml|txt|json|pdf|mp4|webm|mp3|wav|ogg|zip)$ [NC]
RewriteRule ^ - [L]

# Ask for markdown? try .md first. The `;q=0` guard avoids serving
# Markdown to a client that explicitly rejected it via `text/markdown;q=0`.
# Two rules cover both common build layouts: sibling files (/about.md)
# and directory indexes (/about/index.md).
RewriteCond %{HTTP:Accept} "text/markdown" [NC]
RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0" [NC]
RewriteCond %{DOCUMENT_ROOT}/$1.md -f
RewriteRule ^(.+?)/?$ /$1.md [L]

RewriteCond %{HTTP:Accept} "text/markdown" [NC]
RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0" [NC]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}/index.md -f
RewriteRule ^(.+?)/?$ /$1/index.md [L]

# Default to HTML — same pair of layouts.
RewriteCond %{DOCUMENT_ROOT}/$1.html -f
RewriteRule ^(.+?)/?$ /$1.html [L]

RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}/index.html -f
RewriteRule ^(.+?)/?$ /$1/index.html [L]

# Root helpers.
RewriteRule ^$ /index.html [L]

# Ensure type + vary.
<FilesMatch "\\.md$">
  ForceType text/markdown
  Header set Content-Type "text/markdown; charset=utf-8"
</FilesMatch>
Header merge Vary Accept

Known limitations of Option A: no 406 when the client rejects both representations, no Link: rel="alternate" advertising on HTML, substring matching can misfire on exotic Accept headers.

Keep Apache serving static files, but proxy only content routes to a tiny app that parses Accept correctly and picks text/html vs text/markdown.

RewriteEngine On

# Leave assets and API routes alone.
RewriteCond %{REQUEST_URI} ^/(?:api/|_next/|assets/) [NC,OR]
RewriteCond %{REQUEST_URI} \.(?:css|js|png|jpe?g|webp|gif|svg|ico|woff2?|map|txt|xml|json)$ [NC]
RewriteRule ^ - [L]

# Proxy content requests to app server for strict negotiation.
RewriteRule ^ http://127.0.0.1:3000%{REQUEST_URI} [P,L]

ProxyPassReverse / http://127.0.0.1:3000/
Header merge Vary Accept

The app server should return:

  • 200 text/markdown when markdown is preferred + available
  • 200 text/html otherwise
  • 406 Not Acceptable when client rejects both
  • Link: </path/index.md>; rel="alternate"; type="text/markdown" on HTML when sibling exists

Verify

curl -sI https://your-site.com/about/
# Content-Type: text/html; charset=utf-8
# Vary: Accept

curl -sI -H "Accept: text/markdown" https://your-site.com/about/
# Content-Type: text/markdown; charset=utf-8
# Vary: Accept

curl -sI -H "Accept: application/pdf" https://your-site.com/about/
# HTTP/2 406