【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のフィルターを利用して、本文内のh2・h3に自動で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を一致させる必要があります。
しかし、記事を書くたびにh2やh3へ手動でIDを設定するのは手間がかかります。
そこで、WordPressのフィルターを利用して、本文内のh2・h3に自動で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');この処理では、本文内のh2・h3を取得し、以下のように自動で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の目次を実装できます。
コード量もそれほど多くなく、デザインや機能も自由にカスタマイズできるため、オリジナルテーマを制作している方には特におすすめです。
また、今回追加したような開閉機能も、既存の目次に少しコードを追加するだけで実装できます。
サイトの運用状況に合わせて、表示件数やデザイン、アニメーションなどを柔軟に変更できる点も、自作目次のメリットです。
