本文へ

開発記録

記事の目次は本文のレンダリング後の見出しから作る

このサイトの記事レイアウトを例に、レンダリング後の見出しから目次を作り、見出しの文字列・順序・アンカーの参照元を共通化する方法と、モバイルの Drawer、JavaScript なしでの閲覧、現在の節の表示を分けて扱う理由を説明します。

林予晴
公開
  • コンテンツ設計
  • Astro
  • Markdown
  • 記事の目次

記事の節の見出しを変えるたびに、横の目次も修正する必要があると、二つの構造にずれが生じます。本文の見出しが変わったり、節の順序が入れ替わったりしても、目次は以前の位置を指したままかもしれません。

このサイトでは、記事の目次用に別の一覧を書くのではなく、Markdown のレンダリング後に得られる見出しデータを使っています。どの節があるかは本文が決め、目次はその中からナビゲーションに適したものを表示します。

この方法で減らせるのは、二つの構造を維持する手間です。それだけで記事が読みやすくなるわけではありません。見出しの分かりやすさや節の順序は、引き続き執筆と校正で整える必要があります。

本文を先に書き、目次に載せる見出しを選ぶ

このサイトの記事タイトルは frontmatter にあり、レイアウトが主見出しの h1 として出力します。Markdown の本文は h2 から始めます。記事の取得処理でも本文に h1 が混ざっていないかを確認し、レイアウトのタイトルと本文にそれぞれ最上位の見出しができることを防いでいます。

記事レイアウトは内容をレンダリングした後、見出しデータから depth === 2 の項目を選び、現在の目次に使います。

そのため、書き手が別の toc フィールドを埋めたり、各節へのリンクを手作業で書いたりする必要はありません。本文の h2 の文字列や順序を変えれば、次のレンダリングで目次にも新しいデータが使われます。

これは意図的に範囲を絞ったルールです。現在は主な節だけを目次に載せ、すべての h3 や h4 を多階層の一覧に展開してはいません。今の記事構造にはこの形が合っています。将来、長いチュートリアルで個々の細かな手順に直接移動する必要が出てきたら、階層化した目次を検討します。

アンカーは推測せず、レンダリング結果を使う

Astro の render() は、コンテンツコンポーネントと見出しの配列を返せます。見出しには depth、slug、text などのデータがあります。[3] このサイトの目次は text をリンクの文字列に使い、slug から #... の移動先を組み立てます。

次の図は現在のデータの流れを簡略化したものです。書き手が手作業で維持する設定を増やすものではありません。

Markdown の節の見出し
        ↓
コンテンツのレンダリングで見出しデータを生成
        ↓
h2 の文字列・順序・slug を取り出す
        ↓
目次のリンクが本文の見出し ID を指す

目次側に、中国語の文字列を URL に変換する関数を別途作ることはしません。句読点、英字の大文字と小文字、重複する見出しの扱いは、本文のレンダリングで使う識別子と一致させる必要があります。別に計算し直すと、かえって異なる移動先になる可能性があります。

保守上の制限もあります。節の見出しを書き換えると、見出し ID も変わる可能性があります。誰かが以前の節へのリンクを保存していても、自動生成の目次が古いアンカーとの互換性を維持してくれるわけではありません。長期にわたって安定した参照が必要な節には、別のアンカー方針が必要です。記事の stable ID と節のアンカーを同じものとして扱うことはできません。

デスクトップとモバイルで構造を共有し、表示方法は分ける

現在、デスクトップでは本文の横のサイドバーに目次を置き、モバイルでは開閉できる Drawer を使っています。どちらにも同じ主見出しの一覧を渡し、目次コンポーネントも共用しています。

それぞれに内容を手書きして、一方では節が抜け、もう一方では古い見出しが残るといったずれを避けられます。共有するのは節の構造であり、二つの画面サイズでまったく同じレイアウトを使う必要はありません。

記事に主見出しがなければ、目次関連のブロックは出力しません。現在のルールでは、一覧に項目があれば表示します。「見出しが二つしかなければ隠す」といった件数のしきい値は実装していません。今後変更するなら、表示条件を明示的に変えるべきであり、まだ存在しない動作を記事に書くべきではありません。

スクリプトがなくても本文とリンクを使えるようにする

目次のリンクには、HTML の段階で実在する節への移動先があります。デスクトップの目次は、スクリプトの実行を待って生成するものではありません。モバイルにも別途 noscript の一覧を用意し、JavaScript がない読者も通常のリンクを使えるようにしています。

これは、スクリプトがなくてもモバイルの Drawer 自体が通常どおり動くという意味ではありません。Drawer の開閉は対話的な操作であり、noscript の一覧はそれとは別の閲覧経路です。両者を分けて説明することで、「本文を読める」を「すべての動的な操作にスクリプトが不要」と取り違えずに済みます。

スクリプトが有効なときは、現在の節の表示、該当する目次項目の aria-current、モバイルの目次を選んだ後に Drawer を閉じる処理も加えています。スクロールは reduced-motion の設定に応じて、滑らかな移動か即時の移動を選びます。

これらの機能は、本文に実際に存在する h2[id] と目次リンクの移動先を使います。節の名前を別の一覧として保存するわけではありません。

自動生成でも確認は省けない

見出しから目次を作ることで減らせるのは、手書きによる不一致のリスクだけです。書き手が「その他」「補足」のような分かりにくい見出しを付けたり、小さな注意事項を主な節に格上げして、目次を長くしすぎたりすることはあります。

まず目次から記事を読む順序が伝わるかを確認し、その後で生成結果を調べます。

  • 目次の文字列と本文の主見出しは一致していますか?
  • 各リンクに対応する見出し ID はありますか?
  • デスクトップ、モバイル、スクリプトなしの一覧には同じ節が含まれていますか?
  • 主見出しがないときに、用途のない目次コンテナが残っていませんか?

レイアウトを変更した後は、実際のページを操作する必要もあります。クリックで目的の節に移動するか、モバイルの Drawer が閉じるか、固定ナビゲーションが見出しを隠さないか、現在の節の表示が妥当かを確認します。データが一致していても、閲覧位置や見た目まで検収済みとは限りません。

記事構造の変更箇所を本文に一本化する

この設計では、記事の校正を本文に戻せます。目次が分かりにくければ、サイドバーだけに見栄えのよい名前を付けるのではなく、まず節の見出しと順序を直します。

読者が節へのリンクを開いた先で見るのは、やはり本文です。目次は内容を探す道のりを短くできますが、本文とは別の、見栄えだけのよい構造を作って、両者の違いを隠すものではありません。

この記録の範囲

この記事が説明するのは、このサイトの記事レイアウトでの見出しの絞り込み、目次コンポーネント、モバイルの Drawer、JavaScript なしで使う一覧、ブラウザのスクリプトが担う役割です。現在すでに多階層の目次や固定された節アンカーの契約があること、読みやすさの効果を研究したこと、すべての端末で検収を終えたことは主張していません。記事の識別と多言語ルーティングは別の責務であり、目次の一覧が決めるものではありません。

出典

[3] https://docs.astro.build/en/reference/modules/astro-content