# 這個部落格的文章在桌面版讀起來太窄太小 — 照著 dev.to 調整後,順便抓出一個 CJK 行長 bug

> Astro 文章頁面原本在 68ch/17px 下顯得又窄又小。依照 dev.to 的實測數字放大後,也順便修好了 ch 單位一直藏著的 CJK 行長 bug。

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

---
**重點摘要**

- 這個部落格的 `68ch` 內文欄本來就跟 Zenn 差不多,也落在英文標準的 **45–75 字元** 範圍內。真正的 bug 出在別的地方。
- `ch` 量的是拉丁數字的寬度,大約只有全形 CJK 字元的一半寬。所以同一個 `68ch`,日文和繁體中文文章實際上**每行只有大約 34 個字**,是名目字數的一半,而這個 token 從來沒有人特地為 CJK 設定過。
- 實際即時量測 dev.to(用 Playwright,不是看截圖)之後,訂出了新的共用基準:**`47rem`(752px)搭配 `19px` 的內文字級**。
- 這個基準不需要另外幫日文或中文設定字級:**每行大約落在 37.5 個字**,剛好對上一個常被引用的真實案例基準,也在 WCAG 1.4.8 對 CJK 訂出的上限之內。
- 程式碼審查抓出了計算沒發現的問題:內文變大之後超過了標題固定的大小,導致標題顯示得比內文還小。

## 引言

我把自己的一篇文章拿來跟 Zenn 上另一篇文章,用相同的視窗寬度並排閱讀,差異一眼就看得出來:對方的段落佔掉螢幕更多空間,我的文章卻窩在一條窄欄裡,兩側留著大片空白。我請智能體老實說,這個網站的內文欄到底是真的太窄,還是只是我不習慣而已。

智能體一開始的答案是「欄寬沒問題」。`68ch` 落在排版指南常給拉丁文字的 45–75 個字元範圍內,量出來也跟 Zenn 自己的欄寬相差無幾。這個答案對英文來說是對的,對這個部落格另外兩種語言卻悄悄地錯了,因為決定欄寬的這個單位,跟日文、繁體中文用的文字系統根本無關。修好這個問題的過程中,又冒出了第二個 bug:修正本身讓標題顯示得比底下的內文還小。

這篇文章會依序談:英文的檢查為什麼會通過、同一個 measure 為什麼讓 CJK 每行的字數大概只剩英文的一半、怎麼用 dev.to 的實測數字訂出新的欄寬與字級,以及上線前程式碼審查抓到的標題階層 bug。

## 內文欄本來就跟 Zenn 差不多了,至少英文是這樣

這個部落格用一份 MDX 原始檔搭配三種語言各自的檔案,發布英文、日文和繁體中文。內文欄的寬度,原本就只靠一個 CSS 自訂屬性決定:`--measure: 68ch`。

`ch` 是真實存在的 CSS 單位,量的是目前使用的字型中數字「0」的寬度。網頁排版的慣例是把 `N`ch 當成大約 `N` 個字元,這也是為什麼 `max-width: 65ch` 到處都被當成「最適合閱讀行長」的公式。在 `68ch` 這個值下,這個部落格的欄寬已經落在經典的 **45–75 字元範圍**內,跟 Zenn 自己的欄寬相比也幾乎分不出差別。如果只憑這一點就把欄寬加大,那只是在追著一張截圖跑的表面調整,稱不上是修正。

## 同一個 measure,卻讓日文和中文的每行字數少了將近一半

把 `68ch` 一視同仁地套用到三種語言上,乍看沒什麼問題,但這正是真正該檢查的地方。`ch` 是以拉丁數字的字形為基準,而全形的 CJK 字元依照字型設計就是佔滿一個 em,寬度大約是前者的兩倍。實際用 Playwright 對正在執行的開發伺服器量測(而不是看截圖),直接證實了這一點:`--measure: 68ch` 在這個網站 `17px` 的內文字級下,解析成 `583px` 的欄寬,而且三種語言完全一樣,因為當時沒有任何一個語言有自己專屬的字級。

CJK 讀者看到的是同一個 `583px` 的方框,但裡面每一個字大約有 `17px` 寬,跟同樣字級下拉丁文「0」量出來的大約 `8.5px` 差了一大截。這幾乎正好是半個 em,也剛好對上 CSS 規範自己訂的預設值,也就是瀏覽器量不到字形寬度時的備援數字。用實際套用在 CJK 文字上的字元寬度去除這個方框,一行能放的是**大約 34 個字**,不是 68 個。實際的日文網頁排版把舒適的範圍訂在**37 到 47 個字**之間,其中 37 個字(Yahoo!新聞)常被當成具體的參考基準,而 [WCAG 1.4.8](https://www.w3.org/WAI/WCAG21/Understanding/visual-presentation.html) 把 CJK 內文的上限訂在每行 40 個字,拉丁文則是 80 個字。這個部落格的日文和繁體中文文章,用的是一個從來沒有人為 CJK 特別設定過的 token,一直落在這整個範圍之下。

*Figure — LatinVsCjkGlyph: 拉丁文的「0」寬度大約是半個 em;全形的 CJK 字元則佔滿一個 em,寬度是前者的兩倍。同樣以 ch 為基準的欄寬,兩種文字系統每行能放的字數卻差了將近一倍。*

## dev.to 自己的數字,即時量出來的

如果只修 CJK 的部分,做法會是另外給日文和中文各自的字級,英文維持原樣。我選擇把修正的範圍放大:dev.to 的文章讀起來明顯更大、欄也更寬,我希望新的基準是從這個比較量出來的,而不是憑感覺挑一個數字。字級本身,就成了在同一個共用欄寬裡,把每種文字系統的行長控制在合理範圍的那根槓桿。

處理這件事的那個 session 裡,Chrome DevTools MCP 用不了,所以智能體寫了一支用完即丟的小腳本,用的是這個 repo 本來就有的 `playwright` 函式庫,跟 `scripts/check-figure-fit.ts` 量測實際渲染文字(而不是相信一張截圖)用的是同一套。這支腳本載入 dev.to 上線的文章,等 `document.fonts.ready` 完成,再讀出實際段落元素的 computed style:**內文字級 `20px`,欄寬 `724px`**。同一支腳本量這個部落格自己開發伺服器上的舊設定,結果是 `17px` 搭配 `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 };
});
```

根據這兩組實測數字,而不是憑截圖猜測,新的共用基準訂為 `--font-size-5: 1.1875rem`(`19px`),`--measure` 也徹底脫離 `ch`,改成一個接近 dev.to 欄寬的固定 `rem` 數值。

## 第一版修正漏算了兩個 padding 用的 rem,CJK 的字數算少了

第一次試的數值是 `--measure: 45rem`(`720px`)。實際量測的結果是日文和繁體中文**每行 35.8 個字**,比手算預期的大約 38 個字要少。那次手算直接把 `--measure` 的原始數值當成欄寬,但實際的段落是在 `.page` 自己 `1.25rem` 的內距裡面。實際的內容寬度是 `720px` 減掉 `40px`,不是 `720px` 本身。直接量測真正的 DOM,而不是相信計算,馬上就抓出了這個落差。

把這個內距補算進去、重新求解之後,得到 `--measure: 47rem`(`752px`):內容寬度變成 `712px`,在 `19px` 的字級下,日文和繁體中文文章**每行落在 37.5 個字**,幾乎正好對上 Yahoo!新聞的基準,也在 WCAG 40 字上限之內留有餘裕。因為全形的 CJK 字形依照設計就是整整一個 em,這個除法幾乎是精確的,跟前一節拉丁文的情況不一樣,`ch` 在那邊只是個大略的估計值,不是直接量出來的字元寬度。日文和繁體中文最後完全不需要各自專屬的字級:為了對齊 dev.to 而選的這個共用數值,本身就已經落在 CJK 的目標範圍裡。

| Token | 調整前 | 調整後 |
| --- | --- | --- |
| `--measure` | `68ch` | `47rem`(`752px`) |
| `--font-size-5`(內文) | `1.0625rem`(`17px`) | `1.1875rem`(`19px`) |
| 欄寬(三種語言共用) | `583px` | `752px` |
| CJK 每行字數 | 約 34 字 | 約 37.5 字 |

## 程式碼審查抓到了計算沒發現的問題

這次 token 的變更本身驗證得很乾淨。`pnpm check`、`pnpm figures:fit`,還有實測的每行字數檢查,全部通過。但這些檢查沒有一個去確認,頁面其他部分在新的、更大的內文字級底下是不是還合理。

針對這次變更跑的程式碼審查(這個部落格自己的 `/release` pipeline,每個 pull request 都會跑一次)抓到了問題:`.prose h3` 還固定在 `1.15rem`(`18.4px`),完全沒有隨著 token 一起變動,而新的內文字級 `19px` 已經超過了它。**第三級標題這時候顯示得比底下的內文段落還小**,剛好是標題階層該有的順序反過來。`h2` 固定在 `1.4rem`(`22.4px`),沒有受到影響,三者之中仍然是最大的。

*Figure — TypeScaleRegression: h3 固定在 1.15rem,從來沒有跟著內文字級的 token 一起變動,內文變大之後就超過了 h3,階層因此反過來,後來把 h3 也調大才修正回來。*

修正只有一行:把 `.prose h3` 改成 `1.3125rem`(`21px`),重新回到比新的內文大、又比 `h2` 小的位置。型別檢查、build,還有每行字數的實測,單靠自己都抓不到這個問題,因為它們都沒有把兩個各自獨立設定的字級拿來互相比較。**這種比較,正是程式碼審查這道關卡存在的理由。**

*Image: 這個部落格加寬後的內文欄與變大的內文字級,實際在帶有這次修正的文章上渲染出來的樣子*

*兩項變更都套用之後的欄寬與內文字級,用實測而不是用眼睛看出來的結果。*

## 總結

內文欄本來就跟 Zenn 差不多了,至少英文沒問題。真正的 bug 是同一個 measure,用一個以拉丁數字為基準的單位表示,卻讓日文和繁體中文每行拿到的字數大約只剩英文的一半,這個差距沒有人特地決定過,也沒有人檢查過。為了對齊 dev.to 而放大共用基準,用實測取代截圖比對,兩個問題一次都修好了:新的 `47rem` / `19px` 組合讓日文和繁體中文每行落在大約 37.5 個字,不需要另外設定,也落在 WCAG 1.4.8 的上限之內,對上了一個真實案例的基準。**那些計算完全沒抓到的一件事,是標題顯示得比底下的內文還小**,而這正是程式碼審查這道關卡的用處。

## 參考連結

- [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)
