/** * Page metadata collection and rendering * * Collects typed metadata contributions from plugins via the page:metadata hook, * validates them, and resolves them into a deduplicated structure ready to render. */ import type { PageMetadataContribution, PageMetadataLinkRel } from "../plugins/types.js"; // ── Resolved output ───────────────────────────────────────────── export interface ResolvedPageMetadata { meta: Array<{ name: string; content: string }>; properties: Array<{ property: string; content: string }>; links: Array<{ rel: PageMetadataLinkRel; href: string; hreflang?: string; }>; jsonld: Array<{ id?: string; json: string }>; } interface RenderPageMetadataOptions { includeJsonLd?: boolean; } // ── Validation ────────────────────────────────────────────────── /** Schemes safe for use in link href attributes */ const SAFE_HREF_RE = /^(https?|at):\/\//i; const HTML_ESCAPE_MAP: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; const HTML_ESCAPE_RE = /[&<>"']/g; /** Escape a string for safe use in an HTML attribute value */ export function escapeHtmlAttr(value: string): string { return value.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch] ?? ch); } /** Validate that a URL uses a safe scheme (http, https, at) */ function isSafeHref(url: string): boolean { return SAFE_HREF_RE.test(url); } // ── JSON-LD serialization ─────────────────────────────────────── const JSONLD_LT_RE = //g; const JSONLD_U2028_RE = /\u2028/g; const JSONLD_U2029_RE = /\u2029/g; /** * Safely serialize a value for embedding in a " in a nested string breaks out of the script tag * - "