ねかつちう。

お金や節約に敏感な20代30代のための物欲系雑記ブログ

Wordpressで特定の見出しだけの目次を作成するサンプルコード

[PR]このページにはアフィリエイトプログラムによる商品・サービス等の広告を掲載しています

Wordpressで「特定の見出し」の目次を作成する方法を備忘録を兼ねてシェアします。

目次というのは基本的に、記事ページ内に含まれる全てのHタグを抽出しますが、特定のHタグのみ抽出して目次を作成することができます。

使いどころとしては、あるH2タグの傘下の中だけで小目次を作りたい場合や、誘導用のピックアップ目次を作りたい場合に有効かと思います。

なお、以下で紹介するコードは、Xakuro Blogさんで公開されているものをベースに一部改変したものとなります。こちらのオリジナルコードの強みは、jQueryを一切使っていないためWordpressで設置しやすいところでしょうね。

サンプルコード

ではまず、サンプルコードから。

こちらのコードをfunction.phpに貼り付けます。オリジナルのコードと異なる箇所を赤字にしています。説明はのちほど。

  1. class Toc_Shortcode {
  2.     private $add_script = false;
  3.     private $atts = array();
  4.     public function __construct() {
  5.         add_shortcode( 'itoc', array( $this, 'shortcode_content' ) );
  6.         add_action( 'wp_footer', array( $this, 'add_script' ), 999999 );
  7.         add_filter( 'the_content', array( $this, 'change_content' ), 9 );
  8.     }
  9.     function change_content( $content ) {
  10.         return "<div id=\"toc_content\">{$content}</div>";
  11.     }
  12.     public function shortcode_content( $atts ) {
  13.         global $post;
  14.         if ( ! isset( $post ) )
  15.             return '';
  16.         $this->atts = shortcode_atts( array(
  17.             'id' => '',
  18.             'class' => 'toc',
  19.             'title' => '目次',
  20.             'toggle' => true,
  21.             'opentext' => '開く',
  22.             'closetext' => '閉じる',
  23.             'close' => false,
  24.             'showcount' => 2,
  25.             'depth' => 0,
  26.             'toplevel' => 2,
  27.             'scroll' => 'smooth',
  28.             'mode' => '0',
  29.         ), $atts );
  30.         $this->atts['toggle'] = ( false !== $this->atts['toggle'] && 'false' !== $this->atts['toggle'] ) ? true : false;
  31.         $this->atts['close'] = ( false !== $this->atts['close'] && 'false' !== $this->atts['close'] ) ? true : false;
  32.         $content = $post->post_content;
  33.         $headers = array();
  34.         if($this->atts['mode'] == '1'){
  35.             preg_match_all( '/<([hH][1-6]).*?itoc1.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  36.         }
  37.         elseif($this->atts['mode'] == '2'){
  38.             preg_match_all( '/<([hH][1-6]).*?itoc2.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  39.         }
  40.         elseif($this->atts['mode'] == '3'){
  41.             preg_match_all( '/<([hH][1-6]).*?itoc3.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  42.         }
  43.         else {
  44.             preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  45.         }
  46.         $header_count = count( $headers[0] );
  47.         $counter = 0;
  48.         $counters = array( 0, 0, 0, 0, 0, 0 );
  49.         $current_depth = 0;
  50.         $prev_depth = 0;
  51.         $top_level = intval( $this->atts['toplevel'] );
  52.         if ( $top_level < 1 ) $top_level = 1;
  53.         if ( $top_level > 6 ) $top_level = 6;
  54.         $this->atts['toplevel'] = $top_level;
  55.         // 表示する階層数
  56.         $max_depth = ( ( $this->atts['depth'] == 0 ) ? 6 : intval( $this->atts['depth'] ) );
  57.         $toc_list = '';
  58.         for ( $i = 0; $i < $header_count; $i++ ) {
  59.             $depth = 0;
  60.             switch ( strtolower( $headers[1][$i] ) ) {
  61.                 case 'h1': $depth = 1 - $top_level + 1; break;
  62.                 case 'h2': $depth = 2 - $top_level + 1; break;
  63.                 case 'h3': $depth = 3 - $top_level + 1; break;
  64.                 case 'h4': $depth = 4 - $top_level + 1; break;
  65.                 case 'h5': $depth = 5 - $top_level + 1; break;
  66.                 case 'h6': $depth = 6 - $top_level + 1; break;
  67.             }
  68.             if ( $depth >= 1 && $depth <= $max_depth ) {
  69.                 if ( $current_depth == $depth ) {
  70.                     $toc_list .= '</li>';
  71.                 }
  72.                 while ( $current_depth > $depth ) {
  73.                     $toc_list .= '</li></ul>';
  74.                     $current_depth--;
  75.                     $counters[$current_depth] = 0;
  76.                 }
  77.                 if ( $current_depth != $prev_depth ) {
  78.                     $toc_list .= '</li>';
  79.                 }
  80.                 if ( $current_depth < $depth ) {
  81.                     $class = $current_depth == 0 ? ' class="toc-list"' : '';
  82.                     $style = $current_depth == 0 && $this->atts['close'] ? ' style="display: none;"' : '';
  83.                     $toc_list .= "<ul{$class}{$style}>";
  84.                     $current_depth++;
  85.                 }
  86.                 $counters[$current_depth - 1]++;
  87.                 $number = $counters[0];
  88.                 for ( $j = 1; $j < $current_depth; $j++ ) {
  89.                     $number .= '.' . $counters[$j];
  90.                 }
  91.                 $counter++;
  92.                 $toc_list .= '<li><a href="#toc' . ($i + 1) . '"><span class="contentstable-number">' . $number . '</span> ' . $headers[2][$i] . '</a>';
  93.                 $prev_depth = $depth;
  94.             }
  95.         }
  96.         while ( $current_depth >= 1 ) {
  97.             $toc_list .= '</li></ul>';
  98.             $current_depth--;
  99.         }
  100.         $html = '';
  101.         if ( $counter >= $this->atts['showcount'] ) {
  102.             $this->add_script = true;
  103.             $toggle = '';
  104.             if ( $this->atts['toggle'] ) {
  105.                 $toggle = ' <span class="toc-toggle">[<a class="internal" href="javascript:void(0);">' . ( $this->atts['close'] ? $this->atts['opentext'] : $this->atts['closetext'] ) . '</a>]</span>';
  106.             }
  107.             $html .= '<div' . ( $this->atts['id'] != '' ? ' id="' . $this->atts['id'] . '"' : '' ) . ' class="' . $this->atts['class'] . '">';
  108.             $html .= '<p class="toc-title">' . $this->atts['title'] . $toggle . '</p>';
  109.             $html .= $toc_list;
  110.             $html .= '</div>' . "\n";
  111.         }
  112.         return $html;
  113.     }
  114.     public function add_script() {
  115.         if ( ! $this->add_script ) {
  116.             return false;
  117.         }
  118.         $var = wp_json_encode( array(
  119.             'open_text' => isset( $this->atts['opentext'] ) ? $this->atts['opentext'] : '開く',
  120.             'close_text' => isset( $this->atts['closetext'] ) ? $this->atts['closetext'] : '閉じる',
  121.             'scroll' => isset( $this->atts['scroll'] ) ? $this->atts['scroll'] : 'smooth',
  122.         ) );
  123.         ?>
  124. <script type="text/javascript">
  125. var xo_toc = <?php echo $var; ?>;
  126. let xoToc = () => {
  127.   const entryContent = document.getElementById('toc_content');
  128.   if (!entryContent) {
  129.     return false;
  130.   }
  131.   /**
  132.    * スムーズスクロール関数
  133.    */
  134.   let smoothScroll = (target, offset) => {
  135.     const targetRect = target.getBoundingClientRect();
  136.     const targetY = targetRect.top + window.pageYOffset - offset;
  137.     window.scrollTo({left: 0, top: targetY, behavior: xo_toc['scroll']});
  138.   };
  139.   /**
  140.    * アンカータグにイベントを登録
  141.    */
  142.   const wpadminbar = document.getElementById('wpadminbar');
  143.   const smoothOffset = (wpadminbar ? wpadminbar.clientHeight : 0) + 2;
  144.   const links = document.querySelectorAll('.toc-list a[href*="#"]');
  145.   for (let i = 0; i < links.length; i++) {
  146.     links[i].addEventListener('click', function (e) {
  147.       const href = e.currentTarget.getAttribute('href');
  148.       const splitHref = href.split('#');
  149.       const targetID = splitHref[1];
  150.       const target = document.getElementById(targetID);
  151.       if (target) {
  152.         e.preventDefault();
  153.         smoothScroll(target, smoothOffset);
  154.       } else {
  155.         return true;
  156.       }
  157.       return false;
  158.     });
  159.   }
  160.   /**
  161.    * ヘッダータグに ID を付与
  162.    */
  163.   const headers = entryContent.querySelectorAll('h1, h2, h3, h4, h5, h6');
  164.   for (let i = 0; i < headers.length; i++) {
  165.     headers[i].setAttribute('id', 'toc' + (i + 1));
  166.   }
  167.   /**
  168.    * 目次項目の開閉
  169.    */
  170.   const tocs = document.getElementsByClassName('toc');
  171.   for (let i = 0; i < tocs.length; i++) {
  172.     const toggle = tocs[i].getElementsByClassName('toc-toggle')[0].getElementsByTagName('a')[0];
  173.     toggle.addEventListener('click', function (e) {
  174.       const target = e.currentTarget;
  175.       const tocList = tocs[i].getElementsByClassName('toc-list')[0];
  176.       if (tocList.hidden) {
  177.         target.innerText = xo_toc['close_text'];
  178.       } else {
  179.         target.innerText = xo_toc['open_text'];
  180.       }
  181.       tocList.hidden = !tocList.hidden;
  182.     });
  183.   }
  184. };
  185. xoToc();
  186. </script>
  187.         <?php
  188.     }
  189. }
  190. new Toc_Shortcode();

以下、オリジナルコードからの変更箇所について説明します。

5行目:ショートコード名を変更

オリジナルではショートコード名が[toc]となっており、これは「table of contents(目次)」の略なので、いかにも他とバッティングしそうですよね。ここでは、ショートコード名を[itoc]に変更しています。

  1. add_shortcode( 'itoc', array( $this, 'shortcode_content' ) );

28行目:パラメータ「mode」を追加

ショートコードのパラメータを追加します。[itoc mode=1]のようにパラメータの数字を指定することで、目次に抽出する見出しの範囲を変更します。詳しくは後ほど。

  1. 'mode' => '0',

34行目以降:条件分岐を追加

オリジナルでは記事本文から全てのHタグを拾ってくるプログラムですが、特定のクラス(ここでは「itoc1~3」)が付いたHタグを拾えるようにしています。

さらに、前出の追加パラメータ「mode」の数字により条件分岐させ、mode=1ならitoc1の付いた見出しだけを、mode=2ならitoc2の付いた見出しだけを拾えます。3つで足りなければ、条件分岐を増やしてください。なお、パラメータを設定しなければ、オリジナルと同じ挙動(全てのHタグを抽出)となります。

  1.         if($this->atts['mode'] == '1'){
  2.             preg_match_all( '/<([hH][1-6]).*?itoc1.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  3.         }
  4.         elseif($this->atts['mode'] == '2'){
  5.             preg_match_all( '/<([hH][1-6]).*?itoc2.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  6.         }
  7.         elseif($this->atts['mode'] == '3'){
  8.             preg_match_all( '/<([hH][1-6]).*?itoc3.*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  9.         }
  10.         else {
  11.             preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  12.         }

以上で作業終了です。CSSはXakuro Blogさんに掲載のものをお使いください。

試しに、Wordpressのエディタ画面に以下を入力すると、「表示します」だけの目次が作られると思います。

  1. [itoc mode=1]
  2. <h3 class="itoc1">表示します</h3>
  3. <h3 class="itoc1">表示します</h3>
  4. <h3 class="itoc1">表示します</h3>
  5. <h3 class="itoc2">表示しません!</h3>

手短ですが、以上となります。