目次ブロックを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には目次ブロックが一つだけ生成されるようになりました。

PCでの表示
モバイルでの表示

PAGE TOP