mirror of
https://github.com/Twigpine/node-explorer.git
synced 2026-11-10 17:57:39 -10:00
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.
38 lines
1.6 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
}
|