/** * HTTP content negotiation for the raw-Markdown variants. The `.md` * endpoints already serve a page's source verbatim; these helpers let the dev * server honor `Accept: text/markdown` by transparently rewriting a page * request to its `.md` variant. * * The Cloudflare negotiation Worker embeds a plain-JavaScript copy of these * helpers (`deploy/cloudflare-negotiation.ts` — its deploy bundle is uploaded * unbundled, so it cannot import this module); when editing here, mirror the * change there. Parity is enforced by `test/cloudflare-negotiation.test.ts`. */ interface AcceptEntry { q: number; type: string; } const parseAccept = (accept: string): AcceptEntry[] => accept.split(",").map((part) => { const segments = part.trim().split(";"); const type = (segments[0] ?? "").trim().toLowerCase(); const qSegment = segments .slice(1) .map((segment) => segment.trim()) .find((segment) => segment.startsWith("q=")); const q = qSegment ? Number(qSegment.slice(2)) : 1; return { q: Number.isNaN(q) ? 1 : q, type }; }); /** * Whether the client explicitly prefers Markdown over HTML. Browsers never send * `text/markdown`, so an ordinary page request (`text/html`, `*​/*`) is false. */ export const prefersMarkdown = (accept: string | null | undefined): boolean => { if (!accept) { return false; } let markdownQ = -1; let htmlQ = 0; for (const { q, type } of parseAccept(accept)) { if (type === "text/markdown" || type === "text/x-markdown") { markdownQ = Math.max(markdownQ, q); } else if (type === "text/html") { htmlQ = Math.max(htmlQ, q); } } return markdownQ > 0 && markdownQ >= htmlQ; }; /** * Map a page request URL to its `.md` variant, or `null` when the requested * path is not a known content route (Vite/Astro internals, assets, API routes, * landing pages, and user `.astro` pages all fall through). `routes` is the set * of page paths that have a raw-Markdown variant. */ export const markdownVariantUrl = ( rawUrl: string | null | undefined, routes: ReadonlySet, base?: string ): string | null => { if (!rawUrl) { return null; } const queryIndex = rawUrl.indexOf("?"); const query = queryIndex === -1 ? "" : rawUrl.slice(queryIndex); const rawPath = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex); // A non-root `deployment.base` prefixes the dev-server URL but not the logical // content routes, so strip it before matching and re-add it to the variant. const prefix = base && base !== "/" ? base.replace(/\/$/u, "") : ""; let path = rawPath; if (prefix) { if (path === prefix || path.startsWith(`${prefix}/`)) { path = path.slice(prefix.length) || "/"; } else { return null; } } const trimmed = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path; // Requests arrive percent-encoded while routes are stored decoded, so a // non-ASCII route (`/ja/はじめに` requested as `/ja/%E3%81%AF…`) must be // decoded before the lookup. Malformed sequences stay verbatim. let pathname = trimmed; try { pathname = decodeURIComponent(trimmed); } catch { // Keep the raw path; it simply won't match a content route. } if (!routes.has(pathname)) { return null; } const target = pathname === "/" ? "/index" : pathname; // Re-encode: the variant URL goes back into the request pipeline. return `${prefix}${encodeURI(target)}.md${query}`; };