【WordPress】目次を自作する方法|プラグインなしで開閉機能も追加

WordPressで作るコピペで簡単目次を自作&カスタマイズ

WordPressで目次を作るならプラグインを使う方法が一般的ですが、機能が多すぎたり、サイトを軽く保ちたいという理由から、自作したい場面もあります。

今回は、私が実際にこのサイトで実装した「プラグインを使わない目次機能」の作り方を忘備録としてまとめます。

完成イメージ

PC版目次

PC版目次 ホバー表示

  • h2・h3を自動取得
  • アンカーリンク付き
  • h3はインデント表示

完成コード

template-parts/toc.php に記載

<?php
$content = get_the_content();
preg_match_all(
    '/<h([2-3])[^>]*>(.*?)<\/h[2-3]>/i',
    $content,
    $matches,
    PREG_SET_ORDER
);
if (empty($matches)) {
    return;
}
?>

<nav class="toc">
    <p class="toc__title">目次</p>
    <ul class="toc__list">
        <?php
        $count = 1;
        foreach ($matches as $heading) :
            $level = $heading[1];
            $text = wp_strip_all_tags($heading[2]);
        ?>
            <li class="toc__item toc__item--h<?php echo esc_attr($level); ?>">
                <a href="#toc-<?php echo esc_attr($count); ?>">
                    <?php echo esc_html($text); ?>
                </a>
            </li>
        <?php
            $count++;
        endforeach;
        ?>
    </ul>
</nav>

任意のcssに以下をペーストしてください。

/* ==========================
   目次
========================== */

.toc {
    margin-top: 4rem;
    padding: 1.5rem 1rem;
    border: 1px solid #eee;
    border-radius: 18px;
}

.toc__title {
    position: relative;
    margin-bottom: 14px;
    padding-left: 12px;
    font-size: 1rem;
    font-weight: var(--fw-bold);
    letter-spacing: 0.06em;
}

.toc__title::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    width: 4px;
    height: 16px;
    background: var(--color-main);
    transform: translateY(-50%);
}

.toc__list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;

}

.toc__item a {
    display: block;
    padding: 2px 8px;
    color: #333;
    font-size: 14px;
    line-height: 1.6;
    text-decoration: none;
    transition: background 0.2s ease, transform 0.2s ease;
}

.toc__item a:hover {
    background: #f5f5f5;
    transform: translateX(4px);
}

.toc__item--h3 a {
    position: relative;
    padding-left: 28px;
    color: #555;
    font-size: 13px;
}

.toc__item--h3 a::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 14px;
    width: 4px;
    height: 4px;
    background: #999;
    border-radius: 50%;
    transform: translateY(-50%);
}

目次リンクをクリックして該当箇所へ移動させるには、目次側のリンク先と本文側の見出しIDを一致させる必要があります。

そこで、WordPressのフィルターを利用して、本文内のh2h3に自動でIDを付与します。

functions.phpに記載

function add_heading_id_to_content($content) {
	if (is_single()) {
		$count = 1;
		$content = preg_replace_callback(
			'/<h([2-3])([^>]*)>(.*?)<\/h[2-3]>/i',
			function ($matches) use (&$count) {
				$id = 'toc-' . $count;
				if (strpos($matches[2], 'id=') === false) {
					return '<h' . $matches[1] . ' id="' . $id . '"' . $matches[2] . '>' . $matches[3] . '</h' . $matches[1] . '>';
				}
				return $matches[0];
			},
			$content
		);
	}
	return $content;
}
add_filter('the_content', 'add_heading_id_to_content');

目次のコードを書く場所 

目次を表示するためのコードはテンプレートパーツとして作成することをおすすめします。

テンプレートパーツとして分けておくことでコードの管理がしやすくなり、他のテンプレートからも再利用できます 。

まずは、テーマフォルダ内に以下のような構成でファイルを作成してください。

テーマフォルダ

├── single.php

└── template-parts

    └── toc.php

その後、目次を表示したい場所(例:single.php)で、以下のコードを記述してテンプレートパーツを読み込みます。

<?php get_template_part( 'template-parts/toc' ); ?>

これで、toc.phpに記述した目次が記事ページへ表示されるようになります。

目次の仕組み

最初に全体の流れを説明します。

投稿本文を取得

 ↓

h2・h3を抽出

 ↓

見出しにIDを付与

 ↓

<ul>を自動生成

 ↓

目次リンクと本文の見出しを紐づけ

 ↓

目次を表示

ここを先に理解してもらうと、その後のコードが読みやすくなります。

1. 投稿本文を取得する

まずは記事本文を取得します。

$content = get_the_content();

get_the_content()を使うことで、投稿本文を文字列として取得できます。

今回はこの本文からh2・h3だけを探して目次を作ります。

2. h2・h3だけ取得する

preg_match_all(
    '/<h([2-3])[^>]*>(.*?)<\/h[2-3]>/i',
    $content,
    $matches,
    PREG_SET_ORDER
);

この正規表現では、h2とh3だけを取得しています。

例えば以下の見出しを作ったとします。

<h2>サービス</h2>
<h3>料金</h3>
<h2>お問い合わせ</h2>

取得結果は以下のようになります。

Array
(
    [0]
        level => 2
        text => サービス
    [1]
        level => 3
        text => 料金
    [2]
        level => 2
        text => お問い合わせ
)

3. 見出しがない場合は表示しない

if (empty($matches)) {
    return;
}

見出しがない記事では目次も不要です。

この処理があることで空の目次が表示されません。

4. HTMLを生成する

<nav class="toc">
    <p class="toc__title">目次</p>
    <ul class="toc__list">
        <?php
        $count = 1;
        foreach ($matches as $heading) :
            $level = $heading[1];
            $text = wp_strip_all_tags($heading[2]);
        ?>
            <li class="toc__item toc__item--h<?php echo esc_attr($level); ?>">
                <a href="#toc-<?php echo esc_attr($count); ?>">
                    <?php echo esc_html($text); ?>
                </a>
            </li>
        <?php
            $count++;
        endforeach;
        ?>
    </ul>
</nav>

<nav class=”toc”>から</nav>までが目次本体です。

ループでforeach ($matches as $heading)を回して、<li>を増やしています。

5. h2とh3でデザインを分ける

ここがこのコードのポイントです。

<li class="toc__item toc__item--h<?php echo esc_attr($level); ?>">

見出しレベルによって

toc__item--h2

toc__item--h3

というクラスが付きます。

そのためCSSでは

.toc__item--h3 a

だけインデントさせています。

以下で左側の余白を作り、

.toc__item--h3 a {
    padding-left: 28px;
}

::beforeで丸を表示しています。

.toc__item--h3 a::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 14px;
    width: 4px;
    height: 4px;
    background: #999;
    border-radius: 50%;
    transform: translateY(-50%);
}

6. アンカーリンクを作る

目次リンクをクリックして該当箇所へ移動させるには、目次側のリンク先と本文側の見出しIDを一致させる必要があります。

しかし、記事を書くたびにh2h3へ手動でIDを設定するのは手間がかかります。

そこで、WordPressのフィルターを利用して、本文内のh2h3に自動でIDを付与します。

functions.phpに以下のコードを追加してください。

function add_heading_id_to_content($content) {
	if (is_single()) {
		$count = 1;
		$content = preg_replace_callback(
			'/<h([2-3])([^>]*)>(.*?)<\/h[2-3]>/i',
			function ($matches) use (&$count) {
				$id = 'toc-' . $count;
				if (strpos($matches[2], 'id=') === false) {
					return '<h' . $matches[1] . ' id="' . $id . '"' . $matches[2] . '>' . $matches[3] . '</h' . $matches[1] . '>';
				}
				return $matches[0];
			},
			$content
		);
	}
	return $content;
}
add_filter('the_content', 'add_heading_id_to_content');

この処理では、本文内のh2h3を取得し、以下のように自動でIDを追加します。

<h2 id="toc-1">見出しタイトル</h2>
<h3 id="toc-2">見出しタイトル</h3>

これにより、目次側の、<a href=”#toc-1″>見出しタイトル</a>と本文側の見出しが紐づき、クリックすると該当箇所までスクロールできるようになります。

記事を書くたびにIDを入力する必要がなくなるため、自作目次を運用する場合は一緒に追加しておくと便利です。

7. スクロール位置を調整する(固定ヘッダーがある場合)

固定ヘッダーがあるサイトでは、以下のコードを追加してください。

h2,
h3{
    scroll-margin-top:120px;
}

120pxのところは任意のサイズに調整して使ってください。

これにより見出しがヘッダーの裏へ隠れません。

8. CSSで見た目を整える

CSSでは次のような点を調整しています。

  • 枠線
  • 角丸
  • ホバーアニメーション
  • h3のインデント
  • タイトル左のアクセントバー

自作の目次は好きなデザインに変更できるのがメリットです。

カスタマイズ例:目次に開閉ボタンを追加する

この実装は簡単に拡張できます。

  • h4まで対応する
  • 番号付き目次にする
  • 開閉ボタンを付ける
  • 現在読んでいる見出しをハイライトする
  • スマホでは途中まで表示して「目次をすべて表示」を付ける

など、お好きにカスタマイズしてください。

私は実際にこの目次を運用していると、記事によっては見出し数が多くなり、目次だけでページ上部のスペースを大きく占有してしまうことがありました。

特に長文記事では、ユーザーが本文へ移動するまでに無駄なスクロールが発生し、必要な情報へアクセスしづらくなる場合があります。

そこで、目次をすべて表示したいユーザーと、概要だけ確認したいユーザーの両方に対応できるよう、開閉ボタンを追加しました。

初期状態では目次を一部だけ表示し、「目次をすべて表示」ボタンを押すことで全文を確認できるようにします。

1.HTMLに開閉ボタンを追加する

まず、前回作成した目次のHTMLにボタンを追加します。

変更前:

<nav class="toc">
	<p class="toc__title">目次</p>

	<ul class="toc__list">
		<?php $count = 1; foreach ($matches as $heading) :
			$level = $heading[1];
			$text = wp_strip_all_tags($heading[2]);
		?>
			<li class="toc__item toc__item--h<?php echo esc_attr($level); ?>">
				<a href="#toc-<?php echo esc_attr($count); ?>">
					<?php echo esc_html($text); ?>
				</a>
			</li>
		<?php $count++; endforeach; ?>
	</ul>
</nav>

変更後:

<nav class="toc">
	<p class="toc__title">目次</p>

	<ul class="toc__list">
		<?php $count = 1; foreach ($matches as $heading) :
			$level = $heading[1];
			$text = wp_strip_all_tags($heading[2]);
		?>
			<li class="toc__item toc__item--h<?php echo esc_attr($level); ?>">
				<a href="#toc-<?php echo esc_attr($count); ?>">
					<?php echo esc_html($text); ?>
				</a>
			</li>
		<?php $count++; endforeach; ?>
	</ul>

	<button class="toc__toggle">
		目次をすべて表示
	</button>
</nav>

buttonを追加するだけなので、既存の目次生成部分はそのまま利用できます。

2.CSSで目次の高さを制限する

次に、初期状態では目次を一定の高さで隠します。

.toc__listに以下を追加します。

.toc__list {
	display:flex;
	flex-direction:column;
	gap:10px;
	margin:0;
	padding:0;
	list-style:none;

	max-height:220px;
	overflow:hidden;
	transition:max-height .4s ease;

	mask-image:linear-gradient(
		to bottom,
		#000 0%,
		#000 80%,
		transparent 100%
	);

	-webkit-mask-image:linear-gradient(
		to bottom,
		#000 0%,
		#000 80%,
		transparent 100%
	);
}

mask-imageを使うことで、最後の部分だけ自然にフェードアウトさせています。

単純に非表示にするより、続きを感じられるデザインになります。

3.開いた状態のCSSを追加する

ボタンを押した時は、フェードを解除します。

.toc.is-open .toc__list {
	mask-image:none;
	-webkit-mask-image:none;
}

高さの変更はJavaScript側で制御するため、ここではグラデーション解除だけ行います。

4.開閉ボタンのデザインを追加する

ボタンを目次下部に配置します。

.toc__toggle {
	display:flex;
	align-items:center;
	justify-content:center;
	gap:.5rem;
	width:fit-content;
	margin:1rem auto 0;
	padding:.8rem 1.4rem;
	border:1px solid #ddd;
	border-radius:999px;
	background:#fff;
	color:#333;
	font-size:.875rem;
	font-weight:var(--fw-bold);
	cursor:pointer;
	transition:.2s ease;
}

.toc__toggle:hover {
	border-color:var(--color-main);
	color:var(--color-main);
}

5.JavaScriptで開閉を制御する

最後に、ボタンを押した時の動きを追加します。

document.addEventListener("DOMContentLoaded", () => {
	const toc = document.querySelector(".toc");
	const list = document.querySelector(".toc__list");
	const button = document.querySelector(".toc__toggle");

	if (!toc || !list || !button) return;

	const closeHeight = 220;
	const openText = "目次を閉じる";
	const closeText = "目次をすべて表示";

	button.addEventListener("click", () => {
		if (toc.classList.contains("is-open")) {
			list.style.maxHeight = closeHeight + "px";
			toc.classList.remove("is-open");
			button.firstChild.textContent = closeText;
		} else {
			list.style.maxHeight = list.scrollHeight + "px";
			toc.classList.add("is-open");
			button.firstChild.textContent = openText;
		}
	});
});

scrollHeightを取得することで、目次の項目数に合わせて開く高さを自動調整しています。

そのため、見出しが少ない記事と多い長文記事のどちらでも対応できます。

まとめ

今回紹介した方法なら、プラグインを使わずにWordPressの目次を実装できます。

コード量もそれほど多くなく、デザインや機能も自由にカスタマイズできるため、オリジナルテーマを制作している方には特におすすめです。

また、今回追加したような開閉機能も、既存の目次に少しコードを追加するだけで実装できます。

サイトの運用状況に合わせて、表示件数やデザイン、アニメーションなどを柔軟に変更できる点も、自作目次のメリットです。

お問い合わせ

CONTACT

小さなお悩みや「何から始めればいいかわからない」といった段階でも大歓迎です。
まずはお気軽にご相談ください。