v0.8: self-hosted fonts replace the Google Fonts CDN link

index.html now loads /api/fonts/fonts.css, synthesized at request time
by goods-editor's mountEditorApi from the host's fonts/ directory
(populated via fetch-fonts in goods-editor-module — the host-local
fetch script shrinks to a pointer). Shipped synchronously in <head>
on purpose: the module's runtime injector races first paint and the
editor's text measurement gives wrong answers against fallback
metrics until real fonts load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
khalid@traclabs.com
2026-09-01 23:29:04 -05:00
parent 6599a5e68f
commit dfa1f90a16
2 changed files with 40 additions and 141 deletions

View File

@@ -6,9 +6,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#fdf2f4" /> <meta name="theme-color" content="#fdf2f4" />
<title>Pawfectly Yours — Customize Your Shirt</title> <title>Pawfectly Yours — Customize Your Shirt</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <!--
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> Fonts are served by the goods-editor module from the host's fonts/
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&family=Bebas+Neue&family=Caveat:wght@400;600;700&family=DM+Sans:wght@400;500;700&family=Fira+Sans:wght@400;500;600;700&family=Lato:wght@400;700&family=Lora:wght@400;500;600;700&family=Merriweather:wght@400;700&family=Montserrat:wght@400;600;700&family=Nunito:wght@400;500;600;700;800&family=Open+Sans:wght@400;600;700&family=Oswald:wght@400;500;700&family=Pacifico&family=Playfair+Display:wght@400;600;700&family=Poppins:wght@400;500;600;700&family=Raleway:wght@400;600;700&family=Roboto:wght@400;500;700&family=Roboto+Condensed:wght@400;700&family=Roboto+Slab:wght@400;700&family=Rubik:wght@400;500;600;700&family=Source+Sans+3:wght@400;600;700&family=Space+Mono:wght@400;700&family=Ubuntu:wght@400;500;700&family=Work+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"> directory (populated by `npm run fetch-fonts` in goods-editor-module).
mountEditorApi synthesizes the @font-face rules at request time
based on whatever .ttf/.otf/.woff2/.woff files are present, so
adding a font is a "drop file + restart" workflow with no code
change here. See goods-editor-module/src/server/fontRegistry.js
for the generator.
We deliberately ship this synchronously in <head> rather than
letting the module inject a <link> at mount time: the module's
runtime injector races first paint, and the editor's font-
dependent text measurement (`measureTextWidth`) gives wrong
answers against fallback metrics until the fonts load. The
host pays a tiny extra TTFB to make first paint correct.
<ApparelDesigner loadFonts={false}> in App.jsx / CustomizeRoute
tells the module not to also inject its own runtime tag.
-->
<link rel="stylesheet" href="/api/fonts/fonts.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,145 +1,28 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Downloads TTF font files for the Google Fonts used in the editor and saves * DEPRECATED — fonts now live with goods-editor-module.
* them under ./fonts/. server.js scans that directory at startup and calls
* registerFont() for each file.
* *
* Why we do this: * The host used to fetch TTFs into its own fonts/ directory and
* node-canvas does not pick up Google Fonts automatically. It only knows * register them at server boot. After the v0.8 module reorg, font
* about fonts that are either (a) registered explicitly via registerFont() * ownership moved into goods-editor-module:
* or (b) installed system-wide and discoverable via fontconfig. Without
* this script, the server falls back to whatever Cairo can find for
* "Roboto", "Bebas Neue", etc., which is usually nothing — exports get
* substituted to a generic sans-serif and look nothing like the preview.
* *
* Source: * • The TTF files live under goods-editor-module/fonts/.
* We pull TTFs from the Fontsource jsDelivr CDN. URLs look like * • Fetch them with `npm run fetch-fonts` inside goods-editor-module.
* https://cdn.jsdelivr.net/fontsource/fonts/<slug>@latest/latin-400-normal.ttf * • mountEditorApi (called from the host's server.js) registers them
* Slugs are the lowercase, hyphenated family name. The script asks for two * with node-canvas at boot and serves them to the browser via
* weights per family (regular + bold) and falls through gracefully if a * /api/fonts/fonts.css.
* weight isn't published. * • The host's index.html loads that stylesheet directly — no more
* Google Fonts CDN, no more host-side TTFs.
* *
* Usage: * This file is kept as a tombstone so anyone who looks for it
* npm run fetch-fonts * understands what happened. Safe to `rm scripts/fetch-fonts.mjs`
* * in the next housekeeping commit; nothing references it anymore
* Re-running is cheap: existing files are skipped unless --force is passed. * (package.json's scripts block was already trimmed during the
* module-split work in May 2026).
*/ */
import { mkdir, writeFile, access } from 'node:fs/promises'; console.error(
import { constants as fsConstants } from 'node:fs'; '[fetch-fonts] This script has moved to goods-editor-module.\n' +
import { join, dirname } from 'node:path'; ' Run: (cd ../goods-editor-module && npm run fetch-fonts)'
import { fileURLToPath } from 'node:url'; );
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const FONTS_DIR = join(ROOT, 'fonts');
const FORCE = process.argv.includes('--force');
// Families used by the editor (FONTS in src/constants/fonts.js + the two
// CSS-level defaults). Keep this list in sync with that file.
const FAMILIES = [
'Roboto',
'Open Sans',
'Lato',
'Montserrat',
'Oswald',
'Raleway',
'Poppins',
'Roboto Condensed',
'Source Sans 3',
'Roboto Slab',
'Merriweather',
'Ubuntu',
'Playfair Display',
'Nunito',
'Rubik',
'Work Sans',
'Lora',
'Fira Sans',
'Barlow',
'Bebas Neue',
'DM Sans',
'Space Mono',
// Display + handwritten faces used by the Pawfectly Yours theme. Pacifico is
// the brand logo face; Caveat is a friendly script fallback exposed in the
// text-tab font picker so users can match the brand voice in their designs.
'Pacifico',
'Caveat',
];
const WEIGHTS = [
{ weight: 400, suffix: 'Regular' },
{ weight: 700, suffix: 'Bold' },
];
const familyToSlug = (family) => family.toLowerCase().replace(/\s+/g, '-');
// Filename convention used by server.js's registration scanner:
// "Family_Name-Variant.ttf" → family = "Family Name", variant = "Variant".
// Underscore-vs-space is so the file system stays portable.
const familyToFileBase = (family) => family.replace(/\s+/g, '_');
async function fileExists(path) {
try {
await access(path, fsConstants.R_OK);
return true;
} catch {
return false;
}
}
async function fetchOne(family, weight, suffix) {
const slug = familyToSlug(family);
const url = `https://cdn.jsdelivr.net/fontsource/fonts/${slug}@latest/latin-${weight}-normal.ttf`;
const dest = join(FONTS_DIR, `${familyToFileBase(family)}-${suffix}.ttf`);
if (!FORCE && await fileExists(dest)) {
return { family, weight, status: 'skipped', dest };
}
const res = await fetch(url);
if (!res.ok) {
return { family, weight, status: 'missing', dest, code: res.status };
}
const buf = Buffer.from(await res.arrayBuffer());
await writeFile(dest, buf);
return { family, weight, status: 'fetched', dest, bytes: buf.length };
}
async function main() {
await mkdir(FONTS_DIR, { recursive: true });
let fetched = 0, skipped = 0, missing = 0;
for (const family of FAMILIES) {
for (const { weight, suffix } of WEIGHTS) {
try {
const r = await fetchOne(family, weight, suffix);
if (r.status === 'fetched') {
fetched++;
console.log(`✓ ${family} ${weight} → ${r.dest} (${(r.bytes / 1024).toFixed(0)} KB)`);
} else if (r.status === 'skipped') {
skipped++;
console.log(`· ${family} ${weight} (already present)`);
} else {
missing++;
console.log(`! ${family} ${weight} HTTP ${r.code} — variant not published`);
}
} catch (err) {
missing++;
console.log(`! ${family} ${weight} ${err.message}`);
}
}
}
console.log(`\nDone. Fetched ${fetched}, skipped ${skipped}, missing ${missing}.`);
if (missing > 0) {
console.log('Missing variants are not fatal — the server will fall back to other registered weights.');
}
}
main().catch((err) => {
console.error('Font fetch failed:', err);
process.exit(1); process.exit(1);
});