Express
A plain Express middleware + route pattern for strict Accept negotiation with Markdown siblings, Vary: Accept, 406, and Link: rel="alternate".
Express is the most portable baseline: one middleware picks representation, routes return markdown or HTML accordingly.
server.js
import express from 'express';
import fs from 'node:fs/promises';
import path from 'node:path';
const app = express();
const PRODUCES = ['text/html', 'text/markdown'];
function parseAccept(header) {
return header
.split(',')
.map((raw) => {
const parts = raw.trim().split(';').map((s) => s.trim());
const type = (parts[0] || '').toLowerCase();
if (!type) return null;
let q = 1;
for (const param of parts.slice(1)) {
const [name, value] = param.split('=').map((s) => s.trim());
if (name === 'q') {
const parsed = Number(value);
if (!Number.isNaN(parsed)) q = Math.max(0, Math.min(1, parsed));
}
}
const specificity = type === '*/*' ? 0 : type.endsWith('/*') ? 1 : 2;
return { type, q, specificity };
})
.filter(Boolean);
}
function matches(entry, candidate) {
if (entry.type === '*/*') return true;
if (entry.type.endsWith('/*')) return candidate.startsWith(entry.type.slice(0, -1));
return entry.type === candidate;
}
function preferredType(header, produces) {
if (!header) return produces[0] || null;
const entries = parseAccept(header);
if (!entries.length) return produces[0] || null;
let best = null;
let bestQ = -1;
let bestPos = Infinity;
for (const candidate of produces) {
let matched = null;
let matchedPos = Infinity;
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
if (!matches(e, candidate)) continue;
if (matched === null || e.specificity > matched.specificity || (e.specificity === matched.specificity && i < matchedPos)) {
matched = e;
matchedPos = i;
}
}
if (!matched || matched.q <= 0) continue;
if (matched.q > bestQ || (matched.q === bestQ && matchedPos < bestPos)) {
best = candidate;
bestQ = matched.q;
bestPos = matchedPos;
}
}
return best;
}
app.use((req, res, next) => {
const chosen = preferredType(req.header('accept') ?? null, PRODUCES);
res.vary('Accept');
req.prefersMarkdown = chosen === 'text/markdown';
if (chosen === null && req.header('accept')) {
return res
.status(406)
.type('text/plain; charset=utf-8')
.send('Not Acceptable\n\nAvailable: text/html, text/markdown\n');
}
next();
});
async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
// Explicit .md sibling. Matched before /docs/:slug via regex so a
// request for `/docs/foo.md` doesn't fall into the canonical route.
app.get(/^\/docs\/([^/]+)\.md$/, async (req, res) => {
const slug = req.params[0];
try {
const md = await fs.readFile(
path.join(process.cwd(), 'content', 'docs', `${slug}.md`),
'utf8',
);
return res
.type('text/markdown; charset=utf-8')
.set('Vary', 'Accept')
.send(md);
} catch {
return res.status(404).send('Not found');
}
});
app.get('/docs/:slug', async (req, res) => {
const slug = req.params.slug;
const base = path.join(process.cwd(), 'content', 'docs', slug);
if (req.prefersMarkdown) {
try {
const md = await fs.readFile(`${base}.md`, 'utf8');
return res.type('text/markdown; charset=utf-8').send(md);
} catch {
// Markdown missing. Only fall through to HTML if HTML is still
// acceptable — `Accept: text/markdown, text/html;q=0` forbids it.
if (!preferredType(req.header('accept') ?? null, ['text/html'])) {
return res
.status(406)
.type('text/plain; charset=utf-8')
.send('Not Acceptable\n\nMarkdown representation not available and HTML is not acceptable.\n');
}
}
}
try {
const html = await fs.readFile(`${base}.html`, 'utf8');
res.type('text/html; charset=utf-8');
// Advertise the .md sibling via Link: rel="alternate" (RFC 8288)
// so agents (like Codex) can discover it without sending Accept.
if (await exists(`${base}.md`)) {
res.set('Link', `</docs/${slug}.md>; rel="alternate"; type="text/markdown"`);
}
return res.send(html);
} catch {
return res.status(404).send('Not found');
}
});
app.listen(3000);
Verify
curl -sI -H "Accept: text/markdown" http://localhost:3000/docs/intro
curl -sI -H "Accept: application/pdf" http://localhost:3000/docs/intro