Django
Add negotiation middleware in Django and return Markdown or HTML from the same view with proper Vary: Accept and 406 semantics.
Django handles this cleanly with one middleware + normal views.
middleware.py
from typing import Optional
from django.http import HttpResponse
PRODUCES = ["text/html", "text/markdown"]
def parse_accept(header: str):
entries = []
for raw in header.split(','):
parts = [p.strip() for p in raw.strip().split(';')]
media_type = (parts[0] if parts else '').lower()
if not media_type:
continue
q = 1.0
for param in parts[1:]:
name, _, value = param.partition('=')
if name.strip() == 'q':
try:
q = max(0.0, min(1.0, float(value.strip())))
except ValueError:
pass
specificity = 0 if media_type == '*/*' else 1 if media_type.endswith('/*') else 2
entries.append({"type": media_type, "q": q, "specificity": specificity})
return entries
def matches(entry, candidate: str) -> bool:
media_type = entry["type"]
if media_type == '*/*':
return True
if media_type.endswith('/*'):
return candidate.startswith(media_type[:-1])
return media_type == candidate
def preferred_type(header: Optional[str], produces):
if not header:
return produces[0] if produces else None
entries = parse_accept(header)
if not entries:
return produces[0] if produces else None
best = None
best_q = -1.0
best_pos = 10**9
for candidate in produces:
matched = None
matched_pos = 10**9
for idx, entry in enumerate(entries):
if not matches(entry, candidate):
continue
if (
matched is None
or entry["specificity"] > matched["specificity"]
or (entry["specificity"] == matched["specificity"] and idx < matched_pos)
):
matched = entry
matched_pos = idx
if matched is None or matched["q"] <= 0:
continue
if matched["q"] > best_q or (matched["q"] == best_q and matched_pos < best_pos):
best = candidate
best_q = matched["q"]
best_pos = matched_pos
return best
def _append_vary_accept(response):
tokens = [t.strip() for t in (response.get('Vary') or '').split(',') if t.strip()]
if not any(t.lower() == 'accept' for t in tokens):
tokens.append('Accept')
response['Vary'] = ', '.join(tokens)
class NegotiationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
accept = request.headers.get('Accept')
chosen = preferred_type(accept, PRODUCES)
request.prefers_markdown = chosen == 'text/markdown'
# Cache this on the request so views can decide whether to 406
# when a Markdown representation is missing.
request.accepts_html = preferred_type(accept, ['text/html']) is not None
if chosen is None and accept:
response = HttpResponse(
'Not Acceptable\n\nAvailable: text/html, text/markdown\n',
status=406,
content_type='text/plain; charset=utf-8',
)
_append_vary_accept(response)
return response
response = self.get_response(request)
_append_vary_accept(response)
return response
Add middleware in settings.py:
MIDDLEWARE = [
# ...
'yourapp.middleware.NegotiationMiddleware',
]
URLs
Two routes: the canonical URL (negotiates HTML vs. Markdown from
Accept) and an explicit .md sibling (always Markdown, for
crawlers that follow Link: rel="alternate").
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('docs/<slug>/', views.docs_view, name='docs'),
path('docs/<slug>.md', views.docs_markdown_view, name='docs-md'),
]
Views
from pathlib import Path
from django.http import Http404, HttpResponse
from django.shortcuts import render
def docs_view(request, slug: str):
"""Canonical URL. Negotiates HTML vs. Markdown via the middleware."""
base = Path('content/docs') / slug
md_path = base.with_suffix('.md')
html_path = base.with_suffix('.html')
if getattr(request, 'prefers_markdown', False):
if md_path.exists():
return HttpResponse(
md_path.read_text(encoding='utf-8'),
content_type='text/markdown; charset=utf-8',
)
# Markdown missing — 406 if HTML is also not acceptable.
if not getattr(request, 'accepts_html', True):
return HttpResponse(
'Not Acceptable\n\nMarkdown representation not available and HTML is not acceptable.\n',
status=406,
content_type='text/plain; charset=utf-8',
)
if html_path.exists():
response = render(request, 'docs/show.html', { 'slug': slug })
# Advertise the .md sibling via Link: rel="alternate" (RFC 8288)
# so agents like Codex can discover it without sending Accept.
if md_path.exists():
response['Link'] = f'</docs/{slug}.md>; rel="alternate"; type="text/markdown"'
return response
raise Http404()
def docs_markdown_view(request, slug: str):
"""Explicit .md sibling — always Markdown, no negotiation."""
md_path = Path('content/docs') / f'{slug}.md'
try:
body = md_path.read_text(encoding='utf-8')
except FileNotFoundError:
raise Http404()
return HttpResponse(body, content_type='text/markdown; charset=utf-8')
Verify
curl -sI -H "Accept: text/markdown" https://your-site.com/docs/intro
curl -sI -H "Accept: application/pdf" https://your-site.com/docs/intro