SvelteKit
Use a SvelteKit handle hook for Accept negotiation and serve Markdown from route endpoints with Vary: Accept, 406, and Link: rel="alternate".
SvelteKit is a good fit for strict content negotiation: put Accept parsing in
hooks.server.ts, then return markdown from endpoint handlers.
src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
type AcceptEntry = { type: string; q: number; specificity: number };
function parseAccept(header: string): AcceptEntry[] {
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((e): e is AcceptEntry => e !== null);
}
function matches(entry: AcceptEntry, candidate: string): boolean {
if (entry.type === '*/*') return true;
if (entry.type.endsWith('/*')) return candidate.startsWith(entry.type.slice(0, -1));
return entry.type === candidate;
}
function preferredType(header: string | null, produces: string[]): string | null {
if (!header) return produces[0] ?? null;
const entries = parseAccept(header);
if (entries.length === 0) return produces[0] ?? null;
let bestType: string | null = null;
let bestQ = -1;
let bestPos = Infinity;
for (const candidate of produces) {
let matched: AcceptEntry | null = 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)) {
bestQ = matched.q;
bestPos = matchedPos;
bestType = candidate;
}
}
return bestType;
}
function appendVaryAccept(headers: Headers): void {
const existing = headers.get('vary');
if (!existing) {
headers.set('Vary', 'Accept');
return;
}
const tokens = existing.split(',').map((s) => s.trim().toLowerCase());
if (!tokens.includes('accept')) headers.set('Vary', `${existing}, Accept`);
}
// Resolve a request pathname to its Markdown sibling on disk.
// Called for both `/docs/foo` and `/docs/foo.md`.
// Return null for any URL that has no Markdown representation.
// Customize the regex and path to match your content layout.
function siblingFor(pathname: string): { canonical: string; mdPath: string } | null {
const clean = pathname.replace(/\/$/, '');
const isMdRequest = clean.endsWith('.md');
const base = isMdRequest ? clean.slice(0, -3) : clean;
const m = base.match(/^\/docs\/([^/]+)$/);
if (!m) return null;
return { canonical: base, mdPath: `content/docs/${m[1]}.md` };
}
async function readIfExists(path: string): Promise<string | null> {
try {
const { readFile } = await import('node:fs/promises');
return await readFile(path, 'utf8');
} catch {
return null;
}
}
export const handle: Handle = async ({ event, resolve }) => {
const pathname = event.url.pathname;
const sibling = siblingFor(pathname);
// Explicit .md URL: always Markdown, regardless of Accept. This is
// the path that Link: rel="alternate" points at — crawlers that
// follow it may not send an Accept header at all.
if (sibling && pathname.replace(/\/$/, '').endsWith('.md')) {
const body = await readIfExists(sibling.mdPath);
if (body === null) return new Response('Not found', { status: 404 });
const res = new Response(body, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
appendVaryAccept(res.headers);
return res;
}
const accept = event.request.headers.get('accept');
const chosen = preferredType(accept, ['text/html', 'text/markdown']);
const acceptsHtml = !!preferredType(accept, ['text/html']);
if (chosen === null && accept) {
const res = new Response('Not Acceptable\n\nAvailable: text/html, text/markdown\n', {
status: 406,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
appendVaryAccept(res.headers);
return res;
}
// Read the sibling once — used by both branches.
const mdBody = sibling ? await readIfExists(sibling.mdPath) : null;
// Markdown preferred on the canonical URL: short-circuit SvelteKit.
if (chosen === 'text/markdown') {
if (mdBody !== null) {
const res = new Response(mdBody, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
});
appendVaryAccept(res.headers);
return res;
}
// Sibling missing — 406 if HTML is also not acceptable.
if (!acceptsHtml) {
const res = new Response(
'Not Acceptable\n\nMarkdown representation not available and HTML is not acceptable.\n',
{ status: 406, headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
);
appendVaryAccept(res.headers);
return res;
}
// Fall through to HTML.
}
// HTML: let SvelteKit render, then advertise the sibling via Link
// (RFC 8288) so agents like Codex can discover it without Accept.
const response = await resolve(event);
appendVaryAccept(response.headers);
if (mdBody !== null && sibling && response.headers.get('content-type')?.includes('text/html')) {
const linkValue = `<${sibling.canonical}.md>; rel="alternate"; type="text/markdown"`;
const existing = response.headers.get('link');
response.headers.set('Link', existing ? `${existing}, ${linkValue}` : linkValue);
}
return response;
};
Add locals typing (empty now — the hook no longer passes flags downstream, but this is where you’d add per-route state later):
// src/app.d.ts
declare global {
namespace App {
interface Locals {}
}
}
export {};
The canonical URL (/docs/foo) now serves both representations from
one place — browsers get HTML rendered from src/routes/docs/[slug]/+page.svelte
(plus any +page.server.ts you already have), and Accept-aware agents
get the raw .md file short-circuited in the hook. No separate
.md route needed.
For large sites, cache readIfExists behind an LRU or an in-memory
map keyed by mtime — the recipe reads the file on every Markdown-
preferring request.
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