fix(fonts): self-host branding fonts so no Google Fonts <link> is emitted#321
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3750b89a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
d3750b8 to
39a1544
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39a1544a40
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
39a1544 to
e465a56
Compare
…tted The portal/widget rendered a fonts.googleapis.com stylesheet <link> for the selected branding font. On a Cloudflare zone with Cloudflare Fonts enabled, the edge rewrites that link into an inline @font-face block, so the served DOM stops matching the client render and hydration fails (React #418) on every portal page. Bundle every font referenced by the branding picker (FONT_OPTIONS) and the theme presets (presets.ts) via @fontsource and remove the Google Fonts plumbing: GOOGLE_FONT_MAP, getGoogleFontsUrl, the portal/widget/auth-shell links, the theme-preview copy, and ALL_FONTS_URL. Geist ships as the "Geist Sans" family, Lato as 400/700, and Nunito is preset-only. Legacy branding configs saved with the bare "Geist" value are normalised to "Geist Sans" at read time (normalizeFontSans, applied in generateThemeCSS and the admin picker) so they keep rendering, no data migration needed. Pin the web app's zod to 4.3.6: adding the font deps re-resolved the lockfile and hoisted zod 4.4.3 to the top, which is type-incompatible with the MCP SDK used in mcp/tools.ts. The exact pin keeps the app on 4.3.6; the TanStack build tooling keeps 4.4.3.
e465a56 to
a942daa
Compare
Problem
The portal, widget, and auth shells render a
fonts.googleapis.comstylesheet<link>for the selected branding font (getGoogleFontsUrl). When a deployment sits behind a CDN font optimizer that rewrites HTML at the edge (e.g. Cloudflare Fonts), that<link>is replaced with an inline<style>@font-face ...>block before the browser receives it. The served DOM no longer matches what the client bundle renders, so hydration fails with React #418 on every portal page and the tree is re-rendered on the client.It only reproduces where the optimizer is enabled, so two deployments on the same version can behave differently.
Fix
Self-host every selectable branding font and stop emitting any Google Fonts link, so there is nothing for an edge optimizer to rewrite.
FONT_OPTIONSfonts via@fontsource(weights 400-700; Lato ships 400/700) and@importthem inglobals.css.GOOGLE_FONT_MAP,getGoogleFontsUrl, the portal/widget/auth-shell<link>s, thetheme-previewcopy, andALL_FONTS_URL.Notes
@fontsourceas theGeist Sansfamily, so itsFONT_OPTIONSvalue is updated. A workspace that previously stored the bareGeistvalue falls back to the system sans until branding is re-saved (no data migration).routes/api/v1/docs.ts) still loads Google Fonts, but it is static non-hydrated HTML so an edge rewrite there cannot cause #418. Left as-is.Validation
bun run build: 307@font-face, 279 woff2 emitted, 0fonts.googleapis.comreferences in the client output.mcp/tools.tszod errors aside).