/** * @wrnexus/ssr — server-side rendering. * * Pages return an HTML string for the body; this module wraps that body in a * full document with a `` built from page metadata. It is intentionally * isolated from any client runtime: nothing here touches the DOM or ships to * the browser, which keeps "server-only code" genuinely server-only. */ import { escapeHtml, type PageMeta, type SeoConfig } from "@wrnexus/core"; export interface RenderOptions { /** Page metadata for the document head. */ meta: PageMeta; /** Global SEO defaults from `wrnexus.config.ts`. */ seo?: SeoConfig; /** Current request URL, used to resolve canonical/Open Graph URLs. */ url?: URL; /** Rendered HTML for the body (placed inside `#app`). */ body: string; /** * URLs of ``) .join(""); const extraHead = opts.extraHead ? `\n ${opts.extraHead}` : ""; const extraBody = opts.extraBody ? `\n ${opts.extraBody}` : ""; const htmlAttrs = /(?:^|\s)lang\s*=/.test(opts.htmlAttrs ?? "") ? (opts.htmlAttrs ?? "") : `${opts.htmlAttrs ?? ""} lang="en"`; return ` ${title}${seoTags}${preloadTags}${extraHead}
${opts.body}
${scriptTags}${extraBody} `; } interface ResolvedSeo { title: string; description?: string; canonical?: string; robots?: string; keywords?: string; image?: string; siteName?: string; type: string; locale?: string; twitterCard: string; twitterSite?: string; themeColor?: string; } function mergeSeo( globalSeo: SeoConfig | undefined, pageSeo: PageMeta, defaultTitle: string | undefined, url: URL | undefined, ): ResolvedSeo { const pageTitle = pageSeo.title; const baseTitle = pageTitle ?? globalSeo?.title ?? defaultTitle ?? "WrNexus"; const titleTemplate = pageTitle ? globalSeo?.titleTemplate : undefined; const title = titleTemplate?.includes("%s") ? titleTemplate.replace("%s", baseTitle) : baseTitle; const canonical = resolveSeoUrl( pageSeo.canonical ?? globalSeo?.canonical ?? (globalSeo?.canonicalBase && url ? url.pathname : undefined), pageSeo.canonicalBase ?? globalSeo?.canonicalBase, url, ); const image = resolveSeoUrl( pageSeo.image ?? globalSeo?.image, pageSeo.canonicalBase ?? globalSeo?.canonicalBase, url, ); const keywords = pageSeo.keywords ?? globalSeo?.keywords; return { title, description: pageSeo.description ?? globalSeo?.description, canonical, robots: pageSeo.robots ?? globalSeo?.robots, keywords: Array.isArray(keywords) ? keywords.join(", ") : keywords, image, siteName: pageSeo.siteName ?? globalSeo?.siteName, type: pageSeo.type ?? globalSeo?.type ?? "website", locale: pageSeo.locale ?? globalSeo?.locale, twitterCard: pageSeo.twitterCard ?? globalSeo?.twitterCard ?? "summary", twitterSite: pageSeo.twitterSite ?? globalSeo?.twitterSite, themeColor: pageSeo.themeColor ?? globalSeo?.themeColor, }; } function renderSeoTags(seo: ResolvedSeo): string { const tags: string[] = []; if (seo.description) tags.push(meta("description", seo.description)); if (seo.robots) tags.push(meta("robots", seo.robots)); if (seo.keywords) tags.push(meta("keywords", seo.keywords)); if (seo.themeColor) tags.push(meta("theme-color", seo.themeColor)); if (seo.canonical) tags.push(``); tags.push(property("og:title", seo.title)); if (seo.description) tags.push(property("og:description", seo.description)); tags.push(property("og:type", seo.type)); if (seo.canonical) tags.push(property("og:url", seo.canonical)); if (seo.siteName) tags.push(property("og:site_name", seo.siteName)); if (seo.locale) tags.push(property("og:locale", seo.locale)); if (seo.image) tags.push(property("og:image", seo.image)); tags.push(meta("twitter:card", seo.twitterCard)); tags.push(meta("twitter:title", seo.title)); if (seo.description) tags.push(meta("twitter:description", seo.description)); if (seo.image) tags.push(meta("twitter:image", seo.image)); if (seo.twitterSite) tags.push(meta("twitter:site", seo.twitterSite)); return tags.length ? `\n ${tags.join("\n ")}` : ""; } function meta(name: string, content: string): string { return ``; } function property(name: string, content: string): string { return ``; } function resolveSeoUrl( value: string | undefined, base: string | undefined, currentUrl: URL | undefined, ): string | undefined { if (!value) return undefined; try { const origin = base ?? (currentUrl ? currentUrl.origin : undefined); return origin ? new URL(value, origin).toString() : value; } catch { return value; } }