#!/usr/bin/env node /* * build-fonts-css.cjs — bundle the template fonts into ONE self-contained * fonts.css with base64 data-URI @font-face (no sub-resources, no CDN, no * system-font dependency). This is what makes Standard/Cinematic renders * deterministic on ANY machine: hyperframes' renderer only auto-supplies its * ~18 CANONICAL_FONTS; every other family falls back to a generic font unless * the HTML ships a local @font-face. (hyperframes' own font linter says exactly * this: "Use local @font-face declarations with captured .woff2 files.") * * Source woff2 live in ./files/-latin--normal.woff2 (latin subset * from @fontsource via jsdelivr). Re-run after adding/removing a font: * node build-fonts-css.cjs */ const fs = require("fs"); const path = require("path"); const FILES = path.join(__dirname, "files"); const OUT = path.join(__dirname, "fonts.css"); // slug → exact CSS family name (must match what templates declare in font-family). const FAMILY = { anton: "Anton", audiowide: "Audiowide", inter: "Inter", "baloo-2": "Baloo 2", bangers: "Bangers", "bodoni-moda": "Bodoni Moda", caveat: "Caveat", cinzel: "Cinzel", "cormorant-garamond": "Cormorant Garamond", creepster: "Creepster", fredoka: "Fredoka", monoton: "Monoton", orbitron: "Orbitron", "permanent-marker": "Permanent Marker", "press-start-2p": "Press Start 2P", "saira-stencil-one": "Saira Stencil One", "shippori-mincho": "Shippori Mincho", rajdhani: "Rajdhani", "chakra-petch": "Chakra Petch", sora: "Sora", "space-grotesk": "Space Grotesk", "special-elite": "Special Elite", teko: "Teko", vt323: "VT323", }; const files = fs .readdirSync(FILES) .filter((f) => f.endsWith(".woff2")) .sort(); const faces = []; let raw = 0; const seenFamilies = new Set(); for (const f of files) { const m = f.match(/^(.*)-latin-(\d+)-(normal|italic)\.woff2$/); if (!m) { console.error(`[fonts] skip unrecognized filename: ${f}`); continue; } const slug = m[1], weight = m[2], style = m[3]; const family = FAMILY[slug]; if (!family) { console.error(`[fonts] no family mapping for slug "${slug}" (${f}) — add it to FAMILY`); process.exit(1); } const buf = fs.readFileSync(path.join(FILES, f)); raw += buf.length; seenFamilies.add(family); faces.push( `@font-face {\n` + ` font-family: '${family}';\n` + ` font-style: ${style};\n` + ` font-weight: ${weight};\n` + ` font-display: block;\n` + // block (not swap): render text only once the real face is ready — measure-layout + capture see the true glyphs, never a fallback flash ` src: url(data:font/woff2;base64,${buf.toString("base64")}) format('woff2');\n` + `}`, ); } const header = `/* AUTO-GENERATED by build-fonts-css.cjs — do not edit by hand.\n` + ` ${faces.length} faces / ${seenFamilies.size} families, ${Math.round(raw / 1024)}KB raw woff2 (latin subset, base64-inlined).\n` + ` Families: ${[...seenFamilies].sort().join(", ")}.\n` + ` These are the template fonts NOT in hyperframes' auto-resolved set; inlining\n` + ` them makes every render deterministic regardless of installed system fonts. */\n`; fs.writeFileSync(OUT, header + "\n" + faces.join("\n\n") + "\n"); const outKb = Math.round(fs.statSync(OUT).size / 1024); console.log(`[fonts] wrote ${OUT}`); console.log( `[fonts] ${faces.length} faces, ${seenFamilies.size} families, ${outKb}KB css (${Math.round(raw / 1024)}KB raw)`, ); console.log(`[fonts] families: ${[...seenFamilies].sort().join(", ")}`);