Files
Kevin Codex 2d873b3914 feat: serve raw markdown to text clients on /docs and repo pages
Clients that don't advertise text/html in Accept (curl, AI agents,
scripts) got an empty SPA shell from /docs, /docs/:slug, and
/repos/:owner/:name — the advertised agent entry points were blank
without JavaScript. Negotiate on the Accept header: text clients now
get the section's raw markdown (or a generated index for the docs hub)
and a markdown repo summary with the clone URL, owner DID, recent
signed push certificates, and raw node-API links.

Social and search crawlers send wildcard Accept headers too, so a UA
allowlist keeps serving them the meta-tagged HTML shell — link preview
cards are unaffected. All negotiated responses carry
Vary: Accept, User-Agent.
2026-08-17 16:11:52 +08:00

38 lines
1.6 KiB
TypeScript

// Content negotiation between the SPA's HTML shell and raw markdown.
//
// The explorer's primary audience includes AI agents and CLI tools that never
// execute JavaScript — to them the built SPA shell is an empty page. Any
// client that does not advertise `text/html` in its Accept header receives
// markdown instead.
//
// Social and search crawlers are the exception: many of them send a wildcard
// Accept header exactly like curl does, but they consume the meta-tagged HTML
// shell (OG cards, canonical URLs). They are recognized by user agent and
// always receive HTML, so link previews keep working.
const HTML_CRAWLERS =
/googlebot|bingbot|duckduckbot|yandex|baiduspider|applebot|facebookexternalhit|facebot|twitterbot|slackbot|slack-imgproxy|discordbot|linkedinbot|whatsapp|telegrambot|pinterestbot|redditbot|embedly|iframely|skypeuripreview/i;
/** Responses that negotiate on these headers must declare it to caches. */
export const NEGOTIATION_VARY = 'Accept, User-Agent';
export function prefersHtml(request: Request): boolean {
const accept = request.headers.get('accept') ?? '';
if (/\btext\/html\b/i.test(accept)) return true;
return HTML_CRAWLERS.test(request.headers.get('user-agent') ?? '');
}
export function markdownResponse(
body: string,
init: { status?: number; cacheControl: string },
): Response {
return new Response(body, {
status: init.status ?? 200,
headers: {
'Cache-Control': init.cacheControl,
'Content-Type': 'text/markdown; charset=utf-8',
'X-Content-Type-Options': 'nosniff',
Vary: NEGOTIATION_VARY,
},
});
}