目次ブロックをJavascriptで自動生成する
WordPressのコアブロックには目次ブロックがあるのですが、まだexperimental(実験段階)でした。
プラグインを使うよりもJavascriptでDOMに自動挿入される方が便利なので、実装してみました。
目次ブロックを差し込む場所を作る
サイドバーのカテゴリー欄の下にdivタグで差し込む場所を作ります。
PC /wp-content/themes/mytemplate/sidebar.php
<aside class="l-sidebar">
<div class="l-sidebar__inner">
<?php if ( is_active_sidebar( 'sidebar' ) ) : ?>
<?php dynamic_sidebar( 'sidebar' ); ?>
<?php endif; ?>
<div id="js-toc-block" class="c-toc">
<!-- この中に目次ブロックを差し込む -->
</div>
</div>
</aside>
モバイルではサイドバーはハンバーガーメニューの中に入れているため、もう一箇所場所を作ります。
SP /wp-content/themes/mytemplate/header.php
<header class="l-header">
<div class="l-header__logo">
…省略
</div>
<div class="l-header__nav">
…省略
<div class="l-header__inner">
<?php if ( is_active_sidebar( 'sidebar' ) ) : ?>
<?php dynamic_sidebar( 'sidebar' ); ?>
<?php endif; ?>
<div id="js-toc-block--sp" class="c-toc">
<!-- この中に目次ブロックを差し込む -->
</div>
…省略
</div>
</div>
</header>
目次ブロックを生成
このスクリプトでは、以下の機能を実装しています。
- 投稿の見出しにidとclassを自動で付与
- 目次ブロックの枠を作成
- 目次ブロックの枠内にHTMLタグのaタグで囲まれた見出しのタイトルを挿入
- PCではサイドバー、SPではハンバーガーメニューの中に目次ブロックを挿入
/wp-content/themes/mytemplate/assets/js/lib/toc.js
// divにid=“js-anchor0(1,2,3…)” class=“anchor”を付与する関数
function createAnchor(id) {
const tocLinks = document.createElement("div");
tocLinks.setAttribute("id", `js-anchor${id}`);
tocLinks.setAttribute("class", "anchor");
return tocLinks;
}
// 目次ブロックの枠を生成する関数
function createTOCBlock() {
const tocblock = document.createElement("div");
tocblock.className = "c-toc__inner";
const tocHeader = document.createElement("h3");
tocHeader.className = "c-toc__header";
tocHeader.innerText = "記事の目次";
tocblock.appendChild(tocHeader);
return tocblock;
}
// 目次ブロック枠内に、アンカーリンクをつけた見出しリストを差し込む関数
function generateTOC() {
const headings = document.querySelectorAll(
".post__content h2, .post__content h3"
);
const createTocArea = document.createElement("ol");
createTocArea.className = "c-toc__lists";
let lastH2 = null; // 直近の見出しを追跡する目印
for (let i = 0; i < headings.length; i++) {
const heading = headings[i];
// 画面幅を変えた時に、見出しのアンカーリンクが重複して生成されるのを防ぐ
const existingAnchor = heading.querySelector(".anchor");
if (existingAnchor) {
existingAnchor.remove();
}
if (heading.tagName === "H2") {
const tocTitle = document.createElement("li");
tocTitle.className = "c-toc__title--h2";
tocTitle.innerHTML = `<a class="c-toc__link" href="#js-anchor${i}">${heading.innerText}</a>`;
createTocArea.appendChild(tocTitle);
lastH2 = tocTitle; // h2見出しをlastH2に代入
} else if (heading.tagName === "H3" && lastH2) { // h3見出しであり、見出し2(lastH2)があったら
const tocSubTitle = document.createElement("li");
tocSubTitle.className = "c-toc__title--h3";
tocSubTitle.innerHTML = `<a class="c-toc__link" href="#js-anchor${i}">${heading.innerText}</a>`;
let nestedOl = lastH2.querySelector("ol");// 見出し2の直下に見出し3リストを挿入
if (!nestedOl) {
nestedOl = document.createElement("ol");
lastH2.appendChild(nestedOl);
}
nestedOl.appendChild(tocSubTitle);
}
// 投稿の見出しの直下にアンカーidを挿入
heading.appendChild(createAnchor(i));
}
const tocblock = createTOCBlock();
tocblock.appendChild(createTocArea);
return tocblock;
}
// 完成した目次ブロックを、PCとモバイルで差し込む位置を切り替える関数
function insertTOC() {
const tocblock = generateTOC();
const isMobile = window.innerWidth <= 768;
const existingTOCPC = document.getElementById("js-toc-block");
const existingTOCSP = document.getElementById("js-toc-block--sp");
if (existingTOCPC) {
existingTOCPC.innerHTML = ""; //PC用目次ブロックをクリアにし初期化
}
if (existingTOCSP) {
existingTOCSP.innerHTML = ""; //モバイル用目次ブロックをクリアにし初期化
}
if (isMobile) {
if (existingTOCSP) {
existingTOCSP.appendChild(tocblock);
}
} else {
if (existingTOCPC) {
existingTOCPC.appendChild(tocblock);
}
}
}
//insertTOC関数を発火するタイミングを設定
document.addEventListener("DOMContentLoaded", insertTOC);
window.addEventListener("resize", () => {
setTimeout(insertTOC, 100); // 100ms後にinsertTOCを呼び出す
});
スクリプトとCSSを読み込む
functions.phpから、共通で読み込むものと、投稿ページのみ、その他特定のページのみで読み込むものとで出し分ける処理をします。
/wp-content/themes/mytemplate/functions.php
/**
* CSS,JSを読み込ませる
*/
function mytemplate_enqueue_scripts() {
// WordPressに同梱されているjQueryを明示的に呼び出す
wp_enqueue_script('jquery');
// jQueryに依存しているものがあるときのテーマ独自のJavaScriptファイルを読み込み
// true:スクリプトをfooterで読み込む
wp_enqueue_script('mytemplate', get_template_directory_uri() . '/assets/js/main.js', array('jquery'), null, true);
// 外部スタイルシートの読み込み
wp_enqueue_style('font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css');
wp_enqueue_style('google-web-fonts', 'https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');
// テーマ独自のCSSの読み込み
// ハンドル名app-styleで二重読み込み防止と順序をコントロール
wp_enqueue_style('app-style', get_template_directory_uri() . '/assets/css/app.css');
// 投稿ページのみの読み込み
if (is_single()) {
wp_enqueue_script('toc-script', get_template_directory_uri() . '/assets/js/lib/toc.js', array(), null, true);
}
// トップページのみの読み込み
if (is_home()) {
…省略
}
}
add_action('wp_enqueue_scripts', 'mytemplate_enqueue_scripts');
目次ブロックのスタイルは、ご自分の開発環境で読み込んでいるcssファイルに記述し、読み込んでください。
これで、PCとモバイルで目次ブロックの場所を出しわけ、DOMには目次ブロックが一つだけ生成されるようになりました。

