黒いカードにピンクの炎を伴う白のAstroロケットロゴとロゴタイプ

このブログの記事はデスクトップで本文が狭く小さすぎた — dev.to に合わせて直したら、CJKの行長バグも見つかった

Astro の記事ページは 68ch/17px で狭く小さかった。dev.to の実測値に合わせて拡大したら、ch 単位が隠していた CJK の行長バグも直った。

目次

はじめに

自分の記事をZennの記事と同じウィンドウ幅で並べて読んでいたとき、違いは一目瞭然でした。相手の段落は画面をより広く使っているのに、自分の記事は左右に余白を残したまま狭いカラムに収まっていたのです。私はエージェントに、このブログの本文カラムは本当に狭いのか、それとも見慣れていないだけなのか、率直な意見を尋ねました。

エージェントの最初の答えは「カラムは問題ない」というものでした。68ch はラテン文字の記事に typography のガイドがよく挙げる45〜75文字の範囲に収まっており、Zenn自身のカラムとも近い値だったからです。その答えは英語に関しては正しく、このブログが公開しているもう二つの言語には静かに間違っていました。カラムの幅を決めている単位が、日本語や繁体字中国語が使う文字体系とは何の関係もないものだったからです。この不具合を直したことで、もう一つ別の不具合も表面化しました。修正そのものが、見出しを本文より小さく表示させてしまっていたのです。

この記事では、英語のチェックがなぜ通ったのか、同じ measure が CJK では英語のおよそ半分の文字数しか許していなかった理由、dev.to の実測値をもとに新しい幅とフォントサイズをどう決めたか、そして公開前にコードレビューが見つけた見出しの階層バグについて順に説明します。

本文カラムはすでに Zenn と同じくらいだった、英語については

このブログは英語・日本語・繁体字中国語の3言語で、1つのMDXソースから記事を公開しています。本文カラムの幅は --measure: 68ch という1つのCSSカスタムプロパティで決まっていました。

ch は実在するCSS単位で、いま使われているフォントで数字の「0」がどれだけの幅になるかを表します。ウェブタイポグラフィの慣習では Nch はおおよそ N 文字分と見なされ、だからこそ max-width: 65ch が「読みやすい行長」のレシピとしてあちこちに登場します。68ch の時点で、このブログのカラムはすでに 45〜75文字の標準的な範囲 に収まっており、Zenn自身のカラムと見比べても違いが分からないほど近い値でした。この事実だけを根拠に幅を広げていたら、それはスクリーンショットを追いかけただけの見た目の変更であり、修正ではなかったはずです。

同じ measure でも、日本語と中国語は行の文字数がほぼ半分だった

68ch を3言語すべてに一律で適用するのは一見無害に見えましたが、実際に確かめる価値があったのはまさにそこでした。ch はラテン文字の数字グリフを基準にした単位であり、全角のCJK文字はフォントの設計上、1em、つまりその2倍の幅を占めるよう定義されています。実際のサイトを(スクリーンショットではなく、稼働中の開発サーバーに対して)Playwrightで計測すると、それがそのまま裏付けられました。--measure: 68ch は、このブログの 17px の本文サイズにおいて 583px のカラムに解決されており、しかも3言語すべてでまったく同じ値でした。当時はどの言語もまだ独自のフォントサイズを持っていなかったからです。

CJKの読者が見ていたのは同じ 583px の箱でしたが、そこに入る一文字はおよそ 17px の幅があり、同じサイズでラテン文字の「0」が測る 8.5px ほどとは大きく違います。これはほぼちょうど半emで、ブラウザがグリフの幅を直接測れないときにCSS仕様自身が定めるフォールバック値と偶然一致します。実際にCJKに当てはまる文字幅で箱を割ると、1行に入るのは68文字ではなく 34文字程度 になります。実際の日本語のウェブタイポグラフィでは、快適な範囲はおよそ 37〜47文字 とされ、37文字(Yahoo!ニュース)が具体的な目安としてよく引用されます。またWCAG 1.4.8は、CJKの本文を1行40文字以内、ラテン文字は80文字以内と定めています。このブログの日本語と繁体字中国語の記事は、CJKを意識して設定されたことのないトークンの上で、その範囲全体を下回ったまま表示されていたのです。

Latin の ch と全角CJK文字の幅比較2枚のカードが横に並ぶ図。左は半角数字の0で、その幅がおよそ0.5emであることを示すバーがつく。これが ch という単位の基準。右は全角のCJK文字で、幅は2倍の1emを示すバーがつく。全角文字が実際に占める幅を表す。半角数字「0」0ch はこの文字の幅≈ 0.5emCJK文字(全角)全角文字は1em で2倍の幅同じ ch 基準の幅でも、CJKでは1行の文字数が半分になる
ラテン文字の「0」の幅はおよそ半emです。全角のCJK文字はその2倍、1emを占めます。同じ ch 基準のカラムでも、片方の文字体系はもう片方のおよそ2倍の文字数を1行に収められることになります。

dev.to 自身の数値を、実測して使う

CJKだけを直すのであれば、日本語と中国語にだけ専用のフォントサイズを与え、英語はそのままにするという方法もありました。私はそれよりも修正の範囲を広げることを選びました。dev.toの記事はこのブログよりも明らかに大きく、幅も広く読めたので、新しい基準値は勘ではなく、その比較から実際に選びたいと考えたのです。フォントサイズそのものを、1つの共通のカラム幅の中で各言語の行の長さを適正な範囲に収めるためのレバーとして使うことにしました。

このセッションの作業中、Chrome DevTools MCPは使えない状態でした。そこでエージェントは、このリポジトリにすでに依存関係として入っている playwright ライブラリを使い、使い捨てのスクリプトを書きました。scripts/check-figure-fit.ts がスクリーンショットではなく実際にレンダリングされたテキストを測るのに使っているのと同じライブラリです。このスクリプトはdev.toの実際の記事を読み込み、document.fonts.ready を待ってから、実際の段落要素のコンピューテッドスタイルを読み取りました。本文は 20px、カラムは 724px でした。同じスクリプトで、このブログ自身の開発サーバーを旧設定のまま測ると 17px583px でした。

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 };
});

この2つの実測値をもとに(スクリーンショットではなく)、新しい共通の基準値は --font-size-5: 1.1875rem(19px)になり、--measurech を完全にやめて、dev.to自身のカラムに近い固定の rem 値に変わりました。

最初のバージョンは、パディング分2remを見落としていてCJKを少なく見積もっていた

最初に試した値は --measure: 45rem(720px)でした。実際に計測すると 1行35.8文字 となり、手計算で見込んでいたおよそ38文字より少ない結果でした。その手計算は --measure の生の値をそのままカラム幅として扱っていましたが、実際の段落は .page 自身の 1.25rem のパディングの内側にあります。実際のコンテンツ幅は 720px から 40px を引いた値であり、720px そのものではありません。実際のDOMを測り直したことで、この見落としはすぐに見つかりました。

このパディング分を補正して計算し直した結果、--measure: 47rem(752px)に落ち着きました。コンテンツ幅は 712px となり、19px のフォントサイズでは日本語と繁体字中国語の記事が 1行37.5文字 に収まります。これはYahoo!ニュースのベンチマークにほぼ一致し、WCAGの40文字上限にも余裕を持って収まっています。全角のCJKグリフはフォント設計上ちょうど1emなので、この割り算はほぼ正確です。一つ前のセクションのラテン文字の場合と違い、ch は文字幅を直接測るのではなく、あくまで近似にすぎません。日本語と繁体字中国語には、結局、専用のフォントサイズはまったく必要ありませんでした。dev.toとの整合のために選んだ共通の値が、そのままCJKの目標値の中に収まっていたのです。

トークン 変更前 変更後
--measure 68ch 47rem(752px)
--font-size-5(本文) 1.0625rem(17px) 1.1875rem(19px)
カラム幅(全言語共通) 583px 752px
CJKの1行の文字数 約34文字 約37.5文字

コードレビューが、計算では気づけなかった問題を見つけた

トークンの変更自体は問題なく通りました。pnpm checkpnpm figures:fit、そして実測による1行の文字数チェック、すべて通過しました。しかしこれらのどれも、ページの他の部分が新しい大きな本文サイズのもとで見た目として成立しているかまではチェックしていませんでした。

この変更に対して実行されたコードレビュー(このブログ自身の /release パイプラインが、すべてのプルリクエストに対して実行しているものです)がそれを見つけました。.prose h31.15rem(18.4px)という固定値のままで、トークンの変更の影響を受けていなかったのです。そして新しい本文サイズの 19px は、その値を追い越してしまっていました。見出しレベル3が、その下にある段落の本文より小さく表示されるようになっていたのです。見出しの階層としては正反対の状態です。h2 は固定の 1.4rem(22.4px)で影響を受けておらず、3つの中で最も大きいままでした。

本文が h3 の固定サイズを超えたため、h3 を拡大して修正「変更前」と「変更後」の2枚のカード。変更前は 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 に固定されたまま本文テキストのトークンと連動しておらず、本文を大きくした結果、h3 を追い越して階層が逆転しました。その後 h3 も大きくして修正しました。

修正はたった1行でした。.prose h31.3125rem(21px)に変更し、新しい本文サイズより上、h2 より下という位置に戻したのです。型チェックもビルドも、1行の文字数の実測も、単独ではこの問題を見つけられませんでした。どれも、独立して設定された2つのフォントサイズを互いに比べてはいないからです。その比較こそ、レビューという工程が存在する理由です。

このブログの本文カラムが広がり、本文サイズも大きくなった様子。この修正を含む記事自身で実際にレンダリングしたもの

両方の変更を反映したあとのカラムと本文サイズ。目視ではなく実測で確認しています。

まとめ

本文カラムはすでにZennと同じくらいで、英語については問題ありませんでした。本当のバグは、ラテン文字の数字を基準にした単位で表現された同じmeasureが、日本語と繁体字中国語には英語のおよそ半分の文字数しか与えていなかったことでした。誰かが意図して決めたわけでも、誰かがチェックしたわけでもない差です。dev.toに合わせて共通の基準値を広げ、スクリーンショットの見比べではなく実測を使ったことで、両方が同時に直りました。新しい 47rem / 19px の組み合わせは、専用のオーバーライドなしで日本語と繁体字中国語を1行およそ37.5文字に収め、WCAG 1.4.8の上限にも収まり、実例のベンチマークにも一致します。その計算がまったく気づけなかったのが、見出しが本文より小さく表示されるという問題であり、それを見つけるのがレビューという工程の役目です。

参考リンク

この記事をシェア