WordPressで目次を自作する方法を紹介します。
自作の目次は、functions.phpにコードを2つ(目次生成とスクロールアニメーション)、single.php等のテンプレートファイルにコードを数行挿入する(目次表示)ことで実装できます。
今回実装するの目次の機能
今回紹介する目次に実装する機能は下記のとおりです。
- 目次自動生成:H2タグが2つ以上ある場合に目次を自動生成する。
- 目次自動挿入:希望する任意の位置に目次を自動挿入できる。あらゆるテンプレートファイルに利用可能。例)single.php, single-$posttype.php, page.php, page-$slug.phpなどの希望の位置に直接目次コードを書き込むことができる。
- スクロールアニメーション:目次項目クリックでアンカー位置までスクロールアニメーションで移動。
- 固定ヘッダーの高さを考慮:アンカー先に移動する時、固定ヘッダーがあると隠れてしまいます。その対策として、固定ヘッダーの高さをpx単位で予め指定することで、アンカー上部に余白を残して移動します。
- 別ページのページ内セクションへの移動:サイトマップ等から別ページ内のセクション(H2見出しの位置等)にもリンクをつなげたい時のコード
この方法は、WordPressの the_contentフィルターを通じて処理されるコンテンツ(主に投稿やページの本文)に対してのみ機能します。直接、固定ページ等のテンプレートファイルにコーディングした見出しには対応できません。
コード1:目次の自動生成(functions.php)
- 「目次」というテキストをタイトルとして使用
- 目次全体を囲うタグ(<div class=”toc_wrapper”>)を設置
- コンテンツ内かH2タグを検出
- 目次に項目を追加
- H2タグのテキストからアンカー名を生成(ただし、空白は-に変換、大文字は小文字に変換)
- コンテンツ内のH2タグにアンカーを追加
- H2タグが2つ以下の場合は目次を表示しない
functions.phpに目次を自動生成するための下記コードをコピペして下さい。
/*----------------------------------------------------------*/
/* 目次自動生成 */
/*----------------------------------------------------------*/
function custom_generate_table_of_contents($content) {
$toc_content = '<h2>目次</h2><ul>';
$pattern = '/<h2 class="(wp-block-heading)">(.*?)<\/h2>/i';
$content_with_anchors = $content;
if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
// H2タグが2つ以上ある場合のみ目次を生成
if (count($matches[2]) > 1) {
$offset = 0;
foreach ($matches[2] as $index => $match) {
$anchor = strtolower(str_replace(' ', '-', $match[0]));
$toc_content .= '<li><a href="#' . $anchor . '">' . $match[0] . '</a></li>';
$anchor_tag = '<h2 class="' . $matches[1][$index][0] . '" id="' . $anchor . '">' . $match[0] . '</h2>';
$position = $matches[0][$index][1] + $offset;
$content_with_anchors = substr_replace($content_with_anchors, $anchor_tag, $position, strlen($matches[0][$index][0]));
$offset += strlen($anchor_tag) - strlen($matches[0][$index][0]);
}
$toc_content .= '</ul>';
// 目次の外枠を追加
$toc = '<div class="toc_wrapper">' . $toc_content . '</div>';
} else {
// H2タグが1つ以下の場合は空の文字列を返す
$toc = '';
}
} else {
// H2タグが見つからない場合も空の文字列を返す
$toc = '';
}
return ['toc' => $toc, 'content' => $content_with_anchors];
}
5行目のH2タグを検出するための正規表現パターンでは、今回はsingle.phpのブロックエディターで挿入したH2タグに自動付与されるclass属性「wp-block-heading」を使っています。
上手く動作しない時はChromeのデベロッパーツールなどで対象とするclass属性を確認してみて下さい。
補足:2種類以上のclass属性のh2タグを対象とするとき
今回は、投稿タイプ(single.php)およびカスタム投稿タイプ(single-$posttype.php)のブロックエディターで挿入したh2見出しに自動付与されるclass属性「wp-block-heading」のみを対象しているために下記のようにコード内で記述しています。
// H2タグを検出するための正規表現パターン
$pattern = '/<h2 class="wp-block-heading">(.*?)<\/h2>/i';
これに対して、例えば、class属性page_h2(任意)のh2タグも対象としたい場合には下記のようにコードを書くことで対応可能です。`|`は「または」を意味する正規表現の特殊文字です。
// H2タグを検出するための正規表現パターン:class属性2つ以上
$pattern = '/<h2 class="(wp-block-heading|page_h2)">(.*?)<\/h2>/i';
class属性が3つ以上に増えた場合も同様に対応可能です。
コード2:スクロールアニメーション(JavaScript)
- アンカー移動に上部に余白を持たせる:px単位で調整可能
- アンカーを移動時にスクロールアニメーション
script.js等のスクリプトファイルに下記コードをコピペして下さい。
/*----------------------------------------------------------*/
// 目次機能オプション
// ・アンカー移動時にスクロールアニメーション
// ・アンカー移動先に対して固定ヘッダーを考慮
/*----------------------------------------------------------*/
document.addEventListener('DOMContentLoaded', function () {
var headerOffset = 100; // 固定ヘッダーの高さに応じて調整
document.querySelectorAll('.toc_wrapper a').forEach(function (link) { // 目次リンクのCSSセレクター
link.addEventListener('click', function (e) {
e.preventDefault();
var targetId = link.getAttribute('href').substring(1);
var targetElement = document.getElementById(targetId);
if (targetElement) {
// スクロール位置を計算(固定ヘッダーの高さを考慮)
var topPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - headerOffset;
// スムーズスクロールを実行
window.scrollTo({
top: topPosition,
behavior: 'smooth'
});
}
});
});
});
上のコードでは2行目の下記コードでアンカー移動時の上部余白量を調整しています。今回は100pxとしていますが、状況に応じて変更して下さい。
var headerOffset = 100; // 固定ヘッダーの高さに応じて調整
document.querySelectorAll('.toc_wrapper a').forEach(function (link) {
セレクター名が異なる複数の目次でスクロールアニメーションを動作させたい時
複数のページ且つセレクター名が異なる場合には、上記コードで複数セレクターを指定する必要があります。それを変数を使って書き直したコードが下記です。
/*----------------------------------------------------------*/
// 目次機能オプション
// ・アンカー移動時にスクロールアニメーション
// ・アンカー移動先に対して固定ヘッダーを考慮
/*----------------------------------------------------------*/
document.addEventListener('DOMContentLoaded', function () {
var headerOffset = 50; // 固定ヘッダーの高さに応じて調整
// 両方の目次に対するセレクターを指定
var selector = '.toc_wrapper a, .privacy-table-contents_wrapper a';
document.querySelectorAll(selector).forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var targetId = link.getAttribute('href').substring(1);
var targetElement = document.getElementById(targetId);
if (targetElement) {
// スクロール位置を計算(固定ヘッダーの高さを考慮)
var topPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - headerOffset;
// スムーズスクロールを実行
window.scrollTo({
top: topPosition,
behavior: 'smooth'
});
}
});
});
});
3行目の変数で、複数のセレクターを指定しています。複数セレクターは,
で区切ればOKです。
// 両方の目次に対するセレクターを指定
var selector = '.toc_wrapper a, .privacy-table-contents_wrapper a';
コード3:目次を挿入(テンプレートファイル: single.phpなど)
下記コードを目次を挿入したテンプレートファイル(single.phpなど)に直接書いて下さい。
<!-- 目次を出力 -->
<?php
// 目次とアンカーが追加されたコンテンツを取得
$toc_and_content = custom_generate_table_of_contents(get_the_content());
// 目次とアンカーを出力
echo $toc_and_content['toc']; ?>
<!-- アンカーが追加されたコンテンツを出力 -->
<?php echo apply_filters('the_content', $toc_and_content['content']); ?>
上のコードには2つの機能がありますので、機能別に使い方を説明していきます。
目次を出力
目次出力に関するコードをテンプレートファイル内の目次を挿入したい位置に直接書いて下さい。
<!-- 目次を出力 -->
<?php
// 目次とアンカーが追加されたコンテンツを取得
$toc_and_content = custom_generate_table_of_contents(get_the_content());
// 目次とアンカーを出力
echo $toc_and_content['toc'];
?>
目次生成プラグイン等では、目次はコンテンツ内の先頭見出しの上が一番上位になりますが、この方法を使うとさらの上の位置に目次を挿入できます。例えば、タイトル(H1)とサムネイルの間に目次を表示させたい場合にはプラグインでは困難だと思います。
アンカーが追加されたコンテンツを出力
single.phpのループ内ではコンテンツを下記WordPress関数「the_content();」で呼び出しますが、
<?php the_content(); ?>
上記WordPress関数をそっくりそのまま下記のコードと差し替えて下さい。
<!-- アンカーが追加されたコンテンツを出力 -->
<?php echo apply_filters('the_content', $toc_and_content['content']); ?>
これにより、アンカーが追加されたコンテンツが出力されるようになり、目次のアンカーリンクが機能するようになります。
テンプレートファイル内の参考コード
下記に実際にsingle.phpのループ内に直接上記コードを書いた参考コードを載せておきます。
<?php if (have_posts()) : ?>
<?php while (have_posts()) : the_post(); ?>
<div class="single-title-upper_wrapper">
<!-- カテゴリーのデータを取得 -->
<?php
// カテゴリーのデータを取得
$cat = get_the_category();
$cat = $cat[0];
// カテゴリー名を取得
$cat_name = $cat->cat_name;
?>
<!-- カテゴリー名を出力 -->
<?php echo esc_html($cat_name); ?>
</div>
<h1 class="c-single-h1"><?php the_title(); ?></h1>
<!-- 投稿日表示 -->
<div class="single-title-bottom_wrapper"><?php the_time('Y年n月j日') ?></div>
<!-- 目次を出力 -->
<?php
// 目次とアンカーが追加されたコンテンツを取得
$toc_and_content = custom_generate_table_of_contents(get_the_content());
// 目次とアンカーを出力
echo $toc_and_content['toc']; ?>
<!-- サムネイル表示 -->
<div class="single-thumbnail-wrapper">
<?php the_post_thumbnail(); ?>
</div>
<!-- アンカーが追加されたコンテンツを出力 -->
<?php echo apply_filters('the_content', $toc_and_content['content']); ?>
<?php endwhile; ?>
<?php endif; ?>
目次のCSSカスタマイズ
以下に上記コードで生成された目次に対して、CSSをカスタマイズした参考コードを載せておきます。
// ------------------------------------
// 目次
// ------------------------------------
// 目次外枠
.toc_wrapper {
padding: 20px 40px 40px;
border: 5px solid fw.$gray-3;
border-radius: 10px;
margin: 50px 0;
@include fw.mq("sp") {
padding: 20px;
margin: 30px 0;
}
}
// 目次タイトル
.toc_wrapper h2{
font-size: 2.4rem;
font-weight: 600;
line-height: 1.65;
margin-bottom: 24px;
@include fw.mq("sp") {
font-size: 1.6rem;
margin-bottom: 14px;
}
}
// 目次 ulタグ
.single-post .l-main .l-section .toc_wrapper ul,
.single-case .l-main .l-section .toc_wrapper ul {
padding: 0px;
margin-bottom: 0px;
background-color: fw.$white;
list-style: none;
}
// リストマーカー無効化|必要なければ無視して下さい
.single-post .l-main .l-section .toc_wrapper li::after,
.single-case .l-main .l-section .toc_wrapper li::after {
display: none;
}
// 目次項目のフォント太さと文字色
.single-post .l-main .l-section .toc_wrapper li a,
.single-case .l-main .l-section .toc_wrapper li a {
font-weight: 600;
color: fw.$text;
}
上記CSSは、私のケースをそのまま載せたものですので、適宜変更して下さい。私の場合は投稿タイプのブロックエディターで挿入するulやolリストのCSSも自作していますので、上記コードのセレクター詳細度が複雑なものになっています。
コード4:別ページのセクションへのリンク
例えば、サイトマップ等から別ページの各セクションへのリンクを作りたい時は、上で紹介したコード3では表示にバグが現れるので、ここで紹介する「コード4」のJavaScriptに差し替えて下さい。
- サイトマップから別ページの各H2見出しへのリンクを作った時に、スマートフォン画面でH2見出しが固定ヘッダーに隠れてしまう。
- H2見出しに飛んだときにスクロールがカクつく
コード4ではこれらの対策を施しています。
/*----------------------------------------------------------*/
// 目次機能オプション
// ・アンカー移動時にスクロールアニメーション
// ・アンカー移動先に対して固定ヘッダーを考慮
// ・別ページのセクションへリンクアニメーションを考慮
/*----------------------------------------------------------*/
document.addEventListener('DOMContentLoaded', function () {
var headerOffset = window.innerWidth <= 767 ? 80 : 50;
var selector = '.toc_wrapper a, .privacy-table-contents_wrapper a, .page-fv-link_ul a';
function smoothScroll(targetElement) {
var topPosition = targetElement.getBoundingClientRect().top + window.scrollY - headerOffset;
window.scrollTo({
top: topPosition,
behavior: 'smooth'
});
// スクロール完了後に位置を微調整
var scrollEndTimer;
function checkScrollEnd() {
if (window.scrollY === topPosition) {
clearInterval(scrollEndTimer);
// 最終的な位置を再計算して微調整
var finalPosition = targetElement.getBoundingClientRect().top + window.scrollY - headerOffset;
if (Math.abs(finalPosition - topPosition) > 1) {
window.scrollTo(0, finalPosition);
}
}
}
scrollEndTimer = setInterval(checkScrollEnd, 100);
}
document.querySelectorAll(selector).forEach(function (link) {
link.addEventListener('click', function (e) {
e.preventDefault();
var targetId = link.getAttribute('href').substring(1);
var targetElement = document.getElementById(targetId);
if (targetElement) {
smoothScroll(targetElement);
}
});
});
// URLハッシュに基づくスクロール処理
if (window.location.hash) {
var targetElement = document.querySelector(window.location.hash);
if (targetElement) {
window.addEventListener('load', function() {
setTimeout(function() {
smoothScroll(targetElement);
}, 0);
});
}
}
// ウィンドウリサイズ時のヘッダーオフセット更新
window.addEventListener('resize', function() {
headerOffset = window.innerWidth <= 767 ? 80 : 50;
});
});
コード4を使う時の注意
8行目と58行目のヘッダーオフセットの数値は、ご自分のケースに合わせて変更して下さい。
8行目
var headerOffset = window.innerWidth <= 767 ? 80 : 50;
- 767:スマートフォン表示に設定したブレークポイント
- 80:スマートフォンのヘッダーオフセット量(移動先セクションアンカーの上部余白)
- 50:スマートフォンを除くデバイス画面のヘッダーオフセット量
58行目
// ウィンドウリサイズ時のヘッダーオフセット更新
window.addEventListener('resize', function() {
headerOffset = window.innerWidth <= 767 ? 80 : 50;
});
8行目のコードの数値と同じにする。