Pond fonts: catalog build, admin UI, and application #66

Closed
opened 2026-07-04 14:52:34 +02:00 by fable-5 · 1 comment
Collaborator

Context

Per-pond font selection with self-hosted Google Fonts is a distinctive vision feature (ADR 0016) and a prerequisite for faithful PDF export.

Scope

Implement the font catalog build step (deploy/fonts/: pinned download of ~15 OFL/Apache families as WOFF2 subsets into the web image, license records, generated @font-face CSS), the pond settings 'Appearance' section (three slots: heading/body/mono with live preview, defaults Roboto 400 / Roboto 200 / Fira Code), application via CSS variables on the pond root, and the catalog attribution page.

Acceptance criteria

  • no request leaves the origin when rendering any pond (network assertion in e2e — the GDPR guarantee)
  • font choice applies to editor and read view immediately and persists
  • defaults match the vision; a pond without settings renders them
  • catalog page lists licenses; build fails if a family lacks license info

Technical notes

  • ADR 0016, security.md (CSP font-src 'self').

Dependencies

Depends on #21, #52.

Size: ~1.5 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Per-pond font selection with self-hosted Google Fonts is a distinctive vision feature (ADR 0016) and a prerequisite for faithful PDF export. ## Scope Implement the font catalog build step (`deploy/fonts/`: pinned download of ~15 OFL/Apache families as WOFF2 subsets into the web image, license records, generated @font-face CSS), the pond settings 'Appearance' section (three slots: heading/body/mono with live preview, defaults Roboto 400 / Roboto 200 / Fira Code), application via CSS variables on the pond root, and the catalog attribution page. ## Acceptance criteria - [ ] no request leaves the origin when rendering any pond (network assertion in e2e — the GDPR guarantee) - [ ] font choice applies to editor and read view immediately and persists - [ ] defaults match the vision; a pond without settings renders them - [ ] catalog page lists licenses; build fails if a family lacks license info ## Technical notes - ADR 0016, security.md (CSP `font-src 'self'`). ## Dependencies Depends on #21, #52. **Size**: ~1.5 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M6 — Import, export & attachments milestone 2026-07-04 14:52:34 +02:00
fable-5 added the
deployment
frontend
labels 2026-07-04 14:52:34 +02:00
Collaborator

Done in f500198. Pipeline fully green (all 7 contexts, incl. the new fonts e2e pack); deployed to Test + Int and verified live.

What shipped (ADR 0016)

  • Catalog: a curated 15-family OFL/Apache list in shared (family, weights, category, license, google-webfonts-helper id). deploy/fonts/build-fonts.mjs validates every entry has license info (fails the build otherwise), downloads the WOFF2 weights into apps/web/public/fonts/ (gitignored), and generates the @font-face stylesheet — run at image build time from the web Dockerfile (with retries), never from a visitor's browser.
  • Application: PondFontScope sets --font-heading/body/mono (+ weights) from pond.settings.fonts on the editor + read view; the app-wide CSS already reads those custom properties, so headings/body/code re-resolve to the pond's fonts. A pond with no settings arrives defaulted (Roboto 400 / Roboto 200 / Fira Code), so the vision defaults always render.
  • Admin UI: pond-settings 'Appearance' section — three slots (family + weight) with a live preview, Pond-Admin-gated (fonts added to updatePondInputSchema, merged in PondsService.update); a font catalog attribution page (/fonts) listing families + licenses. New font i18n namespace (de+en).
  • CSP: strict Content-Security-Policy in nginx.conf (default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; …).

Acceptance criteria

  • no request leaves the origin when rendering any pond — e2e network assertion (fonts pack) + verified live: the CSP header is font-src 'self' and catalog.css (53 @font-face) references only /fonts/…, zero third-party URLs.
  • font choice applies to editor and read view immediately and persists — e2e (Merriweather applies + survives reload) + live PATCH persistence check.
  • defaults match the vision; a pond without settings renders them — e2e (a fresh pond renders Roboto).
  • catalog page lists licenses; build fails if a family lacks license info — /fonts page + build-fonts.mjs validation + a shared catalog-integrity unit test.

Tests: shared fonts.test.ts (the catalog invariants the build enforces) + e2e fonts.spec.ts (3 cases). Quality bar green: typecheck, lint, i18n, api 213, web 50, shared 126.

Live verification: Test + Int serve the CSP header (font-src 'self'), /fonts/catalog.css (53 faces, 0 off-origin URLs), and real WOFF2 (roboto-400.woff2, valid wOF2). The Appearance API persists a font change (Int). CD smoke tests green — the CSP does not break the app. The web image build downloaded the catalog fonts from upstream successfully.

Next: #67 (PDF export via Gotenberg — a new sidecar, provisioned like pandoc; inlines the pond's fonts, ADR 0009/0016).

Done in `f500198`. Pipeline fully green (all 7 contexts, incl. the new fonts e2e pack); deployed to Test + Int and verified live. **What shipped (ADR 0016)** - **Catalog**: a curated 15-family OFL/Apache list in shared (family, weights, category, license, google-webfonts-helper id). `deploy/fonts/build-fonts.mjs` **validates every entry has license info (fails the build otherwise)**, downloads the WOFF2 weights into `apps/web/public/fonts/` (gitignored), and generates the `@font-face` stylesheet — run **at image build time** from the web Dockerfile (with retries), never from a visitor's browser. - **Application**: `PondFontScope` sets `--font-heading/body/mono` (+ weights) from `pond.settings.fonts` on the editor + read view; the app-wide CSS already reads those custom properties, so headings/body/code re-resolve to the pond's fonts. A pond with no settings arrives defaulted (Roboto 400 / Roboto 200 / Fira Code), so the vision defaults always render. - **Admin UI**: pond-settings 'Appearance' section — three slots (family + weight) with a live preview, Pond-Admin-gated (`fonts` added to `updatePondInputSchema`, merged in `PondsService.update`); a font catalog attribution page (`/fonts`) listing families + licenses. New `font` i18n namespace (de+en). - **CSP**: strict `Content-Security-Policy` in nginx.conf (`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; …`). **Acceptance criteria** - [x] no request leaves the origin when rendering any pond — e2e network assertion (fonts pack) + verified live: the CSP header is `font-src 'self'` and `catalog.css` (53 `@font-face`) references only `/fonts/…`, zero third-party URLs. - [x] font choice applies to editor and read view immediately and persists — e2e (Merriweather applies + survives reload) + live PATCH persistence check. - [x] defaults match the vision; a pond without settings renders them — e2e (a fresh pond renders Roboto). - [x] catalog page lists licenses; build fails if a family lacks license info — `/fonts` page + `build-fonts.mjs` validation + a shared catalog-integrity unit test. **Tests**: shared `fonts.test.ts` (the catalog invariants the build enforces) + e2e `fonts.spec.ts` (3 cases). Quality bar green: typecheck, lint, i18n, api 213, web 50, shared 126. **Live verification**: Test + Int serve the CSP header (`font-src 'self'`), `/fonts/catalog.css` (53 faces, 0 off-origin URLs), and real WOFF2 (`roboto-400.woff2`, valid `wOF2`). The Appearance API persists a font change (Int). CD smoke tests green — the CSP does not break the app. The web image build downloaded the catalog fonts from upstream successfully. Next: #67 (PDF export via Gotenberg — a new sidecar, provisioned like pandoc; inlines the pond's fonts, ADR 0009/0016).
Sign in to join this conversation.
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#66
No description provided.