I opened the network tab on a theme I had written, and watched it fetch a hundred and twenty font files from a domain I do not own, before a single word appeared on screen.
Worse: I was selling that theme with “local fonts, no CDN” in the description. I believed it when I wrote it. It was simply not true.
How it gets in
If you have ever added a font through the Site Editor, or started from a theme that shipped a font library, look at your theme.json:
"fontFamilies": [
{
"fontFamily": "Abel",
"slug": "abel",
"fontFace": [
{ "src": [ "https://fonts.wp.com/…/abel-regular.woff2" ] }
]
}
]
That src is a URL, not a path. WordPress emits an @font-face pointing at it, and every visitor’s browser goes and fetches it.
Mine had a hundred and twenty-one of those in one theme and a hundred and thirty-three in another — every weight of every family in a typography library I had bundled for buyers to choose from. And two of them were not optional extras: the default heading font of one theme and the default body font of another were both remote. Not an edge case a buyer might hit. The first paint of every site running those themes.
The reason it is easy to miss is that everything works. The fonts render. The site looks right. Nothing in your editor tells you the difference between a local face and a remote one, because from the editor’s point of view there is none.
Why it is worth caring about
A visitor’s IP address is personal data. Under GDPR, sending it to a third party requires a lawful basis, and a font that could have been served from the same domain is a hard one to justify. This is not theoretical — a German regional court awarded damages against a site owner over exactly this in 2022, and the case has been widely reported since. I am a theme author, not a lawyer, and you should treat that as context rather than advice. But the risk is not mine to take on a buyer’s behalf: they get the visitors, and they get the complaint.
It contradicts what a theme claims to be. If you sell on privacy or performance, one CDN URL in theme.json undoes the sentence.
It is a dependency you cannot see fail. A font host having a bad day is your buyer’s site rendering in Times New Roman, and no error anywhere.
Checking your own theme
Two greps and one look:
grep -rn "fonts.googleapis\|fonts.gstatic\|fonts.wp.com" your-theme/
grep -rn '"src"' your-theme/theme.json | grep http
Then open the site with the network tab filtered to Font and check every row’s domain. The greps catch what is declared; the network tab catches what is actually fetched, including anything a plugin adds.
After my fix, both greps return nothing across four themes, and I re-check it before every release — because it is exactly the sort of thing that creeps back in when you add a font through the editor without thinking.
Doing it properly
1. Download the actual files you already reference. Not “a similar font” — the exact families, from the very URLs your theme requests at runtime. Mine were all Google Fonts under the SIL Open Font License, which is what made this a chore rather than a licensing problem.
2. Put them in the theme and use relative paths. WordPress resolves file:./ against the theme directory:
"fontFace": [
{
"fontFamily": "Abel",
"fontWeight": "400",
"fontStyle": "normal",
"src": [ "file:./assets/fonts/abel/abel-regular.woff2" ]
}
]
3. Ship the licence. SIL OFL requires the licence text to travel with the fonts. One OFL.txt per family, in the family’s folder. It costs nothing and it is the difference between bundling fonts and appropriating them.
4. Prefer woff2 and delete the rest. I found eighteen redundant .ttf files sitting beside their woff2 equivalents. Every current browser takes woff2; the .ttf was dead weight in every download.
The size question, honestly
My themes now carry about thirty font families and a hundred and thirty files each — roughly 12 MB in the theme folder.
That sounds alarming and mostly is not, because @font-face is lazy. A declared face is downloaded only if something on the page actually uses it. A visitor to a site using two families downloads two families. The 12 MB is download size for the buyer, once, not page weight for their visitors.
It is still a real trade-off and worth being deliberate about. If you bundle a library for buyers to choose from, that is a defensible reason to carry it. If you have thirty families and use three, delete twenty-seven.
The part I got wrong first
My first attempt at this “solved” it by dropping eleven typography variations and substituting different fonts for the two remote defaults. That is not a fix, it is a redesign — and it silently changed how every existing customer’s site looked.
The correct move was the boring one: fetch the exact files, bundle them, change the paths, change nothing visible. A buyer should be able to update and see no difference at all. That is what makes it a fix rather than a new version.
En bref
- Vérifiez
theme.jsonfor"src"values that are URLs rather thanfile:./paths. - A remote font face sends every visitor’s IP to a third party before the page paints.
- It is easy to miss because nothing breaks — the fonts render fine.
- Download the exact families you already reference, bundle them with their licences, use relative paths.
- Keep
woff2, dropttfduplicates. - Re-run the grep before every release. It creeps back in through the editor.
