berrypod/docs/plans/admin-font-loading.md

71 lines
4.0 KiB
Markdown
Raw Normal View History

# Admin font loading
Status: Open — needs discussion
## Problem
Custom fonts don't load on admin or setup pages. The setup page at `/setup` renders in the browser's default serif font on production.
### Root cause
The admin CSS (`admin.css`) has **no `@font-face` declarations** and **no `font-family` on body/root**. Font family CSS variables are defined in `theme-primitives.css` (e.g. `--p-font-inter: 'Inter', system-ui, sans-serif`) but the actual woff2 font files are only loaded via `@font-face` rules generated at runtime by `CSSGenerator` for shop pages (injected into `<style>` tags via `ThemeHook` / `LoadTheme` plug).
Admin and setup pages use the `root.html.heex` layout which loads `admin.css` only — no generated theme CSS, no `@font-face` declarations.
### Why it works locally
If you have Inter or other theme fonts installed on your system, the browser resolves the font-family names without needing `@font-face`. On production or other machines without the fonts installed, it falls through to browser defaults (usually serif).
### Secondary bug: cache miss path
Three places regenerate CSS on ETS cache miss **without** the `path_resolver` for digested font URLs. This means if the cache is cold (after invalidation, restart race, etc.), the cached CSS contains bare paths like `/fonts/inter-v20-latin-regular.woff2` which 404 in production (only digested paths are served).
Affected files:
- `lib/berrypod_web/plugs/load_theme.ex:30``CSSGenerator.generate(settings)` (no resolver)
- `lib/berrypod_web/theme_hook.ex:29` — same
- `lib/berrypod_web/controllers/error_html.ex:117` — same
`CSSCache.warm/0` does it correctly: `CSSGenerator.generate(settings, &BerrypodWeb.Endpoint.static_path/1)`
## Options
### Option A: Quick fix — system font stack for admin
Add `font-family: system-ui, -apple-system, sans-serif` to the admin reset CSS. Admin pages get a clean sans-serif font without loading any custom font files.
Pros: zero extra bytes, fast, no font loading flash
Cons: admin and shop have completely different typographic feel
### Option B: Load Inter for admin pages
Add static `@font-face` declarations for Inter (the default body font) to admin.css. Admin gets a consistent font with minimal overhead (~50KB for regular + medium weights).
Pros: consistent typography across admin/shop
Cons: extra font loading, static declarations need updating if font files change
### Option C: Shared font loading infrastructure
Rethink the split between admin and shop CSS. Currently they're two completely separate stacks:
- **Shop**: runtime-generated CSS with `@font-face` via `CSSGenerator`, theme tokens, semantic layer
- **Admin**: hand-written CSS with DaisyUI-derived colour themes, no font loading
Could unify font loading so both share the same `@font-face` declarations (either static in a shared CSS file, or via a shared plug/hook). The theme font primitives are already in `theme-primitives.css` which both stacks import.
This is a bigger piece of work and might be overengineering for the current state — the admin and shop serve very different purposes and their CSS stacks reflect that.
### Option D: Full admin CSS rework
Rebuild admin CSS to share more with the shop CSS architecture. Both would use the three-layer system (primitives, attributes, semantic) with the admin getting its own semantic layer but sharing primitives and font loading.
This is the most ambitious option and would need its own plan. Worth considering if the admin UI is going to grow significantly, but could be premature for MVP.
## Recommendation
Fix the cache miss bug regardless (Option A baseline). For fonts specifically, Option A or B are both reasonable quick wins. Option C/D are worth revisiting when the admin UI needs a bigger rework.
## Related
- Cache miss bug affects shop pages too (not just admin)
- `CSSCache.warm/0` runs on startup so the bug only manifests on race conditions or manual invalidation
- The `@font-face` declarations in generated CSS use bare paths without `path_resolver`, which 404s in production