
This blog's articles were too narrow and small on desktop — dev.to fixed it, and found a CJK bug
Astro article pages ran narrow and small at 68ch/17px. Widening them against dev.to's own numbers also fixed a CJK line-length bug the ch unit had hidden.
On this page
Introduction
I was reading one of my own articles next to someone else’s on Zenn, at the same window size, and the difference was obvious: their paragraphs used more of the screen, and mine sat in a narrow column with empty space on both sides. I asked the agent whether the site’s reading column was actually too narrow, or whether that was just unfamiliarity talking.
The agent’s first answer was that the column was fine. 68ch sits inside the 45–75-character range most typography guides give for Latin text, and it measured out close to Zenn’s own column. That answer was correct for English and quietly wrong for the two other languages this blog ships in, because the unit behind that column has nothing to do with the alphabet Japanese and Traditional Chinese use. Fixing it also surfaced a second bug on the way out: the fix itself, once shipped, made a heading render smaller than the body text sitting under it.
This article covers why the English check passed, why the same measure gave the CJK locales roughly half the characters per line it gave English, how a live measurement of dev.to’s own numbers set the new width and font size, and the heading-hierarchy bug a code review caught before it went out.
The reading column already matched Zenn, for English
This blog ships every article in English, Japanese, and Traditional Chinese from one MDX source per locale, and the reading column’s width lived in a single CSS custom property: --measure: 68ch.
ch is a real CSS unit: the rendered width of the digit “0” in whichever font is active. The common web-typography convention treats Nch as roughly N characters of prose, which is why max-width: 65ch shows up everywhere as an “optimal line length” recipe. At 68ch, this blog’s column was already inside the classic 45–75-character window, and close enough to Zenn’s own column that the two were hard to tell apart. Widening it on that basis alone would have been a cosmetic change chasing a screenshot, not a fix.
The same measure gave Japanese and Chinese about half the line
Applying 68ch uniformly to all three locales looked harmless, and it was the thing actually worth checking. ch is anchored to a Latin digit glyph, and a full-width CJK character is defined to occupy a full em, roughly twice that glyph’s width. Measuring the live site with Playwright, against a running dev server rather than a screenshot, confirmed it directly: --measure: 68ch resolved to a 583px column at the site’s 17px body text, for all three locales identically, since none of them had a font-size of their own yet.
A CJK reader saw the same 583px box, but each of their characters is about 17px wide instead of the roughly 8.5px a Latin “0” measures at that size. That is almost exactly half an em, which happens to match the CSS specification’s own fallback value for ch when a browser cannot measure the glyph directly. Dividing the box by the character width that actually applies to CJK text puts roughly 34 characters on a line, not 68. Real Japanese web typography puts the comfortable range at 37–47 characters, with 37 (Yahoo News) cited as a specific benchmark, and WCAG 1.4.8 caps CJK body text at 40 characters per line against 80 for Latin. This blog’s Japanese and Traditional Chinese articles had been rendering under that whole range, on a token nobody had ever set with CJK in mind.
dev.to’s own numbers, measured live
Fixing the measure for CJK alone would have meant giving Japanese and Chinese their own font-size while leaving English exactly as it was. I widened the scope of the fix instead. dev.to’s articles read noticeably bigger and wider than this blog’s, so I wanted the new baseline chosen from that comparison rather than picked by feel. Font-size itself became the lever that keeps each script’s line length in range inside one shared column width.
Chrome DevTools MCP was unavailable in the session doing this work, so the agent wrote a throwaway script using the playwright library already in this repo’s dependencies, the same one scripts/check-figure-fit.ts uses to measure real rendered text rather than trust a screenshot. It loaded the live dev.to article, waited on document.fonts.ready, and read the computed style of the actual paragraph element: 20px body text, in a 724px column. The same script measured this blog’s own dev server at the old settings: 17px in 583px.
const result = await page.evaluate(() => { const p = document.querySelector(".crayons-article__body p"); const cs = getComputedStyle(p); return { fontSize: cs.fontSize, width: p.getBoundingClientRect().width };});From those two real numbers, not from a screenshot, the new shared baseline became --font-size-5: 1.1875rem (19px), and --measure converted off ch entirely, onto a fixed rem value close to dev.to’s own column.
The first version of the fix undercounted CJK by two padding rems
The first value tried was --measure: 45rem (720px). Live-measuring the result gave 35.8 characters per line for Japanese and Traditional Chinese, not the roughly 38 the hand arithmetic upstream had predicted. That arithmetic had used the raw --measure value as the column width, not accounting for .page’s own 1.25rem padding on each side. The real content width is 720px minus 40px, not 720px itself. Re-measuring against the real DOM rather than trusting the calculation caught the gap immediately.
Correcting for that padding and re-solving landed on --measure: 47rem (752px): a content width of 712px, which at 19px puts 37.5 characters per line on Japanese and Traditional Chinese articles, matching the Yahoo News benchmark almost exactly and comfortably inside WCAG’s 40-character cap. Because a full-width CJK glyph is a full em by design, that division is close to exact, unlike the Latin case a section back, where ch only approximates a character rather than measuring one directly. Japanese and Traditional Chinese ended up needing no separate font-size of their own at all: the shared value, chosen for dev.to parity, already landed inside the CJK target.
| Token | Before | After |
|---|---|---|
--measure |
68ch |
47rem (752px) |
--font-size-5 (body) |
1.0625rem (17px) |
1.1875rem (19px) |
| Column width, all locales | 583px |
752px |
| CJK characters per line | ≈34 | ≈37.5 |
Code review caught what the arithmetic never checked
The token change validated cleanly. pnpm check, pnpm figures:fit, and the live character-per-line measurement all passed. What none of them checked was whether the rest of the page still made sense at the new, bigger body text.
A code-review pass run against the change, this blog’s own /release pipeline runs one on every pull request, found it: .prose h3 was still pinned to a fixed 1.15rem (18.4px), untouched by the token change, and the new body text at 19px had grown past it. A level-3 heading was now rendering smaller than the paragraph text underneath it, the exact opposite of a heading hierarchy. h2, at a fixed 1.4rem (22.4px), was unaffected and still comfortably the largest of the three.
The fix was a single line: .prose h3 moved to 1.3125rem (21px), back above the new body text and still comfortably below h2. Neither the type check, the build, nor the character-count measurement would have caught this on their own, because none of them compares two independently-set font sizes against each other. That comparison is exactly what a review pass is for.

Summary
The reading column matched Zenn already, for English. The bug was that the same measure, expressed in a unit anchored to a Latin digit, silently gave Japanese and Traditional Chinese roughly half the characters per line it gave English, a gap nobody had set on purpose and nobody had checked. Widening the shared baseline for dev.to’s sake, using live measurement instead of a screenshot comparison, fixed both: the new 47rem/19px pair puts Japanese and Traditional Chinese at roughly 37.5 characters per line without a separate override, inside WCAG 1.4.8’s cap and matching a real-world benchmark. The one thing none of that arithmetic caught was a heading rendering smaller than the body text beneath it, and that is what a review pass is for.

