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