Markdown の目次とアンカー:GitHub 形式のスラッグ生成と落とし穴

約7分

長い技術記事には目次(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. テキストを小文字に変換
  2. 句読点・記号を削除! @ # $ % ^ & * ( ) + = < > ? , . ; : ' " \ | / [ ] { } などを削除
  3. 空白をハイフンに置換:連続する空白も 1 つのハイフンに
  4. 同名の見出しが複数ある場合は連番を付与-1-2、… の suffix

見出し生成されるスラッグ
## Hello Worldhello-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 を付与(タイトルと無関係)
HackMDGitHub に近いが微妙に違う
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 準拠のスラッグ生成で目次を出力します。長い記事を書いた後に貼り付けて、目次部分をそのまま記事の冒頭に追加するワークフローで使えます。