# 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.

- Source: https://oharu121.com/blog/astro-measure-font-size-devto-cjk-ch-unit/
- Published: 2026-08-31T01:52:46+09:00
- Tags: Astro, CSS, i18n

---
**Key takeaways**

- This blog's `68ch` reading column already matched Zenn's, and sat inside the standard **45–75-character** range for English. The real bug was somewhere else.
- `ch` measures the width of a Latin digit, roughly half a full-width CJK character, so the same `68ch` silently gave Japanese and Traditional Chinese articles only **about 34 characters per line**, half the nominal count, on a token nobody had set with CJK in mind.
- Live-measuring dev.to (Playwright against a running site, not a screenshot) set the new shared baseline: **`47rem` (752px) and `19px` body text**.
- That baseline needed no separate font-size for Japanese or Chinese at all: it already lands at **roughly 37.5 characters per line**, matching a widely cited real-world benchmark and inside WCAG 1.4.8's own cap for CJK text.
- Code review caught a regression the arithmetic never checked: the bigger body text had grown past a fixed heading size, so a heading rendered smaller than the paragraph beneath it.

## 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 `N`ch 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](https://www.w3.org/WAI/WCAG21/Understanding/visual-presentation.html) 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.

*Figure — LatinVsCjkGlyph: A Latin "0" measures about half an em; a full-width CJK character measures a full em. The same ch-based column gives one script roughly twice the characters per line of the other.*

## 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`.

```ts
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.

*Figure — TypeScaleRegression: h3 was fixed at 1.15rem and never moved with the body-text token, so raising body text past it inverted the hierarchy until h3 was raised too.*

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.**

*Image: The widened reading column and larger body text, rendered live on the article that carries this fix*

*The reading column and body text after both changes, measured live rather than eyeballed.*

## 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.

## References

- [WCAG 2.1: Understanding Success Criterion 1.4.8, Visual Presentation](https://www.w3.org/WAI/WCAG21/Understanding/visual-presentation.html)
- [What is the CSS ch Unit?](https://meyerweb.com/eric/thoughts/2018/06/28/what-is-the-css-ch-unit/)
- [WEBメディアの記事の読みやすい1行の文字数はどれ位か調査してみた](https://web-kiwami.com/the-number-of-characters-in-one-line.html)
