WordPressの投稿記事に目次(Table of Contents)を表示したい場合、プラグインを使うのが一般的です。しかし、プラグインを増やしすぎるとサイトが重くなったり、デザインの統一が難しくなることがあります。
この記事では、プラグインを使わずに、functions.phpとCSSだけで目次機能を実装する方法を解説します。テーマに最適化されたデザインで、軽量な目次機能が手に入ります。
この下に表示されているものになります。
プラグインを使わない目次実装のメリット
サイトの軽量化
プラグインは便利ですが、不要な機能も一緒に読み込まれることが多く、ページの読み込み速度に影響します。

デザインの完全なコントロール
プラグインのデザインをカスタマイズしようとすると、CSSの優先順位で苦労することがあります。
プラグインの場合:プラグインのCSSの優先度が高い (上書きが難しい )
自作の場合:テーマのCSSに直接記述 (確実に反映)
テーマとの一体化
css命名規則など、テーマの設計思想に合わせた実装ができます。
目次機能の仕組み
WordPressで目次を自動生成するには、以下の流れで実装します
the_content()フィルターの活用
WordPressにはthe_content()という、投稿本文を出力する関数があります。この関数は内部でapply_filters('the_content', $content)を実行しており、ここにフック(処理を追加)することで、本文を自動的にカスタマイズできます。
1.投稿ページかどうかを判定
- 投稿ページ(single.php)の場合のみ処理を続行
- 固定ページや一覧ページでは、本文をそのまま返して終了
2.本文から見出しタグを抽出
- 本文内のh2とh3タグを正規表現で検索
- 各見出しから「レベル(2 or 3)」「既存の属性(classなど)」「テキスト内容」の3つの情報を取得
- すべての見出し情報を配列に格納
3.見出しの数をチェック
- 見出しが2個未満の場合は目次不要と判断
- 本文をそのまま返して処理終了
4.目次のHTMLを組み立て開始
- 目次の外側となるdiv要素を作成
- 「目次」というタイトルを追加
- リスト(ul要素)の開始タグを追加
5.各見出しをループ処理
- 見出しレベル(h2 or h3)を取得
- 見出しごとに以下の処理を実行
- 既存の属性(classなど)を取得
- 見出しのテキストからHTMLタグを除去
- ユニークなID(heading-0, heading-1…)を生成
6.ID属性を追加する処理
- 既存の属性にid属性がない場合は、新たに追加
- すでにid属性がある場合は、新しいIDで上書き
- 重要:既存のclass属性などは保持(テーマのスタイルを崩さない)
7.本文内の見出しタグを更新
- 元の見出しタグを、ID属性付きの見出しタグに置き換え
- 最初に見つかった1個だけを置換(同じ見出しが複数ある場合の対策)
8.目次リストに項目を追加
- 見出しがh3の場合は、インデント用のクラスを付与
- 見出しテキストと、対応する見出しへのリンク(#heading-0など)を含むリスト項目を生成
- 特殊文字をエスケープしてセキュリティ対策
9.目次HTMLを完成させる
- リストの閉じタグを追加
- 目次全体の閉じタグを追加
10.本文に目次を挿入
- 最初のh2タグを検索
- そのh2タグの直前に、生成した目次を挿入
- 結果として、導入文の後、最初の見出しの前に目次が配置される
11.更新された本文を返す
- 目次とID付き見出しを含む本文を返す
- WordPressがこれを画面に出力
functions.phpのコード
/**
* 目次を自動生成
*
* @param string $content 投稿本文
* @return string 目次を含む本文
*/
function generate_table_of_contents($content)
{
// 投稿ページ以外は処理しない(固定ページや一覧ページでは実行しない)
if (!is_single()) {
return $content;
}
// 見出しタグを取得(h2とh3を対象に、class属性なども含めて取得)
// $matches[0] = 見出し全体、$matches[1] = レベル(2 or 3)、$matches[2] = 属性、$matches[3] = テキスト
preg_match_all('/<h([2-3])(.*?)>(.*?)<\/h[2-3]>/i', $content, $matches, PREG_SET_ORDER);
// 見出しが2つ未満なら目次は不要なので、本文をそのまま返す
if (count($matches) < 2) {
return $content;
}
// 目次のHTMLを組み立て開始
$toc = '<div class="p-article__toc">';
$toc .= '<div class="p-article__toc-title">目次</div>';
$toc .= '<ul class="p-article__toc-list">';
// 見出しに付与するID用のカウンター
$index = 0;
// 各見出しをループ処理
foreach ($matches as $heading) {
$level = $heading[1]; // 見出しレベル (2 or 3)
$attributes = $heading[2]; // 既存の属性(class など)
$title = strip_tags($heading[3]); // 見出しテキスト(HTMLタグを除去)
$id = 'heading-' . $index; // ユニークなID(heading-0, heading-1, ...)
// 既存の属性にIDを追加(既存のclassなどを保持)
if (strpos($attributes, 'id=') === false) {
// id属性がない場合は追加
$new_attributes = $attributes . ' id="' . $id . '"';
} else {
// すでにid属性がある場合は上書き
$new_attributes = preg_replace('/id=["\'].*?["\']/', 'id="' . $id . '"', $attributes);
}
// 本文内の見出しを、ID付きの見出しに置き換え
// preg_quote()で特殊文字をエスケープし、最後の1は「最初の1個だけ置換」を意味する
$content = preg_replace(
'/' . preg_quote($heading[0], '/') . '/',
'<h' . $level . $new_attributes . '>' . $heading[3] . '</h' . $level . '>',
$content,
1
);
// 目次リストに項目を追加(h3の場合はインデント用のクラスを付与)
$class = ($level == 3) ? ' class="p-article__toc-item--sub"' : '';
// esc_html()で特殊文字をエスケープ(セキュリティ対策)
$toc .= '<li' . $class . '><a href="#' . $id . '">' . esc_html($title) . '</a></li>';
// 次の見出し用にカウンターを増やす
$index++;
}
// 目次のHTMLを完成させる
$toc .= '</ul></div>';
// 最初のh2の前に目次を挿入
// '$0'は正規表現でマッチした文字列(最初のh2タグ)を表す
// 最後の1は「最初の1個だけ置換」を意味する
$content = preg_replace('/<h2.*?>/i', $toc . '$0', $content, 1);
// 目次とID付き見出しを含む、更新された本文を返す
return $content;
}
// WordPressのthe_content()フィルターに、この関数をフック(登録)
// これにより、投稿本文が出力される前に、自動的にこの関数が実行される
add_filter('the_content', 'generate_table_of_contents');CSSでのスタイリング
function.phpで付与したクラスのスタイルをそれぞれ好みのデザインに指定します。
.p-article__toc
.p-article__toc-title
.p-article__toc-list
// h3の目次項目(インデント)
.p-article__toc-item--sub
まとめ
WordPressで目次を自作実装することで、記事全体を把握しやすくなり、読みたい箇所へアクセスしやすくなります。プラグインを使わない実装は、サイトを軽量化することができ、テーマのデザインに完全統合できるため、スタイルも管理しやすくなります。functions.phpとCSSだけで実装できるシンプルさでありながら、カスタマイズの自由度が高いので試してみてください。