Markdown の目次とアンカー:GitHub 形式のスラッグ生成と落とし穴
長い技術記事には目次(Table of Contents、TOC)があると、読者がスキャンしやすく離脱率も下がります。Markdown では [見出し名](#anchor) という単純な形式で書きますが、その「アンカー名」が見出しからどう生成されるかは GitHub などのレンダラーが暗黙のルールで決めています。本記事ではそのルールと、目次を自動生成するときに気をつけるべき点を整理します。
なぜ目次が要るのか
技術記事でよくある「3000字以上のスクロールが必要なページ」は、目次なしだと読者がどこに何が書いてあるか把握できません。目次があると:
- スキャンしやすい:ページ全体の構成が一目で分かる
- 目的のセクションに直接ジャンプできる:時間の節約
- SEO 上もメリット:Google の検索結果で「ジャンプリンク」が表示されることがある(
Jump links to ...) - 共有しやすい:「この記事の section X を読んで」と特定セクションの URL を共有できる
短い記事(800字以下)には不要ですが、1500字を超えるなら目次の検討価値があります。
Markdown のアンカーリンク:基本形式
Markdown では見出しに自動的にアンカー(HTML の id 属性)が付与され、[テキスト](#anchor) でリンクできます:
## Section A
ここが Section A の本文。
## Section B
[Section A に戻る](#section-a) #section-a の部分が自動生成されたアンカー名(スラッグ)です。問題は、このスラッグがレンダラーごとに微妙に違うルールで生成されることです。
GitHub Flavored Markdown のアンカー生成ルール
GitHub Flavored Markdown(GFM)は事実上の標準です。GitHub のレンダラーは見出しから次のルールでスラッグを作ります:
- テキストを小文字に変換
- 句読点・記号を削除:
! @ # $ % ^ & * ( ) + = < > ? , . ; : ' " \ | / [ ] { }などを削除 - 空白をハイフンに置換:連続する空白も 1 つのハイフンに
- 同名の見出しが複数ある場合は連番を付与:
-1、-2、… の suffix
例
| 見出し | 生成されるスラッグ |
|---|---|
## Hello World | hello-world |
## What's that? | whats-that |
## Setup (1 回目) | setup |
## Setup (2 回目) | setup-1 |
## Setup (3 回目) | setup-2 |
## 日本語の見出し | 日本語の見出し(そのまま保持) |
## API Reference (v2) | api-reference-v2 |
CJK 文字はそのまま保持されますが、リンクとして使うときは URL エンコードされて %E6%97%A5%... のような形になります。これが「日本語アンカーが URL で読みづらい」と言われる理由です。
他のレンダラーとの違い
GFM 以外の主要レンダラーも独自ルールを持っています:
| レンダラー | スラッグの違い |
|---|---|
| GitHub | 標準ルール(上記) |
| GitLab | 似ているが大文字も保持される場面あり |
| Notion | 独自のハッシュ ID を付与(タイトルと無関係) |
| HackMD | GitHub に近いが微妙に違う |
| Hugo / Jekyll | 設定で切り替え可能(GFM 互換 or 独自) |
異なるレンダラー間で記事を移動すると、目次のリンクが切れる原因になります。「自分が使うレンダラーのルール」を把握しておくのが第一原則です。
目次自動生成のロジック
ツールが目次を自動生成するときの基本フローは:
1. Markdown を行ごとに走査
2. # で始まる行を「見出し」として抽出
3. 見出しレベル(# の数)を記録
4. 見出しテキストからスラッグを生成
5. 同名スラッグの重複検出と連番付与
6. ネストしたリスト形式の Markdown を出力 実装で気をつけるポイント:
1. コードブロック内の # を見出しと誤認しない
```python
# This is a Python comment, not a heading
def foo():
pass
``` コードフェンス(“または~~~`)の内側はパースから除外する必要があります。コードフェンスのネストや、開始・終了マーカーが一致しないケースも考慮が必要です。
2. ATX クローズ形式の # を除去
ATX 見出しには「閉じ #」を付ける形式があります:
## Section スラッグ生成時は閉じる # を取り除いてから処理します。
3. インライン Markdown の処理
見出しに 太字 や code が含まれる場合、表示用テキストとスラッグ用テキストで処理が分かれます:
## **Important** stuff GitHub の挙動:
- 表示:太字込みでレンダリング
- スラッグ:マークアップを除いた
important-stuff
ツール側でマークアップを正規表現で除去するか、Markdown パーサーで AST に変換してからテキストノードだけ取るのが確実です。
4. インデント幅の選択
ネストしたリストのインデント幅は通常 2 または 4 スペース:
- [Section A](#section-a)
- [Subsection A.1](#subsection-a1) ← 2 スペース
- [Section B](#section-b) GitHub のリストパーサーは 2 スペースインデントでもネスト扱いしますが、別のレンダラーでは 4 スペース必須のものもあります。出力先に合わせて調整します。
目次設計のコツ
1. H1 は目次に含めない
H1 はページタイトルなので、本文中の見出しを目次にする場合は H2 以降を含めるのが慣例。記事タイトルが H1 で、目次は H2〜H4 を対象にするのが標準的。
2. 深さは H4 までで止める
H4 や H5 まで含めると目次が密になりすぎて、概観としての価値が下がります。「大きな構造を見せる」が目次の目的なので、深さは抑えめが読みやすい。
3. 同名見出しは避ける
連番付与(-1、-2)が発動すると、リンクが「2 番目の同名見出し」を指すようになり、後から記事を編集すると順番が変わって壊れます。各セクションに固有の名前を付けるのが安全。
4. 動的に再生成する習慣
記事を編集して見出しを変更するたびに、目次も再生成する必要があります。手動で同期させると忘れがちなので、エディタや CI で自動化するか、執筆プロセスのチェックリストに入れるのが良いです。
まとめ
- 1500 字以上の記事には目次があると読みやすい
- Markdown のアンカーは「小文字化 + 句読点除去 + 空白→ハイフン + 重複連番」で生成される
- レンダラーごとに微妙にルールが違うので、使う環境のルールを把握する
- 自動生成ツールではコードブロック内の
#除外と ATX クローズ形式の処理に注意
本サイトの Markdown TOC ジェネレーターは GFM 準拠のスラッグ生成で目次を出力します。長い記事を書いた後に貼り付けて、目次部分をそのまま記事の冒頭に追加するワークフローで使えます。