How to self-host google fonts: gdpr-compliant css generator
- Step 1Type your families into the Families field — The only text input is **Families** (placeholder `Inter, Roboto:wght@400;700`). Enter comma-separated family names, optionally with CSS2 axis specifiers: `Inter:wght@400;700`, `Roboto Flex:opsz,wght@8..144,100..1000`, `Playfair Display`. Spaces in names are fine — the tool converts them to `+` for the API. There is **no separate URL-paste box in the browser UI**; the Families field is where everything goes.
- Step 2Pick a font-display value — The **font-display** dropdown defaults to `swap` and also offers `auto`, `block`, `fallback`, and `optional`. Whatever you choose is appended to the API request as `&display=<value>`, so the returned `@font-face` blocks carry it. `swap` is the safe default for body text; see [the font-display strategy tool](/font-tools/font-display-strategy) to pick deliberately.
- Step 3Click Generate — The tool builds `https://fonts.googleapis.com/css2?family=...&display=swap` and fetches it from your browser. Because your browser sends its own modern User-Agent, Google returns WOFF2 with `unicode-range` subset blocks — not the larger TTF a generic client would get. The result panel shows two metrics: **WOFF2 files referenced** and **font-display**.
- Step 4Copy or download the CSS — Use **Copy to clipboard** or **Download CSS** (`google-fonts-self-hosted.css`). The file has three parts: a header comment with the exact source URL and instructions, the rewritten `@font-face` blocks with `url("./woff2/<hash>.woff2")` paths, and a trailing comment containing the `curl` script.
- Step 5Run the embedded curl script from your fonts directory — Copy the `curl` lines out of the trailing CSS comment into `fetch-fonts.sh`, `chmod +x fetch-fonts.sh`, and run it from the directory where the CSS will live. It does `mkdir -p woff2` then `curl -sSL` each unique WOFF2 into `./woff2/`, matching the rewritten paths exactly. Note the downloaded filenames are Google's opaque hashes (e.g. `UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2`), not human-readable names.
- Step 6Drop the CSS in and keep the licence — Import the CSS into your stylesheet (or link it) and commit both the CSS and `./woff2/`. Most Google Fonts ship under the SIL Open Font License, which permits redistribution as long as you retain the licence text — grab `OFL.txt` from the font's page on Google Fonts and commit it alongside. Your visitors now never contact Google.
What the two UI controls do
The entire browser interface for this tool. There is no file upload (it is generative) and no URL-paste box — the schema has a googleFontUrl field but it is only reachable via the API/runner, not the rendered options panel.
| Control | Type | Effect on the request |
|---|---|---|
| Families | Text input | Split on commas, each part URL-encoded (spaces → +) into a family= parameter. Axis specifiers like :wght@400;700 are percent-encoded (%3Awght%40400%3B700) — Google's API accepts the encoded form and returns 200. |
| font-display | Select (swap default) | Appended as &display=<value>. Values: swap, auto, block, fallback, optional. Applied to every @font-face in the response. |
| Generate button | Action | Fetches fonts.googleapis.com/css2 from your browser, rewrites gstatic URLs, builds the curl script, renders the result. |
Anatomy of the generated CSS file
The single .css output (downloaded as google-fonts-self-hosted.css) is built from three concatenated parts. Everything except the @font-face blocks lives inside CSS comments, so the file is valid CSS you can import as-is.
| Part | Content | Purpose |
|---|---|---|
| Header comment | The exact source fonts.googleapis.com/css2?... URL plus 3 numbered steps | Audit trail — re-run the same families later, or hand off to a teammate |
Rewritten @font-face blocks | Google's CSS verbatim, with every url(https://fonts.gstatic.com/...) replaced by url("./woff2/<file>.woff2") | The CSS you actually ship — unicode-range, font-weight ranges, and font-style all preserved from Google |
| Trailing comment: curl script | #!/usr/bin/env bash, set -e, mkdir -p woff2, one curl -sSL "<gstatic url>" -o woff2/<file> per unique WOFF2 | Downloads the binaries to ./woff2/ so the rewritten paths resolve |
The privacy boundary — who contacts Google, and when
Self-hosting fixes the visitor-facing leak. Be precise about what still touches Google so you can document it for legal sign-off.
| Event | Contacts Google? | Whose IP |
|---|---|---|
| You generate the CSS in this tool | Yes — one fetch to fonts.googleapis.com from your browser | Yours (the developer), once, at build time |
| You run the curl script | Yes — curl hits fonts.gstatic.com for each WOFF2 | Your build machine, once |
| A visitor loads your deployed page | No — CSS and WOFF2 come from your origin | Never reaches Google — this is the GDPR-relevant transfer |
Cookbook
Copy-paste family lists for the common cases, plus what the output looks like. If you want to script this whole flow in CI instead of clicking, see the build-pipeline guide; for migrating a live production site behind a flag, see the marketing-site migration walkthrough. To go fully Google-free at generate time too, you can also inline the fonts with font-to-base64.
Body font, two static weights
ExampleThe most common request: regular + bold for a content site. Type this into the Families field and leave font-display on swap.
Families: Inter:wght@400;700 font-display: swap Generated request: https://fonts.googleapis.com/css2?family=Inter%3Awght%40400%3B700&display=swap Result metrics: WOFF2 files referenced: 7 font-display: swap
Variable weight range — one file, every weight
ExampleRequest the full weight range and Google returns the variable WOFF2 with a font-weight range. Fewer files to host, every weight available.
Families: Inter:wght@100..900
font-display: swap
A representative rewritten @font-face block in the output:
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900; /* variable range, preserved */
font-display: swap;
src: url("./woff2/<hash>.woff2") format('woff2');
unicode-range: U+0000-00FF, U+0131, ...;
}Roman + italic, three weights each
ExampleBoth styles in one request. ital sorts before wght alphabetically, so it leads. The tool encodes the whole specifier; Google accepts the encoded delimiters.
Families: Inter:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700 font-display: swap Generated request (delimiters percent-encoded — still 200 OK): https://fonts.googleapis.com/css2?family=Inter%3Aital%2Cwght%400%2C400%3B...&display=swap
The embedded curl script
ExampleLift these lines out of the trailing CSS comment, save as fetch-fonts.sh, and run from the folder that holds the CSS. Filenames are Google's opaque hashes — that is expected.
#!/usr/bin/env bash # Run from your fonts/ directory to download every WOFF2. set -e mkdir -p woff2 curl -sSL "https://fonts.gstatic.com/s/inter/v.../UcC73Fwr....woff2" -o woff2/UcC73Fwr....woff2 curl -sSL "https://fonts.gstatic.com/s/inter/v.../UcC73Fwr....woff2" -o woff2/UcC73Fwr....woff2 # ... one line per unique WOFF2
Two families in one request
ExampleHeading + body pair. Comma-separate them in the Families field; the tool emits one family= parameter per name.
Families: Playfair Display:wght@700;900, Inter:wght@400;500;700 font-display: swap Generated request: https://fonts.googleapis.com/css2?family=Playfair+Display%3Awght%40700%3B900&family=Inter%3Awght%40400%3B500%3B700&display=swap
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.
Generating the CSS does fetch Google from your browser
By designThe privacy guarantee is for your visitors, not for the act of generating. When you click Generate, the tool runs fetch('https://fonts.googleapis.com/css2?...', { mode: 'cors' }) from your browser, so your IP reaches Google once at build time. There is no server-side proxy. The shipped output — CSS with relative paths plus locally-downloaded WOFF2 — is what removes Google from your visitors' page loads.
Pasting a full fonts.googleapis.com URL into the Families field
Invalid requestThe Families field is bound to the family list, not a URL. If you paste https://fonts.googleapis.com/css2?family=Inter there, the tool URL-encodes the whole string into a single family= value and the API rejects it. In the browser, type only the family names (with optional axis specifiers). The URL-paste path exists in the schema (googleFontUrl) but is not wired into the rendered options panel — it is reachable only via the API/runner.
Nonexistent family name
400 Bad RequestA typo'd or unknown family (family=NotARealFont123) makes Google return HTTP 400 with an HTML error body. The tool surfaces it as Google Fonts API returned 400 Bad Request. Check spelling and capitalisation against the family's page on fonts.google.com — names are case-sensitive on the API.
Asking for an axis the family does not expose
400 Bad RequestInter:wdth@100 fails because Inter has no width axis; Google returns 400 (Missing font family, which really means 'this combination of family + axes does not exist'). Same for requesting italic from a single-style family. The tool passes the error straight through. Check the family's Type Tester on Google Fonts for which axes it actually supports.
Downloaded WOFF2 files have opaque hash names
Expectedgstatic serves files like UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2, not Inter-Regular.woff2. The curl script and the rewritten src paths both use these exact hashes, so they line up — but if you want readable filenames, rename in both places, or use the build-pipeline approach where you control naming.
More @font-face blocks than WOFF2 files
ExpectedRequesting Inter:wght@400;700 produces ~14 @font-face blocks (7 script subsets × 2 weights) but the 'WOFF2 files referenced' metric is 7 — because for a variable-capable family both weights of a subset point to the same variable WOFF2. The tool dedupes URLs before writing the curl script, so you download 7 files, not 14.
All language subsets are downloaded, not just Latin
ExpectedGoogle's response includes @font-face blocks for every script the family supports (latin, latin-ext, cyrillic, greek, vietnamese, …), each gated by unicode-range. Browsers only fetch the subsets a page actually uses, so the extra blocks cost nothing at runtime — but the curl script downloads all of them. For an English-only site that wants a single trimmed file, generate, then run the font subsetter or latin-filter on the WOFF2.
An empty Families field
ErrorIf both the family list and the (API-only) URL are empty, the tool throws Enter a Google Font family name or paste a fonts.googleapis.com URL. before any network call. Type at least one family name.
Your CSP has a font-src directive
Blocks the fontSelf-hosting from your own origin means font-src 'self' is enough — but if you previously allowed Google you may have font-src https://fonts.gstatic.com, and the relative ./woff2/ paths resolve to your origin, so the directive must include 'self'. After migrating, tighten the CSP to drop fonts.gstatic.com and fonts.googleapis.com entirely.
Re-running later returns different filenames
Expected on font revisionGoogle occasionally publishes a new revision of a family, which changes the gstatic hash filenames. The CSS and curl script always match each other within one generation, but a regeneration months later may produce different hashes. That is exactly why you commit the WOFF2 and CSS together — you pin to the revision you tested.
Frequently asked questions
Does the generated CSS leak any data to Google?
No — the shipped output references only relative ./woff2/ paths, so your visitors never contact Google. The one caveat is the generation step itself: when you click Generate, your browser does a single fetch to fonts.googleapis.com to retrieve the CSS, and running the curl script hits fonts.gstatic.com once per file. Both are developer-side, one-time, at build. The GDPR-relevant transfer — visitor IPs to a US third party on every page load — is what self-hosting eliminates.
Where do I paste my Google Fonts URL?
In the browser, you don't — there is only a Families field and a font-display dropdown. Type the family names (with optional axis specifiers like Inter:wght@400;700) into Families. If you have an existing fonts.googleapis.com/css2 URL, read the family= values out of it and type those names in. Pasting the whole URL breaks the request because the field is encoded as a family list, not a URL.
Is redistributing the WOFF2 files legal?
For the vast majority of Google Fonts, yes — they're licensed under the SIL Open Font License (OFL), which explicitly permits redistribution and bundling, provided you keep the licence text and don't sell the fonts standalone. A handful are under Apache 2.0, also redistributable. Download OFL.txt (or the relevant licence) from the family's page on fonts.google.com and commit it next to your WOFF2. The CSS itself is plain text and freely distributable.
Will I stop getting font updates if I self-host?
Yes, and most teams treat that as the point. Once you commit the WOFF2, you're pinned to that revision — deploys are reproducible and a Google-side change can't silently alter your rendering. To refresh, re-run the tool with the same families and re-run the curl script; diff the new hashes against your committed ones to see whether anything actually changed.
Does this support variable fonts?
Yes, transparently. Request a weight range like Inter:wght@100..900 and Google's API returns the variable WOFF2 with font-weight: 100 900 on the @font-face. The tool rewrites the src URL but leaves the weight range and any other axis declarations untouched, so the self-hosted variable font behaves identically to the CDN version.
Why does the curl script download files with random-looking names?
Those are Google's content-addressed filenames on fonts.gstatic.com (e.g. UcC73Fwr....woff2). The tool keeps them as-is so the rewritten src paths and the curl -o targets always match. If you want Inter-Regular.woff2-style names you'll need to rename them in both the CSS and the download targets — or script the whole flow with the CI pipeline approach, which lets you name files yourself.
How many WOFF2 files will I end up hosting?
Depends on the families and weights. A single variable family is often just a handful of files (one per script subset). Inter:wght@400;700 yields 7 unique WOFF2 in our test even though it produces ~14 @font-face blocks, because the variable file is shared across weights. Multi-family, multi-script requests can produce dozens. The result panel shows the exact 'WOFF2 files referenced' count before you commit.
I only need Latin — can I drop the other subsets?
The tool downloads every subset Google offers for the family. At runtime that's harmless (browsers fetch only the unicode-range subsets a page uses), so you can ship all of them and visitors still only pull Latin. If you want a single smaller file, generate first, then run the font subsetter or latin-filter on the WOFF2 to strip non-Latin glyphs.
Does font-display change the font files?
No. font-display is a CSS property baked into the @font-face block, not the binary. The tool appends &display=<value> to the request so the returned CSS carries it, but the WOFF2 bytes are identical regardless. You can also override it later by editing the @font-face directly, or design a deliberate strategy with the font-display strategy tool.
What about preloading the self-hosted fonts?
Once the WOFF2 are on your origin, add <link rel="preload" as="font" type="font/woff2" crossorigin> for the critical above-the-fold weight so the browser starts fetching it before CSS parses. Build the tags with the preload tag builder — point it at your ./woff2/<file>.woff2 paths.
Is anything uploaded to JAD Apps?
No font bytes — this is a generative tool with no file upload, and the result panel shows a '0 bytes uploaded' badge. The only network call is the one your own browser makes to Google's CSS API to fetch the upstream stylesheet. JAD's servers never see the fonts or the generated CSS; for signed-in users a single anonymous run counter is recorded for dashboard stats.
Can I script this instead of clicking?
Yes. GET /api/v1/tools/google-fonts-css-generator returns the option schema; pair the @jadapps/runner and POST to http://127.0.0.1:9789/v1/tools/google-fonts-css-generator/run with { googleFontFamilies, fontDisplayValue } (the runner path can also accept the googleFontUrl field that the browser UI omits). The full CI recipe — config-driven, pinned hashes, cache between runs — is in the build-pipeline guide.
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.