黑色卡片上的白色 Astro 火箭標誌與字樣,火焰為粉紅色

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

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

本頁目錄

引言

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

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

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

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

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

ch 是真實存在的 CSS 單位,量的是目前使用的字型中數字「0」的寬度。網頁排版的慣例是把 Nch 當成大約 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 把 CJK 內文的上限訂在每行 40 個字,拉丁文則是 80 個字。這個部落格的日文和繁體中文文章,用的是一個從來沒有人為 CJK 特別設定過的 token,一直落在這整個範圍之下。

Latin 的 ch 與全形 CJK 字元寬度比較兩張卡片並排。左邊是半形數字 0,搭配一條標示約 0.5em 的長條,這就是 ch 這個單位的基準。右邊是全形 CJK 字元,搭配寬度兩倍、標示 1em 的長條,代表全形字元實際佔用的寬度。半形數字「0」0ch 是這個字元的寬度≈ 0.5emCJK 全形字元全形字元寬度為 1em,是兩倍寬同樣以 ch 為基準的欄寬,CJK 每行的字數卻只剩一半
拉丁文的「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

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 checkpnpm figures:fit,還有實測的每行字數檢查,全部通過。但這些檢查沒有一個去確認,頁面其他部分在新的、更大的內文字級底下是不是還合理。

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

內文超過 h3 固定大小後,調大 h3 修正「調整前」與「調整後」兩張卡片。調整前 h2 為 22.4px、h3 為 18.4px、內文為 19px,h3 比內文還小。調整後 h2 不變,h3 調大為 21px,內文仍為 19px,恢復 h3 大於內文、小於 h2 的正確順序。調整前h222.4pxh318.4px內文19px調整後h222.4pxh321px內文19px內文超過 h3 固定大小後,調大 h3 修正
h3 固定在 1.15rem,從來沒有跟著內文字級的 token 一起變動,內文變大之後就超過了 h3,階層因此反過來,後來把 h3 也調大才修正回來。

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

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

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

總結

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

參考連結

分享這篇文章