/** * Runtime-neutral HTML shell — identical templates to `html.ts` (Bun-only, * protected) but escaping via the pure `escapeHtml()` so it runs on * Node.js and Deno as well as Bun. Shared modules (`server-routes.ts`) and * the non-Bun entries import from here; `build.ts`/`server.ts` keep using * `html.ts` untouched. */ import { escapeHtml } from "./escapeHtml"; import { cspMeta } from "./security"; import type { SeoMeta } from "./seo"; export interface HtmlShellOptions { title: string; description: string; body: string; favicon: string; css: string; js?: string; nonce?: string; /** * Content-Security-Policy value (from `cspHeader()` in security.ts). * When provided, injects `` in ``. * Essential for static deployment where HTTP headers cannot be set. */ csp?: string; extraScripts?: string; themeCss?: string; /** Depth from document root (0=root, 1=subdir, 2=sub/subdir). Used for relative asset paths. */ depth?: number; /** HTML strings to inject before `` (from plugin `injectHead` hooks). */ headExtra?: string[]; /** HTML strings to inject before ``, after the main script (from plugin `injectBody` hooks). */ bodyExtra?: string[]; /** SEO meta tags derived from config + frontmatter */ seo?: SeoMeta; /** Root-absolute asset URLs (`/assets/...`). Required for pages served at * arbitrary paths (404 fallback) — relative depth is wrong there. */ absoluteAssets?: boolean; } export function htmlShell(opts: HtmlShellOptions): string { const { title, description, body, favicon, css, js, nonce, csp, extraScripts, themeCss, depth = 0, headExtra, bodyExtra, absoluteAssets = false, } = opts; const nonceAttr = nonce ? ` nonce="${escapeHtml(nonce)}"` : ""; const themeStyle = themeCss ? `\n ${escapeHtml(themeCss)}` : ""; const headInjection = headExtra?.length ? `\n ${headExtra.join("\n ")}` : ""; const bodyInjection = bodyExtra?.length ? `\n ${bodyExtra.join("\n ")}` : ""; const depthPrefix = depth === 0 ? "" : "../".repeat(depth); const assetPrefix = absoluteAssets ? "/assets/" : depthPrefix + "assets/"; const clientScript = js ? `\n \n ` : ""; const resolvePath = (path: string) => absoluteAssets ? path : path.startsWith("/") ? depthPrefix + path.slice(1) : path; // Build SEO meta tags (OG, Twitter, canonical) let seoTags = ""; if (opts.seo) { const s = opts.seo; const e = escapeHtml; seoTags = `\n \n \n \n \n \n \n `; if (s.image) { seoTags += `\n `; } } return ` ${escapeHtml(title)} ${favicon ? `` : ""}${themeStyle} ${csp ? `` : ""} ${seoTags} try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}${headInjection}
${body}
${clientScript}${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection} `; } export function errorHtml(message: string, stack?: string): string { const msg = escapeHtml(message || "Unknown error"); const st = escapeHtml(stack || ""); return ` Server Error

🔥 Server Error

${msg}${st ? `\n\n${st}` : ""}
`; } export function hmrScript(nonce: string): string { return ``; }