From bd0e052b1a1847718151a16117dacc6c42a2178e Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 4 May 2026 22:28:41 +0200 Subject: [PATCH] docs(fonts): cache rollout, stale-list note, meta.json --- docs/fonts.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/fonts.md b/docs/fonts.md index 430ecad8..2bfca412 100644 --- a/docs/fonts.md +++ b/docs/fonts.md @@ -34,7 +34,8 @@ storage/fonts/ └── / ├── 400.woff2 ├── 600.woff2 - └── 700.woff2 + ├── 700.woff2 + └── meta.json (optional) ``` Rules: @@ -43,6 +44,7 @@ Rules: - **File names** are `.woff2` where `` is an integer (100-900). Other names are ignored. The picker doesn't expose individual weights, but the runtime injects all available weights in the `@font-face` block so headings (semibold/bold) render correctly. - **Format** must be `.woff2`. Other formats are ignored. WOFF2 is universally supported and the smallest on the wire. - **No italics** in v1 (the picker doesn't expose them). Italic files in the folder are silently ignored. +- **`meta.json`** (optional) tells the picker which CSS generic family to fall back to while the font file is loading (and permanently if the file ever 404s). Shape: `{ "generic": "sans-serif" | "serif" | "cursive" | "monospace" }`. Defaults to `sans-serif` if absent. Add this for serif fonts (e.g. Playfair Display) and cursive/display fonts (e.g. Comic Neue, Lobster) so visitors don't briefly see Helvetica during the font fetch. ### 3. Where to download fonts @@ -66,11 +68,13 @@ Either: Refresh the admin customizer; the new family appears in the body and heading dropdowns. +> **Note:** The admin customizer caches the fonts list separately for 5 minutes (React Query staleTime). After the backend picks up a new family, hard-reload the customizer page (⌘+Shift+R / Ctrl+Shift+R) to see it immediately, or wait up to 5 minutes for the frontend cache to expire on its own. The two caches serve different purposes — the backend avoids disk hits per request; the frontend avoids network hits per re-render — so we keep them independent and document the worst case rather than try to synchronise them. + ## How it works - **Scanner**: `backend/src/services/fontsService.js` reads two locations and merges them: `backend/assets/fonts/` (bundled) + `STORAGE_PATH/fonts/` (user). User additions override bundled families of the same name. Cached for 30 s. -- **Listing endpoint**: `GET /api/public/fonts` returns `{ fonts: [{ family, weights }, ...] }`. -- **Static serving**: `GET /fonts//.woff2` returns the actual file. Path-traversal protected. `Cache-Control: max-age=7d, immutable`. +- **Listing endpoint**: `GET /api/public/fonts` returns `{ fonts: [{ family, weights, generic }, ...] }`. +- **Static serving**: `GET /fonts//.woff2` returns the actual file. Path-traversal protected. `Cache-Control: max-age=7d` — clients revalidate via `If-Modified-Since` after expiry, so replacing a file on disk eventually rolls out without admin action (see "Replacing an existing font" below). - **Lazy injection**: `frontend/src/contexts/ThemeContext.tsx` watches `theme.fontFamily` / `theme.headingFontFamily` and injects exactly one `@font-face` block per family the page actually uses, into a single `