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