How to migrating a marketing site off google fonts cdn
- Step 1Inventory every Google Fonts reference — Grep the whole codebase — not just the main layout — for `fonts.googleapis.com` and `fonts.gstatic.com`: `<link>` tags, `@import` rules, inline `<style>` blocks, CMS theme settings, email templates, and any `preconnect`/`preload` hints. Record each exact `css2` URL: the families, weights, italic variants, and `display=` value. Marketing sites accumulate these across landing-page templates and campaign microsites.
- Step 2Reproduce each reference in the generator — For each inventoried URL, read its `family=` values and type those names into the generator's **Families** field (e.g. `Inter:wght@400;700`), set **font-display** to match the original `display=`, and Generate. Download `google-fonts-self-hosted.css` and run the embedded curl script to fetch the WOFF2 into `./woff2/`. Consolidate duplicates — many templates request the same family.
- Step 3Commit the CSS and WOFF2, and a licence file — Put the rewritten CSS at a stable path (e.g. `/public/fonts/google-fonts.css`) and the WOFF2 under `/public/fonts/woff2/`. Add the upstream `OFL.txt` for each family for legal. Commit everything so the fonts are pinned and the deploy is reproducible.
- Step 4Visual-regression test before flipping anything — Snapshot key pages with Percy, Chromatic, or Playwright while still on the CDN, then again pointing at the self-hosted CSS in a preview deploy. Because the WOFF2 are identical, expect pixel-identical output. Any real diff is a missing weight, a wrong `unicode-range`, or an un-migrated template — fix before rollout. (Allow a small antialiasing tolerance; see edge cases.)
- Step 5Roll out behind a feature flag — Gate the choice of CDN vs self-hosted CSS behind a flag (GrowthBook, LaunchDarkly, or an env toggle). Deploy with it off (no behaviour change), then enable for ~1% of traffic, watch LCP and font-related console errors, and ramp to 100% over a day. If anything looks wrong, flip the flag back to the CDN in seconds — no redeploy.
- Step 6Remove the CDN and tighten the CSP — Once at 100% and stable, delete the Google `<link>`/`@import` and the `preconnect` hints, then set `Content-Security-Policy: font-src 'self'` (and drop `fonts.gstatic.com`/`fonts.googleapis.com` from `style-src`/`default-src`). The CSP now physically blocks Google, so a future regression can't silently re-introduce the leak — it'll fail loudly in the console instead.
Where Google Fonts references hide on a marketing site
The audit's job is to find every one of these, not just the obvious <link> in the main layout. Misses here are the #1 cause of a 'migration' that still leaks to Google.
| Location | What to look for | Risk if missed |
|---|---|---|
Main layout / <head> | <link href="https://fonts.googleapis.com/css2?..."> | Obvious — usually caught first |
| Landing-page / campaign templates | Per-template <link> or inline <style> with @import | Silent leak on specific pages; common on marketing sites |
| CMS theme settings | A 'Google Font' picker baked into the theme config | Re-injects the CDN link outside your code; hard to grep |
| preconnect / preload hints | <link rel="preconnect" href="https://fonts.gstatic.com"> | Wasted connection after migration; not a leak but cruft |
| Email templates | @import of a css2 URL in HTML email | Separate concern — most email clients strip @font-face anyway |
Migration rollout phases with rollback
Each phase is reversible. Don't remove the CDN or tighten the CSP until the flag has been at 100% and stable for long enough to trust the field data.
| Phase | Action | Rollback |
|---|---|---|
| 1. Generate + commit | Self-hosted CSS + WOFF2 in the repo, flag off | No behaviour change yet — nothing to roll back |
| 2. Canary | Flag on for ~1% of traffic; watch LCP + console | Flip flag off (seconds, no redeploy) |
| 3. Ramp | Increase to 100% over ~24h | Flip flag off; investigate before retrying |
| 4. Remove CDN | Delete Google <link>/preconnect; redeploy | Re-add the link (requires a redeploy) |
| 5. Lock CSP | font-src 'self'; Google blocked | Relax CSP (requires a redeploy) |
Reading a visual-regression diff
Self-hosted WOFF2 are identical bytes, so a genuine font diff means a config bug. Distinguish real breakage from harmless rendering noise.
| Diff appearance | Likely cause | Action |
|---|---|---|
| Whole block in a different typeface | Missing weight/style, or the self-hosted @font-face failed to load (fallback showing) | Real bug — check the weight is in your families and the WOFF2 path resolves |
| One script renders in fallback (e.g. accented chars) | Missing unicode-range subset for that script | Real bug — regenerate including the needed subset |
| Sub-pixel antialiasing shimmer, same glyphs | Renderer noise across runs/OSes | Harmless — set a small pixel tolerance in the diff tool |
| Layout shift / reflow on load | font-display mismatch vs the original | Match the original display= value in the generator |
Cookbook
The audit, generation, and rollout commands. For the underlying privacy rationale see the GDPR self-hosting guide; for the performance numbers to put in the case study see CDN vs self-hosted; to automate regeneration so the next campaign site doesn't re-add the CDN see the build-pipeline guide.
Audit the whole repo, not just the layout
ExampleFind every Google Fonts reference across templates, partials, and config. Marketing sites scatter these across campaign pages.
# Every reference, with file:line grep -rn -E 'fonts\.(googleapis|gstatic)\.com' \ --include='*.html' --include='*.tsx' --include='*.jsx' \ --include='*.css' --include='*.vue' --include='*.liquid' . # Don't forget preconnect / preload hints grep -rn 'preconnect.*gstatic' .
Reproduce an audited URL in the generator
ExampleRead the family= values out of the old URL and type the names into the Families field; match font-display to the old display= value.
Old reference found in audit: https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap In the generator: Families: Inter:wght@400;700 font-display: swap → google-fonts-self-hosted.css (+ curl script for the WOFF2)
Feature-flag the CSS source
ExampleToggle between CDN and self-hosted at render time so rollback is instant. Deploy with the flag off, then ramp.
// pseudo-template
{#if flags.selfHostedFonts}
<link rel="stylesheet" href="/fonts/google-fonts.css">
{:else}
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">
{/if}
// Ramp: 0% (deploy) -> 1% canary -> 100% over 24h -> remove the else branchSwap preconnect for a self-host preload
ExampleAfter migrating, the Google preconnect hints point at unused origins. Replace them with a preload of your critical font.
Remove after migration:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Add (build with the preload tag builder):
<link rel="preload" href="/fonts/woff2/<hash>.woff2"
as="font" type="font/woff2" crossorigin>Lock the CSP so a regression can't re-leak
ExampleOnce self-hosted, block Google at the CSP layer. A future template that re-adds the CDN link will fail loudly instead of silently leaking.
Content-Security-Policy: default-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline' # A stray fonts.googleapis.com <link> now triggers: # 'Refused to load the stylesheet ... violates Content Security Policy' # -> caught in the console / report-uri, not shipped silently
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
A campaign template still links the CDN after 'migration'
Leak persists — failMarketing sites scatter font links across landing-page and campaign templates, not just the main layout. If the audit misses one, that page still hands visitor IPs to Google and your GDPR claim is false. Grep every template type and CMS theme setting, and — decisively — lock the CSP to font-src 'self' so any missed reference fails loudly in the console instead of leaking silently.
CMS re-injects a Google Fonts link from theme settings
Re-leak outside your codeMany CMS themes have a 'Google Font' picker that injects the CDN <link> at render time, outside your repo where grep can't see it. Disable the theme's font option and load your self-hosted CSS instead. After that, the CSP lock is your safety net: it blocks the re-injected link even if a content editor re-enables the picker.
Visual diff shows a different typeface entirely
Real bug — failIf a whole text block renders in a different font after migration, either a weight/style is missing from your families or the self-hosted @font-face failed to load and the fallback is showing. Check that every weight the page uses is in the generator's family list and that the ./woff2/ paths resolve from the deployed CSS's location. This is the most common real regression.
Accented or non-Latin text falls back
Missing subset — failIf accented characters or a non-Latin script render in the fallback font, the needed unicode-range subset isn't being served. The generator downloads every subset Google offers, so this usually means a download or path error rather than a generation gap — verify the relevant subset WOFF2 is present in ./woff2/ and referenced in the CSS.
Antialiasing shimmer flagged as a regression
Harmless noiseSub-pixel antialiasing differs slightly across rendering runs and OSes even with identical fonts. A pixel-perfect diff tool will flag this as a 'change' though the glyphs are the same. Set a small pixel-difference tolerance in Percy/Chromatic/Playwright so this noise doesn't drown out the real diffs you care about.
Layout shift appears after migration
font-display mismatchIf the page now flashes or reflows on font load when it didn't before, you likely changed the font-display behaviour — e.g. the original used optional and you generated with swap. Match the generator's font-display to the original display= value, or deliberately choose a strategy with the font-display strategy tool and update fallback metrics accordingly.
Stale gstatic preconnect left in the head
Wasted connectionAfter migration the preconnect to fonts.gstatic.com opens a connection the page never uses, wasting part of the browser's early-connection budget. Remove both Google preconnect hints and, if anything, preload your own critical font instead. Not a leak, but cruft that mildly hurts the performance you migrated to gain.
Keeping the CDN as a 'fallback' for resilience
Defeats the migrationListing the Google CDN as a fallback src means a transient origin hiccup re-leaks visitor IPs to Google — and silently, since the page still looks fine. If your CDN is reliable enough to serve HTML it's reliable enough to serve fonts. Remove the fallback; trust your stack. The pinned, committed WOFF2 don't depend on Google at all.
Rolling out to 100% before field data is in
RiskyLab visual-regression tests catch rendering bugs but not real-world LCP across the device/network mix your marketing traffic actually uses. Ramp through a 1% canary and watch field CWV before going to 100%. The feature flag makes rollback instant if the canary's LCP regresses; removing the CDN too early forfeits that.
Forgetting the licence file
Compliance gapSelf-hosting redistributes the WOFF2, which the SIL Open Font License permits — but only if you retain the licence text. Shipping the fonts without OFL.txt is a licence violation even though it's technically the same file Google served. Commit the licence for each family alongside the WOFF2 as part of the migration.
Frequently asked questions
How do I make sure I've found every Google Fonts reference?
Grep the entire codebase — *.html, *.tsx/jsx, *.css, *.vue, *.liquid, partials, and email templates — for both fonts.googleapis.com and fonts.gstatic.com, plus preconnect hints. Then check CMS theme settings, which can inject the CDN link outside your repo. The definitive backstop is locking the CSP to font-src 'self' after migration: any reference you missed then fails loudly in the console instead of leaking silently.
Will the migration change how my fonts look?
It shouldn't — the self-hosted WOFF2 are byte-identical to Google's, so rendering is pixel-perfect. That's exactly why visual-regression testing is the right gate: any real diff means a config bug (a missing weight, a wrong path, an un-migrated template), not a font change. Run snapshots on the CDN version and the self-hosted preview and compare, allowing a small antialiasing tolerance.
How do I roll out without downtime?
Gate the CSS source behind a feature flag (GrowthBook, LaunchDarkly, or an env toggle). Deploy with it off — no behaviour change. Enable for ~1% of traffic, watch LCP and font console errors, then ramp to 100% over a day. If anything regresses, flip the flag back to the CDN in seconds without a redeploy. Only after it's stable at 100% do you remove the CDN link and tighten the CSP.
Could the migration hurt SEO?
More likely to help — self-hosting improves Core Web Vitals (especially LCP on cold loads) by removing Google's two origins from the critical path, and CWV is a page-experience ranking signal. The genuine risk is a silently-broken @font-face (font fails to load, text falls back), which visual-regression tests catch before deploy. See CDN vs self-hosted for the performance rationale.
Should I keep the Google CDN as a fallback?
No — it defeats the migration. A fallback src to Google means a transient origin issue silently re-leaks visitor IPs, and your GDPR documentation becomes false. If your CDN can serve HTML it can serve fonts. Remove the fallback and trust your stack; the committed WOFF2 have no Google dependency at all.
How do I generate the CSS for an existing css2 URL?
Read the family= values out of the old URL and type those family names (with their axis specifiers, e.g. Inter:wght@400;700) into the generator's Families field, set font-display to match the original display= value, and Generate. There's no URL-paste box in the browser UI — the Families field is family names only. Download the CSS and run the curl script to fetch the WOFF2.
What's a realistic timeline?
A small single-template marketing site is about 2 days end to end: audit, generate, visual-regression, flag rollout, CSP lock. A large multi-domain content site with many campaign templates is more like 1–2 sprints — the complexity scales with the number of distinct font references and templates, not the traffic volume. Automating regeneration with the build pipeline makes the next site faster.
How do I run the visual-regression tests?
Snapshot your key pages with Percy, Chromatic, or Playwright on the CDN version, then on a preview deploy pointing at the self-hosted CSS. Expect pixel-identical output since the fonts are the same files. Set a small pixel-difference tolerance so antialiasing shimmer doesn't create false positives, and treat any whole-block typeface change or fallback-font rendering as a real bug to fix before rollout.
What CSP should I set after migrating?
Tighten font-src to 'self' (drop fonts.gstatic.com) and remove fonts.googleapis.com from style-src/default-src. This physically blocks Google, so any stray CDN reference — a re-enabled CMS font picker, a new campaign template — fails loudly in the console (and your report-uri) instead of silently re-introducing the leak. The CSP is your durable guarantee that the migration stays migrated.
Do I need to handle preconnect / preload?
Yes. Remove the Google preconnect hints after migration — they open connections to origins you no longer use. For the weight that renders your LCP text, add a <link rel="preload" as="font" type="font/woff2" crossorigin> pointing at your self-hosted file so the fetch starts early on the warm connection. Build the tags with the preload tag builder.
What about the font licences?
Self-hosting redistributes the WOFF2, which is permitted for OFL-licensed fonts (most of Google Fonts) and Apache-licensed ones — provided you retain the licence text. Download OFL.txt (or the relevant licence) from each family's page on fonts.google.com and commit it alongside your ./woff2/ directory as part of the migration. Skipping it is a licence violation even though the bytes are unchanged.
Can I reuse this migration across multiple sites?
Yes — the generate → visual-test → flag → CSP-lock sequence is identical for every site. For a portfolio, automate the generation step with the build-pipeline guide so each site pulls pinned, hashed fonts from a config, and standardise the CSP lock so no future campaign site re-adds the CDN. The first migration is the slow one; the rest are templated.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.