Mill's Note

【Wordpress Tips】ブロックエディタ用 目次の作り方/修正版

/

投稿ページは長くなりやすいし、目次があると何が書かれているか一目で分かってとても便利^^

今回は相当悩みましたが、parse_blocks() を利用して「ブロックエディタ対応」「リストがきれいにネストする」「カラム内の見出し( innerBlocks) も対応」バージョンが出来ました~ \(^o^)/

ブロックエディタになって以前作成した目次は使えないし、ネットにあったコードは、カラム内の見出しを抜き出せなかったり、リストのネストがおかしかったり、で、しばし休止。。。

リストのネストを直せばいいかな~、程度に考えていたのですが。。。

innerBlock は配列だから独立した見出しと一緒に処理できないとか、add_filter( 'render_block' ~~)にしてみたら目次がおかしくなるとか、あっちの沼こっちの沼に潜りまくり _ _;;

気になっていたところを潰していったら、こんなコードに。
もっと良いやり方があるかも ? ですが、一応これで完成にします ^^;;

目次の作り方 コード

PHP
<?php /* functions.php に直接書き込む場合はこの行を削除 */
function generate_my_toc($content) {
  if ( !is_single() || !in_the_loop() || !is_main_query() ) return $content;

  $toc = '';
  $pv_level = 0;
  $i = 0;
  $blocks = parse_blocks($content);

  function process_blocks($blocks, &$toc, &$content, &$i, &$pv_level) {
    foreach ($blocks as $block) {

      if ($block['blockName'] == 'core/heading') {
        if ( preg_match('/<h([1-6]).*?>(.*?)<\/h\\1>/', $block['innerHTML'], $m) ) {
          
          $level = intval($m[1]);

          /* IDを見出しに追加する */
          $content = preg_replace_callback(
            "/(<h$level.*?>)(" . preg_quote($m[2], '/') . ")(<\/h$level>)/",
            function($m2) use (&$i) {
              return $m2[1] . '<span id="toc-' . $i . '">' . $m2[2] . '</span>' . $m2[3];
            },
            $content
          );
          
          /* 目次を登録 */
          if ($pv_level == 0) { $toc .=  ''; }
          elseif ($level == $pv_level) { $toc .= '</li>'; }
          elseif ($level > $pv_level)  { $toc .= PHP_EOL . '<ul>'; }
          elseif ($level < $pv_level)  { 
            $toc .= '</li>' . PHP_EOL;
            for ($count = 0; $count < ($pv_level - $level); $count++) {
              $toc .= '</ul>' . PHP_EOL . '</li>';
            }
          }
          $pv_level = $level;

          $toc .= PHP_EOL . '<li><a href="#toc-' . $i . '">' . strip_tags($m[2]) . '</a>';
          $i++;
        }
      }

      /* innerBlocksが存在する場合、再帰的に処理 */
      if ( !empty($block['innerBlocks']) ) {
        process_blocks($block['innerBlocks'], $toc, $content, $i, $pv_level);
      }
    }
  }

  /* ブロックを処理 */
  process_blocks($blocks, $toc, $content, $i, $pv_level);

  /* 目次書き出し */
  if ($toc) {
    $toc_html = '
<nav class="my-toc">
  <p class="screen-reader-text">ページの目次</p>
  <ul class="toc_list">' . $toc . '</li>
  </ul>
</nav>' . PHP_EOL;

    $content = preg_replace('/<h2.*?>/i', $toc_html . '$0', $content, 1);
  }
  return $content;
}
add_filter('the_content', 'generate_my_toc', 1);

目次コードの説明

設置方法

上のコードをfunctions.phpに書き込むか、pots-toc.phpとして保存し functions.php で読み込ませます。
( 別ファイルにする場合は、最初の <?php をお忘れなくm(__)m )

固定ページやカスタム投稿にも表示させたい場合は、コード最初にある

if ( !is_single() || !in_the_loop() || !is_main_query() ) return $content;

!is_single()!is_singular() に書き換えます

全体の流れ

大雑把ですが、以下のような流れで処理しています。

  1. parse_blocks()でブロックを取得。
  2. foreach で回して、独立した 'core/heading' を処理
    • $levelに現在の hタグの 数字部分 (h2 → 2 とか) を代入
    • IDを見出しに追加 → hタグにidを入れている場合を考え、idを入れた<span>を挿入
    • 目次を登録$level(処理中の行)、$pv_level (前の行) を比較してネスト用のタグを設定
    • $level (現在の数字) を$pv_level に代入
    • 目次の行を完成
  3. 再帰的に処理innerBlocks に対して同様の処理を行う
  4. ブロックを処理 → 2. 3. をまとめて処理
  5. 目次書き出し → 目次を整形して、最初の h2 上に書き出す

WordPressが用意してくれている'render_block'でやると「最初のh2の上に置く」が上手く働かない為、'the_content' で処理する形にしています。