【WordPress】目次を自動生成するには

最終更新日: 公開日: 2021年06月

ホームページのページには目次を設置することをお勧めする.
このページでは,Table of Contents Plus (TOC+) というプラグインを使わないで目次を自動生成する方法を説明する.

プラグインを利用した目次作成(まえおき)

WordPress を使う場合には Table of Contents Plus という便利な目次自動作成プラグインがある.


Table of Contents Plus 設定画面

プラグインは何でも入れればよいというものではない,あまり入れすぎると更新が面倒になるし,セキュリティの不安も増大する.
そこで出来るだけプラグインは最小限にするべきである.

と書いているが,今までは TOC+ を使っていた.

プラグインを更新したらレイアウトが崩れた

今回,この Table of Contents Plus のバージョンアップが来ていたので更新したところ,目次を押して見出しにジャンプするが,その位置がずれるようになってしまった.
このように書くとこのプラグインが悪いのかと思われるが,悪いのはこちらである.

実は上部固定メニューを使っているために,目次をクリックして飛んでいくとずれてしまう現象があり,そのようなことは TOC+ も承知で設定でそのような場合にずらすための項目がある.

プラグインを直接書き替えるのはよくない

にもかかわらず,弊社のサイトではなぜか機能しなかったので,直接プラグインファイルを書き替えて対応していたのだった.
これは真似しないようにしていただきたい.

WordPress 本体もそうだが,プラグインも基本的に触ってはいけない.バージョンアップ時にこのように消える可能性があるからだ.

結局,本当にかゆいところに手が届くようにはなっていないので,自分で作るしかないかと軽い気持ちで検索してみるといきなり見つかった.

本当にありがとうございます!

WordPress プラグインなしで記事の見出しから目次を作成

まさに目次をプラグインを使わず,functions.php に追加することで実現できる.

プラグインを使わずに目次を自動作成

このプログラムをそのままありがたく使わせていただこうと思ったが,弊社のページには行儀の悪いページがあり,h2 の次に h4 が来ているページで番号の表示がおかしくなってしまった.
またもや,こちらが悪いので申し訳ないなと思いながら修正を行うことにした.
ついでに上部固定メニューの場合にずらす方法を含めて,最後に紹介することにする.

ページには目次を作るのがいい

目次は人の為ならず

目次を作るのはみる人の為だけではない,自分にも返ってくる.

ホームページのページの長さはあまり長くても短くてもよくない.

目安として目次を作れるぐらいの量は書いたほうが良い.

目次を作ると何がよいかというと,書きながらテーマを考えられる.

ダラダラ書いたとしても,目次を作るために見出しを作れば,自然に何を言いたかったのが自分でも明確になっていく.

記事を投稿すると自分の考えをまとめることが出来るが,目次はそれをさらに促進してくれる.

何が書いてあるか,一目瞭然

何も目次がないとタイトルや大見出しだけで内容を判断することになる.

本当は知りたいことが書いてあるのに,横のスクロールバーの長さと最初の文の面白くなさに読むのをやめてしまうことがある.

目次があればある程度細かいことまで何をいおうとしているかが分かる.

つまり,読む人がよみ始めてもらうために重要なのである.

目次を自分で作るのは結構大変

目次を静的に自分で作ったとして,途中で見出しの文面を変えることがあるが,その場合,目次も替える必要が出てくる.
TeX も目次は自動生成してくれるが,せっかくコンピュータで文書を書いているのだから,目次ぐらい自動化してほしい.
自動化されれば,見出しの変更に応じて目次も自動で替わってくれるので手間が省ける.

目次自動作成スクリプト

繰り返すが,元記事は下のURLですので,そちらを参照してください.

WordPress プラグインなしで記事の見出しから目次を作成

h2 から h3 を作らずに h4 にいったり,上部固定メニューを使った場合の改造したい場合だけ,参考にしてほしい.

88~89行目を追加.(h2 から h4 に飛んだときに 0 という数字(h3)が表れるので削除)
145~147行目を追加.(上部固定メニューのためのアンカー)

また,私のブラウザで「戻る」が効かなくなったので,スムーズスクロール関連のコードは削除している.

(なお,73, 83行目の改造は間違っていたので元に戻している.(※2021/08/20追記))

PHP と javascript のソースコード

これを functions.php に挿入すればよい.

(2021/07/27 数字の後ろにピリオドを追加)

class Toc_Shortcode {

  private $add_script = false;
  private $atts = array();

  public function __construct() {
    add_shortcode( 'toc', array( $this, 'shortcode_content' ) );
    add_action( 'wp_footer', array( $this, 'add_script' ), 999999 );
    add_filter( 'the_content', array( $this, 'change_content' ), 9 );
  }

  function change_content( $content ) {
    return "<div id=\"toc_content\">{$content}</div>";
  }

  public function shortcode_content( $atts ) {
    global $post;

    if ( ! isset( $post ) )
      return '';

    $this->atts = shortcode_atts( array(
      'id' => '',
      'class' => 'toc',
      'title' => '目次',
      'toggle' => true,
      'opentext' => '開く',
      'closetext' => '閉じる',
      'close' => false,
      'showcount' => 2,
      'depth' => 0,
      'toplevel' => 2,
      ), $atts );

    $this->atts['toggle'] = ( false !== $this->atts['toggle'] && 'false' !== $this->atts['toggle'] ) ? true : false;
    $this->atts['close'] = ( false !== $this->atts['close'] && 'false' !== $this->atts['close'] ) ? true : false;

    $content = $post->post_content;

    $headers = array();
    preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/us', $content, $headers );
    $header_count = count( $headers[0] );
    $counter = 0;
    $counters = array( 0, 0, 0, 0, 0, 0 );
    $current_depth = 0;
    $prev_depth = 0;
    $top_level = intval( $this->atts['toplevel'] );
    if ( $top_level < 1 ) $top_level = 1;
    if ( $top_level > 6 ) $top_level = 6;
    $this->atts['toplevel'] = $top_level;

    // 表示する階層数
    $max_depth = ( ( $this->atts['depth'] == 0 ) ? 6 : intval( $this->atts['depth'] ) );

    $toc_list = '';
    for ( $i = 0; $i < $header_count; $i++ ) {
      $depth = 0;
      switch ( strtolower( $headers[1][$i] ) ) {
        case 'h1': $depth = 1 - $top_level + 1; break;
        case 'h2': $depth = 2 - $top_level + 1; break;
        case 'h3': $depth = 3 - $top_level + 1; break;
        case 'h4': $depth = 4 - $top_level + 1; break;
        case 'h5': $depth = 5 - $top_level + 1; break;
        case 'h6': $depth = 6 - $top_level + 1; break;
      }
      if ( $depth >= 1 && $depth <= $max_depth ) {
        if ( $current_depth == $depth ) {
          $toc_list .= '</li>';
        }
        while ( $current_depth > $depth ) {
          $toc_list .= '</li></ul>';
          $current_depth--;
          $counters[$current_depth] = 0;
        }
        if ( $current_depth != $prev_depth ) {
          $toc_list .= '</li>';
        }
        if ( $current_depth < $depth ) {
          $class = $current_depth == 0 ? ' class="toc-list"' : '';
          $style = $current_depth == 0 && $this->atts['close'] ? ' style="display: none;"' : '';
          $toc_list .= "<ul{$class}{$style}>";
          $current_depth++;
        }
        $counters[$current_depth - 1]++;
        $number = $counters[0] . '.';
        for ( $j = 1; $j < $current_depth; $j++ ) {
          // 順番を飛ばしている場合に対応.h2 -> h4 など.
          if ($counters[$j] == 0) { continue; }
          $number .=  $counters[$j] . '.';
        }
        $counter++;
        $toc_list .= '<li><a href="#toc' . ($i + 1) . '"><span class="contentstable-number">' . $number . '</span> ' . $headers[2][$i] . '</a>';
        $prev_depth = $depth;
      }
    }
    while ( $current_depth >= 1 ) {
      $toc_list .= '</li></ul>';
      $current_depth--;
    }

    $html = '';
    if ( $counter >= $this->atts['showcount'] ) {
      $this->add_script = true;

      $toggle = '';
      if ( $this->atts['toggle'] ) {
        $toggle = ' <span class="toc-toggle">[<a class="internal" href="javascript:void(0);">' . ( $this->atts['close'] ? $this->atts['opentext'] : $this->atts['closetext'] ) . '</a>]</span>';
      }

      $html .= '<div' . ( $this->atts['id'] != '' ? ' id="' . $this->atts['id'] . '"' : '' ) . ' class="' . $this->atts['class'] . '">';
      $html .= '<p class="toc-title">' . $this->atts['title'] . $toggle . '</p>';
      $html .= $toc_list;
      $html .= '</div>' . "\n";
    }

    return $html;
  }

  public function add_script() {
    if ( ! $this->add_script ) {
      return false;
    }

    $var = wp_json_encode( array(
      'open_text' => isset( $this->atts['opentext'] ) ? $this->atts['opentext'] : '開く',
      'close_text' => isset( $this->atts['closetext'] ) ? $this->atts['closetext'] : '閉じる',
      ) );
?>
<script type="text/javascript">
var xo_toc = <?php echo $var; ?>;
let xoToc = () => {
  const entryContent = document.getElementById('toc_content');
  if (!entryContent) {
    return false;
  }

  /*
   * ヘッダータグに ID を付与
   */
  const headers = entryContent.querySelectorAll('h1, h2, h3, h4, h5, h6');
  for (let i = 0; i < headers.length; i++) {
    headers[i].setAttribute('id', 'toc' + (i + 1));
    var cls = headers[i].getAttribute('class');
    if (cls == null) { cls = ''; }
    headers[i].setAttribute('class', cls + ' anchor');
  }

  /*
   * 目次項目の開閉
   */
  const tocs = document.getElementsByClassName('toc');
  for (let i = 0; i < tocs.length; i++) {
    const toggle = tocs[i].getElementsByClassName('toc-toggle')[0].getElementsByTagName('a')[0];
    toggle.addEventListener('click', function (e) {
      const target = e.currentTarget;
      const tocList = tocs[i].getElementsByClassName('toc-list')[0];
      if (tocList.hidden) {
        target.innerText = xo_toc['close_text'];
      } else {
        target.innerText = xo_toc['open_text'];
      }
      tocList.hidden = !tocList.hidden;
    });
  }
};
xoToc();
</script>
<?php
  }
}
new Toc_Shortcode();

Hタグに margin padding が既に指定してある場合

また,Hタグに装飾をしていて margin とか padding を指定している場合は Hタグに直接,class や id の指定をやめたほうが無難である.
そういう場合は ID を付与する部分を以下のコードと差し替える.

  /*
   * ヘッダータグに ID を付与
   */
  const headers = entryContent.querySelectorAll('h1, h2, h3, h4, h5, h6');
  for (let i = 0; i < headers.length; i++) {
    let span = document.createElement('span');
    span.setAttribute('class', 'anchor');
    span.setAttribute('id', 'toc' + (i + 1));
    span.innerHTML = '&nbsp;';
    headers[i].before(span);
  }

自動で目次を表示させたい場合

自動で目次を表示させたい場合は以下のコードも追加する.
これは参考にしたページと一緒だが,2個以上見出しがあった時に目次作成するように変更している.

function add_toc_content( $content ) {
  if (is_single()) {
    $shortcode = '[toc showcount="2"]';
    $pattern = '/<h2.*?>/i';
    if ( preg_match( $pattern, $content, $matches ) ) {
      $content = preg_replace( $pattern, $shortcode . $matches[0], $content, 1 );
    }
  }
  return $content;
}
add_filter('the_content', 'add_toc_content', 10);

スタイルシートのコード

スタイルシートもほぼ同じだが,ちょっとすき間を広げたりしている.
最後の anchor の数字は上部メニューの高さに合わせて変更する.

.toc {
  width: auto;
  display: table;
  margin: 20px;
  padding: 10px;
  color: #333;
  word-break: break-all;
  word-wrap: break-word;
  border: #ccc solid 1px;
  border-radius: 3px;
  background-color: #fafafa;
}
.toc .toc-title {
  margin: 0;
  padding: 0;
  text-align: center;
  font-weight: bold;
}
.toc .toc-toggle {
  font-weight: normal;
  font-size: 90%;
}
.toc ul, .toc ul li {
  background: 0 0;
  list-style-type: none;
  list-style: none;
  margin: 0;
  padding: 0;
}
.toc .toc-list {
  margin: 0;
  padding: 0;
}
.toc ul ul {
  margin-left: 1.5em;
}
.toc a {
  text-decoration: none;
  text-shadow: none;
}
.anchor {
  padding-top: 110px;
  margin-top: -110px;
}
 

Contact

ご質問等ありましたら,お手数ですが弊社の個人情報保護方針をお読み頂いた上でフォームからお願い致します.

 
   
Chrome 拡張機能 PageSpeed Insights SEO WordPress Youtube お問い合わせフォーム アナリティクス アンケートフォーム サーチコンソール セミナー タグマネージャー データベース ヒアリング プラグイン ページ閲覧解析 マーケティング メール配信 リッチリザルト レスポンシブ対応 ログ解析 勉強会 商品企画 営業 営業力 営業支援システム 実装 検索順位 検索順位チェックツール 追跡 開発者ツール
contact
Pagetop