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

WordPress で記事の見出しから目次を作成する方法を紹介します。

※ ちなみに、目次は英語で table of contents (【略】 TOC )です。

ググってみると既に多くのサイトで紹介されていますが、JavaScript (jQuery) による実装ばかりだったので、ここでは PHP による実装(目次を作成する部分)をしてみました。

PHP コード

functions.php などに下記のコードを記述します。

<?php

/**
 * 目次ショートコードです。
 *
 * @version 2.0.0
 */
class Toc_Shortcode {

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

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

	function enqueue_scripts() {
		if ( !wp_script_is( 'jquery', 'done' ) ) {
			wp_enqueue_script( 'jquery' );
		}
	}

	function change_content( $content ) {
		if ( has_shortcode( $content, 'toc' ) ) {
			$content = "<div id=\"toc_content\">{$content}</div>";
		}
		return $content;
	}

	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,
			'offset' => '',
			'duration' => 'normal'
		), $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].*?>/u', $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++ ) {
					$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;
		}

		$class = $this->atts['class'];
		$offset = is_numeric( $this->atts['offset'] ) ? (int)$this->atts['offset'] : - 1;
		$duration = is_numeric( $this->atts['duration'] ) ? (int)$this->atts['duration'] : '"' . $this->atts['duration'] . '"';
		$opentext = $this->atts['opentext'];
		$closetext = $this->atts['closetext'];
		?>
<script type="text/javascript">
(function ($) {
  var offset = <?php echo $offset; ?>;
  var idCounter = 0;
  $("#toc_content :header").each(function () {
    idCounter++;
    this.id = "toc" + idCounter;
  });
  $(".<?php echo $class; ?> a[href^='#']").click(function () {
    var href = $(this).attr("href");
    var target = $(href === "#" || href === "" ? "html" : href);
    var h = (offset === -1 ? $("#wpadminbar").height() + $(".navbar-fixed-top").height() : offset);
    var position = target.offset().top - h - 4;
    $("html, body").animate({scrollTop: position}, <?php echo $duration; ?>, "swing");
    return false;
  });
  $(".toc-toggle a").click(function () {
    var tocList = $(this).parents(".<?php echo $class; ?>").children(".toc-list");
    if (tocList.is(":hidden")) {
      tocList.show("fast");
      $(this).text("<?php echo $closetext; ?>");
    } else {
      tocList.hide("fast");
      $(this).text("<?php echo $opentext; ?>");
    }
  });
})(jQuery);
</script>
		<?php
	}

}

new Toc_Shortcode();

ショートコード

投稿または固定ページの表示したい場所に toc ショートコードを記述します。

[toc]

使用例:

[toc title="目次" depth="2"]

オプション

オプション デフォルト 説明
id “” ID です。
class “toc”

クラスです。

省略(”” を指定) することはできません。

title “目次” 目次のタイトルです。
toggle “true”

目次リストの開閉リンクを表示するかどうか。

表示する場合 “true”、表示しない場合は “false”。

opentext “開く” 開くリンクのテキストです。
closetext “閉じる” 閉じるリンクのテキストです。
close “false”

最初に目次リストの開閉リンクを閉じた状態にするかどうか。

閉じた状態の場合 “true”、開いた状態の場合は “false”。

showcount 2 目次を表示する見出し項目の数(以上)です。
depth 0

出力する階層数(階層レベルではありません)です。

0 の場合、すべての階層を表示します。

toplevel=”2″ depth=”4″ の場合、h2, h3, h4, h5 のヘッダーが対象となります。

toplevel 2 トップの階層のヘッダー(1 から 6)を指定します。
targetclass “entry-content”

目次を作成する対象のコンテンツのクラスを指定します。

※ バージョン 2.0.0 以降では機能しません。

offset “”

スクロール位置のオフセットです。

初期値は “” です。”” の場合は自動調整(*)となります。

* 通常は 0 ですが、上部固定エリア(管理バーやメニュー等)がある場合、その分の高さがセットされます。

duration “normal”

スクロール速度を指定します。

”slow”、”normal”、”fast”、もしくは完了までの時間をミリ秒単位で指定します。

スタイル(CSS)

スタイル(CSS)の例です。テーマの style.css 等に記述してください。

.toc {
    width: auto;
    display: table;
    margin: 0 0 10px;
    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 {
    list-style: none;
}
.toc .toc-list {
    margin: 0;
    padding: 0;
}

サンプルイメージ

toc

投稿に目次を自動挿入

下記コードは、全ての投稿に目次を自動挿入するサンプル コードになります。

このサンプル コードでは、全ての投稿(シングル)ページの h2 ヘッダー(見出し2)の直前に目次を挿入しています。

function add_toc_content( $content ) {
	if ( is_single() ) {

		$shortcode = '[toc showcount="4"]';

		$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 );

追記

2016年4月20日 jQuery バージョン 1.12 でエラーが発生する不具合を修正しました。

2017年3月9日 ヘッダーが順番に並んでいない場合に正しく目次を作成できない不具合を修正しました。

2017年3月12日 コードを改修しました(修正はなし)。念のため jQuery をエンキュー(組み込み)する処理を追加しました。

2017年7月7日 目次の開閉リンクをクリックすると、ページトップへスクロールする不具合を修正しました。

2017年11月23日 最初に目次の開閉リンクを閉じた状態にするためのオプションを追加しました。

2017年12月4日(バージョン1.0.0) 閉じた状態オプションを指定した場合に正しく目次を作成できない不具合を修正しました。

2017年12月5日(バージョン1.1.0) ページに目次を複数配置できるように変更しました。

2018年8月20日(バージョン2.0.0) 目次を作成する対象のコンテンツを自動取得するように変更しました。これに伴い、targetclass パラメータを削除しました。

コメント

  • 筥庭 蛙 より:

    石鷹さんが「WordPress プラグインなしで記事の見出しから目次を作成(https://xakuro.com/blog/wordpress/277)」で紹介されているPHPコードを使わせて頂いたのですが、どうしても目次の値がすべて「0」になってしまいます。目次の値を1,2,3…と順に並ぶようにしたいのですが、どうすればよいでしょうか。ご教授お願い致します。ちなみに、ショートコードは現在、[toc title=”目次” toplevel=”2″ depth=”2″]と入力しています(目次をh2とh3だけで作りたいから)。

    • 石鷹 より:

      すみません、コードに不具合がありました。
      コードを修正しましたので、もう一度お試しください。
      お手数をおかけして申し訳ございません。m(__)m

      • 筥庭 蛙 より:

        早速ご対応いただき誠にありがとうございました。ただ、先日と同じコード([toc title=”目次” toplevel=”2″ depth=”2″])を入力したところ、h2でしか目次が生成されず(下層のh3では目次が生成されない)、各目次リンクをクリックしても各見出しに移動しないという現象が起こりました。とくに急を要しているわけではないので、時間をかけてゆっくりと対処していただいてかまいません。こちらでもコードの不具合を修正できないか試みてみます。

    • 石鷹 より:

      度々すみません。指定した階層よりも実際の階層が深い場合に正しく動作しませんでした。
      とりあえず修正したもとをアップしましたが、週末にでもじっくり動作確認したいとおもっているので、お急ぎでなければ、しばらくお待ちください。
      それにしても改めてソースををみてみるとぐちゃぐちゃしていて汚いなと反省しています。

      • 筥庭 蛙 より:

        そんなことないですよ。石鷹さんのコードなら「PageSpeed Insights」 にも「The W3C Markup Validation Service」にも引っかからないので、とても助かっています。WordPressの目次作成プラグイン「Table of Contents」だとどちらか一方に必ず引っかかってしまうんですよ。気長に待っているので、どうか無理しないでくださいね。

    • 石鷹 より:

      一通りの動作確認してみましたが、正しく動作しているようです。
      ただし、ちょっとだけコードを(シンプルに)改修し、念のため jQuery を組み込む処理を追加したものをアップしたので、今回のものを使用してみてください。
      お手数かけます。m(__)m

  • CHIE より:

    石鷹さんはじめまして。
    ショートコードを[toc title=”目次” depth=”0″]で使用しました。
    すると閉じる、開くを押すとページトップにスクロースしてしまうのですが、スクロールしない事は出来ないでしょうか。Javascriptで何かソースが必要でしょうか。

    お忙しいところすみません。よろしくお願いします。

    • 石鷹 より:

      こんにちは
      開閉リンクのリンク先(href)を “#” にしていたためスクロールしていました。
      スクロールしないほうがいいですよね。というかバグですね。コードは修正しておきます。
      修正箇所は下記の通りです。
      101行目
      $toggle = ' <span class="toc-toggle">[<a class="internal" href="#">' . $this->atts['closetext'] . '</a>]</span>';
      $toggle = ' <span class="toc-toggle">[<a class="internal" href="javascript:void(0);">' . $this->atts['closetext'] . '</a>]</span>';

      • CHIE より:

        ありがとうございます!!開く、閉じるを押してもページトップにスクロースされなくなりました。

        あと、もうひとつ質問なのですが、目次のタイトルがダブルコーテーションで囲まれて表示されてしまいます。該当すると思われる箇所のtoc-titleのソースを見たのですがダブルコーテーションがどこにも記述されていないように見えるのにダブルコーテーションが表示されてしまいます。

        キャッシュの問題なのでしょうか?できれがこのダブルコーテーション消したいのですが手段はありますか?

    • 石鷹 より:

      ダブルコーテーション(”)が全角になっているのかもしれません。チェックしてみてください。
      それから、[toc title="目次" depth="0"] でしたら、デフォルトのパラメーターなので [toc] とするだけでも OK ですよ。

  • 豆太 より:

    この記事のコードを実際にfunction.phpに書いてみたのですが、そのまま貼ると何故かページの読み込みが出来なくなってしまいます。
    Twenty Twelveをベースにカスタマイズしているのですが、何か考えられる原因は思い当たりますでしょうか。
    php初心者なりに努力しているのですが、どうもうまく行かず。。
    アドバイスやヒントを頂ければ幸いです。

    • 石鷹 より:

      こんにちは
      当該コードの先頭の <?php の行を削除してみてください。
      削除してもページが読み込めない場合は、下記ページを参考にエラーを確認してみてください。
      https://wpdocs.osdn.jp/WordPressでのデバッグ
      当該コードのエラーはもちろん、それ以外でも、なるでく対応していきたいとおもいますので、返信お待ちしております。ではでは。

      • 豆太 より:

        返信ありがとうございます。
        さっそく先頭行を削除してみたところ無事ページは表示されました。

        しかし「閉じる」や、h2,3タグまでのジャンプが効かない状態になっています。今現在こちらでも試行錯誤しているところです。

      • 豆太 より:

        もう一度トライしたところ無事表示されました!
        大変丁寧に対応して頂きありがとうございました!

        • 石鷹 より:

          おっ、よかった!
          今後も何かありましたら気軽にコメントしてください。

  • harukichi より:

    初めまして。
    プラグインなしで目次を作りたくて、こちらの記事にたどり着きました。
    簡単に目次が作れて非常に感謝しています。

    1点質問なのですが、はじめに目次を閉じた状態にするにはどこのコードを変えれば良いのでしょうか。
    お手隙で構いませんので、返信いただけたら嬉しいです!

    • 石鷹 より:

      こんにちは
      おっ、そうですよね初めに閉じた状態がいい場合ありますよね。
      早速、初めに閉じた状態にするためのショートカットのオプション(close)を追加しました。詳細は本文を参照してください。
      なお、デフォルトでは開いた状態としたのでオプションを指定するか、コードの 'close' => false;(26 行目)の false を true にしてください。

  • popoppo より:

    はじめまして!使い易い目次のコード、ありがとうございます!
    早速導入させて頂いたのですが、目次の数字の消し方がわかりません・・・。
    教えていただければ幸いです。

    • 石鷹 より:

      こんにちは、コメントありがとうございます。
      目次の数字は、下記のスタイル(CSS)で消すことができます。テーマの style.css またはカスタマイズの追加 CSS 等に追記してみてください。
      .toc .contentstable-number { display: none; }

      • popoppo より:

        .toc を入れ忘れておりました!!!
        おかげさまで消すことができました。ご親切にありがとうございました。

  • 浅井 より:

    優良テーマの「TCD」の「gorgeous」にテーマを変更したら、それまできちんと動いていた目次移動が、目次の表示はできるのですがクリックしても移動しなくなってしまいました。何か方法はないでしょうか?

    • 石鷹 より:

      こんにちは、コメントありがとうございます。
      有料テーマなので検証できていませんが、
      [toc targetclass="post"]
      では、どうでしょうか。
      コードを変更する場合は、40行目の
      'targetclass' => 'entry-content',
      を、
      'targetclass' => 'post',
      へ変更してみてください。

      • 浅井 より:

        出来ました!

        テーマを変えたら突然動かなくなり、何日も悩んでいたので感動です!本当にありがとうございました。

  • つっちー より:

    はじめまして。
    コード使用させていただいております。
    ありがとうございます。

    ご質問です。
    h2タグより下の(h3以下)階層で目次を表示させたいと考えております。
    表示自体は出来るのですが、h2タグに入っている「id=”toc2″」が、目次のh3を表示しているリンクに入ってしまいます。

    お手数をおかけいたしますが、ご教授いただけますでしょうか。

    • 石鷹 より:

      こんにちは、初めまして。
      すみません、ちょっと症状を把握できていません。目次の表示は正しいがリンクが正しくないということでしょうか?
      もう少し具体的(ショートコードのパラメータやヘッダー構成など)に教えてもらえませんか。

      ショートコード:
      [toc toplevel=3]

      ヘッダー構成:
      ヘッダー2
       ヘッダー3
       ヘッダー3
        ヘッダー4
       ヘッダー3
      ヘッダー2
       ヘッダー3

      • つっちー より:

        迅速なお返事ありがとうございます。
        現状をお伝えいたします。

        HTML内の自動ID挿入で、
        ヘッダー1(h1)タグに「id=”toc1″」、
        ヘッダー2(h2)タグに「id=”toc2″」、
        ヘッダー3(h3)タグに「id=”toc3″」、
        が挿入されており、

        目次のリンク生成の始まりが「a href=”#toc2″」
        となっております。

        「’toplevel’ => 3,」でヘッダー3(h3)タグからスタートさせたいです。

        ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
        ‘showcount’ => 0,
        ‘depth’ => 2,
        ‘toplevel’ => 3,

        $shortcode = ‘[toc showcount=”4″]’;
        $pattern = ‘//i’;

        ヘッダー構成:
        ヘッダー1
         ヘッダー2
          ヘッダー3
           ヘッダー4
          ヘッダー3
          ヘッダー3

        説明が下手ですがわかりますでしょうか?

    • 石鷹 より:

      「投稿に目次を自動挿入」のコードにて、自動挿入する場合のことでしょうか?
      この場合は、

      $shortcode = '[toc showcount="4"]';

      $shortcode = '[toc toplevel="3"]';

      目次を挿入する位置をヘッダー3の直前にする場合は、

      $pattern = '/<h2.*?>/i';

      $pattern = '/<h3.*?>/i';

      となります。

      • つっちー より:

        ありがとうございました。

        はい。
        投稿に目次を自動挿入です。

        「’targetclass’ => ”,」の設定をh1タグのクラスがないところに変更したらうまくいきました。

        もうひとつ教えていただきたいのですが、「showcount」はどんな設定になりますでしょうか?

        • つっちー より:

          何度もすみません。

          やはりリンクタグの取得がずれてしまいます。
          サイトリンクをお送りしますので、一度ご確認いただけますでしょうか?

          お手数をおかけいたしますが、何卒よろしくお願いいたします。

          • 石鷹 より:

            コードを下記のように変更してみてください。

            'targetclass' => 'entry-content',

            'targetclass' => 'article-body',

            showcount は、この値より目次の項目が少ない場合に目次を表示しないようにするパラメータです。

  • つっちー より:

    ご回答ありがとうございます。
    ‘targetclass’ => ‘article-body’,に変更いたしました。
    ありがとうございます。

    既定のhタグに「toc」を挿入しないようにすることはできますでしょうか?

    • 石鷹 より:

      「既定のhタグ」とはどこのことかしら?
      過去の投稿に目次を自動挿入しないということかしら?この場合は「投稿に目次を自動挿入」のコードは不要です。目次を挿入したい投稿の場所にショートコードを記述してください。

      • つっちー より:

        お返事遅くなりすみません。
        サイトURLのページでは「MEMO」のh3タグにtoc6が挿入されています。
        このtoc6が目次に表示されていないためにリンクタグが1つずれているものと思われます。

        「MEMO」のh3タグにtocを追加しないようにすることはできませんでしょうか?

        • 石鷹 より:

          実はあれからバージョンアップ (2.0.0) しました。
          以前は、targetclass パラメータで目次の対象とする場所を指定していましたが、これがコンテンツと一致しない場合に不具合が起こることがありました。
          お手数ですが、コードを更新してみてください。なお、targetclass パラメータは不要となったので削除してください。

          • つっちー より:

            お世話になります。

            バージョンアップしたコードを使用してみましたが、Hタグにtocクラスが追加されなくて、目次のリンクを押してもスクロールしません。

            何度も何度もすみませんが、ご確認お願いできますでしょうか。

          • 石鷹 より:

            前バージョンのコードでカスタマイズした箇所が残っているようです。
            下記のように変更してみてください。
            $(".article-body :header").each(function () {

            $("#toc_content :header").each(function () {

          • つっちー より:

            すみません。
            前のバージョンに戻していました…。

            変更してみましたが、うまく動作しません。

            何度もすみませんが、何卒よろしくお願いいたします。

          • 石鷹 より:

            同じ構成のコンテンツを用意して、検証してみたのですが、正しく動作するんですよね。
            謎です。もうすこし検証作業をしてみるつもりですが・・・時間がかかりそうです。
            お手数おかけしてすみません。

          • つっちー より:

            $content = “{$content}”;

            このコードは何を意味しますでしょうか?

          • 石鷹 より:

            $content = "<div id=\"toc_content\">{$content}</div>";
            ですかね。
            これは、目次の対象とするコンテンツを明確に特定するために、コンテンツをラップするタグ(#toc_content)を追加する処理です。
            以前は、パラメータで指定するようにしていましたが、テーマによって変更する必要があったため、このような処理にしました。

          • つっちー より:

            お世話になります。

            ご回答ありがとうございました。

            他のサイトでテストしてみましたが、動作しませんでした。
            もしかしたらテーマに問題があるのかもしれませんね。
            賢威というテーマを使用しています。

            また何か進展がありましたらご連絡させていただきます。
            ご丁寧な対応感謝しています。引き続きよろしくお願いいたします。

          • 石鷹 より:

            賢威テーマに備わっている目次機能と競合しているのかもしれません。
            有料テーマなのでコードを確認できないため、断定はできませんが・・・。
            お役に立てずにすみません。

コメントを残す

メールアドレスが公開されることはありません。

日本語でコメントを入力してください。