From 8774bf86042a38f61f093df00610dce939fb788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 6 Aug 2026 17:58:55 +0200 Subject: [PATCH] Self-host every font instead of loading them from Google (#23859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google Fonts logs the IP and user agent of everyone who loads a font from it. Any page of ours that links to `fonts.googleapis.com` hands our users (and every self-hoster's users) to a third party for nothing in return, since we can serve the same bytes ourselves. After this PR there is no reference to `fonts.googleapis.com`, `fonts.gstatic.com` or `next/font/google` left in the repo. ## What changed **twenty-front, PDF export.** `exportBlockNoteEditorToPdf` registered Inter by URL against `fonts.gstatic.com`, so exporting a note made the browser fetch three TTFs from Google. The registration turned out to be unnecessary altogether: `@blocknote/xl-pdf-exporter` already registers an `Inter` family for its PDF schema, shipped inlined in the package as a base64 TTF with the same 2849-codepoint coverage. Deleting our `Font.register` means no font request leaves the browser, with 41 fewer lines and nothing vendored. Only weights 400 and 700 were ever used, and 700 already resolved to blocknote's `Inter18pt-Bold` before this branch, so the custom 500/600 registrations were dead. The only rendering change is body text going from `Inter` to `Inter18pt`, the same typeface at its 18pt optical size. **twenty-sdk, OAuth callback page.** The local "you can close this tab" page linked to Google Fonts, which meant running `twenty auth` phoned Google from the developer's browser. Replaced with a system font stack; a transient callback page did not justify a webfont round trip in the first place. **twenty-ui, Storybook.** `preview-head.html` loaded Inter from Google. It now imports `@fontsource/inter` in `preview.tsx`, matching what twenty-front's Storybook already does. **twenty-website.** Host Grotesk, Aleo, Azeret Mono and VT323 came through `next/font/google`. Next self-hosts those at runtime, so this was not a visitor-facing leak, but the build still had to reach Google, which makes builds non-hermetic and fails in an air-gapped environment. The latin subsets are now vendored in `src/fonts/`, next to the Inter files that were already there, and loaded with `next/font/local`. All four are OFL 1.1; `src/fonts/README.md` records each file's upstream and license. Total added weight is ~78 KB, and these are the exact files Next was downloading at build time anyway. Host Grotesk and Azeret Mono ship as single variable files, so they are declared once over their full `wght` axis rather than as one face per weight. ## Also removed Both Storybooks pulled `iframeResizer.contentWindow.min.js` from `cdnjs.cloudflare.com`. Storybook has not needed it since v7 and nothing in either package references `iframeResizer` or `parentIFrame`, so it was a third-party script executing in the preview iframe for no reason. Argos does not screenshot through the manager iframe either: `@argos-ci/storybook` hooks Vitest browser mode and calls `server.commands.argosScreenshot`, so Playwright drives the page directly. ## Verification Not just typecheck. The interesting parts were tested end to end, which caught two bugs an earlier revision of this PR had introduced. **PDF export** — production Vite build, served over HTTP, real Chromium, exporting through the actual `exportBlockNoteEditorToPdf`, then extracting the PDF's text back out: ``` Latin heading Cyrillic: Привет мир Greek: Ελληνικά κείμενο Latin-ext: Zażółć gęślą jaźń, Český Vietnamese: Tiếng Việt PASS Latin / Cyrillic / Greek / Polish / Czech / Vietnamese ``` Embedded fonts are `Inter18pt-Regular` / `Inter18pt-Bold`, no Helvetica fallback, zero requests off-origin. **Website** — built it, audited the build output (12 `@font-face` rules, all `/_next/static/media/`, weights `300 800` / `100 900` / `300` / `400` / `400,500,600`, `display: swap` preserved), then loaded it in Chromium: 136 requests, zero to Google. The deployed preview was checked too: no Google references in the served HTML or across all 21 CSS chunks, every font file returns `200 font/woff2` and parses to the expected family, and the asset hashes match a local build byte for byte. **Two bugs this caught**, both in earlier commits on this branch, both now fixed: 1. Registering `@fontsource/inter`'s latin file dropped coverage from 2849 codepoints to 230, silently removing Cyrillic, Greek, Vietnamese and extended-Latin from every export. fontsource splits Inter into seven per-script files chosen by `unicode-range`, but `Font.register` binds one file per weight with no equivalent. 2. Any woff2 aborts the export outright with `RangeError: Offset is outside the bounds of the DataView`. fontkit parses woff2, but `@react-pdf`'s subsetter chokes on the transformed `glyf` table. Confirmed format was the only variable by running identical content through local TTF, WOFF and WOFF2 files. Both are moot now that the registration is gone, but they are why this is worth a careful look rather than a rubber stamp. ## Left alone, but worth knowing about More third-party calls exist. None are font-related and each is a separate decision: - `twenty-website` loads `dotlottie-player.wasm` from **unpkg.com** at runtime on the homepage, via `@lottiefiles/dotlottie-react`. This is a live third-party CDN request on every visit, the same class of problem as the fonts, and looks like a small config change to self-host. - The halftone studio loads the Draco decoder from `www.gstatic.com` and, in exported scenes, three.js from `unpkg.com`. - The partners marketplace fixtures hotlink logos from `cdn.simpleicons.org` and `upload.wikimedia.org`. - reCAPTCHA and the Front support chat are config-gated and off unless an admin configures them, which seems right. - `APP_REGISTRY_CDN_URL` defaults to `https://unpkg.com`. - `twenty-front/index.html` points its `og:image` at `raw.githubusercontent.com`. Only social crawlers fetch it, so this is cosmetic. --- .../twenty-front/.storybook/preview-head.html | 2 -- .../utils/exportBlockNoteEditorToPdf.ts | 33 +----------------- .../src/cli/utilities/auth/callback-server.ts | 5 +-- .../twenty-ui/.storybook/preview-head.html | 7 ---- packages/twenty-ui/.storybook/preview.tsx | 4 +++ packages/twenty-ui/package.json | 1 + .../src/app/[locale]/layout.tsx | 29 ++++++++------- packages/twenty-website/src/fonts/README.md | 17 +++++++++ .../src/fonts/aleo-latin-300.woff2 | Bin 0 -> 14900 bytes .../fonts/azeret-mono-latin-variable.woff2 | Bin 0 -> 26164 bytes .../fonts/host-grotesk-latin-variable.woff2 | Bin 0 -> 20296 bytes .../src/fonts/vt323-latin-400.woff2 | Bin 0 -> 17936 bytes yarn.lock | 1 + 13 files changed, 42 insertions(+), 57 deletions(-) create mode 100644 packages/twenty-website/src/fonts/README.md create mode 100644 packages/twenty-website/src/fonts/aleo-latin-300.woff2 create mode 100644 packages/twenty-website/src/fonts/azeret-mono-latin-variable.woff2 create mode 100644 packages/twenty-website/src/fonts/host-grotesk-latin-variable.woff2 create mode 100644 packages/twenty-website/src/fonts/vt323-latin-400.woff2 diff --git a/packages/twenty-front/.storybook/preview-head.html b/packages/twenty-front/.storybook/preview-head.html index ed219d82b5..cbd4c1b22a 100644 --- a/packages/twenty-front/.storybook/preview-head.html +++ b/packages/twenty-front/.storybook/preview-head.html @@ -1,5 +1,3 @@ - -