The Font Is Loaded and the Text Is Still Wrong: Three Questions That Find the Bug

QA filed it as one bug: "the font is broken".
It was three bugs. On the checkout button, two characters rendered as empty boxes. On the pricing page, the H1 ignored the font entirely and fell back to the system sans. On a colleague's laptop, everything looked perfect.
Three symptoms, three completely different causes — and no error message anywhere, because a missing glyph is not an exception. The browser quietly draws a box and moves on.
That silence is the whole problem. Here is the diagnosis path that actually finds the bug, one question at a time.
Why this is hard
Pain 1: "the font is broken" covers at least three failures.
The file may not contain the glyph. The CSS may not reference the file correctly. The browser may never have downloaded it. All three look identical on screen.
Pain 2: a font file is opaque.
It is a binary blob of tables. Your OS font preview shows a specimen — it does not tell you the glyph count, the Unicode coverage, the weight class, or whether the file even parses cleanly. You are debugging blind against a format you cannot read.
Pain 3: tofu is silent.
No console warning. No 404. No failed request. The browser falls back per character and says nothing. On a mixed-language page, one box in a button is easy to miss until a customer screenshots it.
Pain 4: the subset you built months ago is now wrong.
If you subsetted the font, new copy means new characters. The subset keeps working — for the old strings. The new string quietly renders as boxes. Nothing tells you the subset went stale.
Pain 5: hand-written @font-face fails quietly too.
A wrong format() string, a font-weight that does not match the file, a relative path that resolves one directory off — each one produces a font that simply never applies, with no error.
The three questions
Every webfont bug is one of these. Ask them in order.
Question 1 — what is actually inside this file?
Font Inspector parses the font locally and reports family name, format, file size, glyph count, technical metrics, and character coverage.
I ran it on a CJK system font, DengXian:
- Family: DengXian · Format: TTF · Size: 15.57 MB · Glyphs: 29,624
- Units per em 2048, ascender 1659, descender −475, weight class 400
- Character coverage: 29,460 mapped characters, 446 unmapped glyphs
Two things to take from that panel. First, 15.57 MB is what a full CJK font costs — that is the number that explains your LCP. Second, 29,460 mapped characters out of 29,624 glyphs tells you the file is healthy: nearly every outline is reachable through the character map. If those two numbers were far apart, the font would have glyphs nobody can type.
The weight class matters more than people expect. If the file says 400 and your CSS asks for 700, the browser will not use this file — it will synthesize a fake bold from it, or skip it entirely.
Question 2 — does it contain the characters I need?
This is the question that finds tofu. Font Unicode Checker takes a font plus a string and reports exactly which characters are missing.
I checked the same font against 首页 登录 注册 立即购买 𠮷:
- Characters: 12 · Supported: 11 · Missing: 1 · Coverage: 91.7%
- The missing one is
𠮷(U+20BB7), a rare character from CJK Extension B — outside the font's coverage.
Every row shows the character, its code point, its glyph ID, and its status. You do not have to guess which character failed; the tool names it.
There are two buttons that close the loop:
- Copy missing characters — take the exact list to the copywriter or the translator.
- Create font subset — jump straight to subsetting the font to the characters you actually use.
That last button is the connection people miss: the check that proves your subset is complete is the same check you should run after subsetting. Build the subset, run the checker on your full string list, ship only when missing is zero.
Question 3 — is my CSS correct?
Font CSS Generator reads the font, detects its family name and format, and writes the @font-face block for you.
Upload the file, and it fills in:
@font-face { font-family: "DengXian"; src: url("./Deng.ttf") format("truetype"); font-weight: 400; font-style: normal; font-display: swap; } The family name comes from the font's own name table, not from the filename — which is why Deng.ttf correctly becomes DengXian. The format() string matches the actual container: truetype for TTF, woff2 for WOFF2. Getting that string wrong is one of the classic silent failures.
Change font-weight and font-display in the dropdowns and the CSS updates with them. Copy it, drop it in, done.
The order matters
Run them in this sequence and you never chase the wrong bug:
- Inspector — does the file contain what you think it contains? (Size, glyph count, weight class.)
- Unicode Checker — do your actual strings survive it? (This is where tofu dies.)
- CSS Generator — is the declaration correct for this specific file?
If all three pass and the font still does not render, the problem is not the font — it is the network, the caching, or a unicode-range you forgot about.
Five failure modes worth memorising
1. Weight mismatch. File says 400, CSS says 700. The browser synthesises bold or skips the file. Always generate the @font-face from the file itself.
2. format() strings. format("truetype") on a WOFF2 file, or omitting format() entirely, produces a font that never loads in some browsers.
3. Stale subsets. New copy, new characters, boxes. Re-run the checker against your current string list whenever copy changes.
4. Rare characters. 𠮷, emoji, and CJK Extension B live outside most CJK fonts. If your data can contain them, test with real data, not with 你好世界.
5. unicode-range scoping. If you split a font into ranges, a character outside every range falls back silently — which looks exactly like a missing glyph but is a CSS bug.
The takeaway
A webfont bug is not one bug. It is a file question, a coverage question, and a CSS question — and the browser answers none of them for you.
Ask them in order, in the browser, without uploading anything: what is in the file, does it have my characters, is my CSS right. Three tabs, about ninety seconds, and the boxes become a fix instead of a mystery.

