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

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

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

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

PHP コード

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

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' ) );
	}

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

	public function shortcode_content( $atts ) {
		$this->atts = shortcode_atts( array(
			'id' => '',
			'class' => 'toc',
			'title' => '目次',
			'toggle' => 'true',
			'opentext' => '開く',
			'closetext' => '閉じる',
			'close' => false,
			'showcount' => 2,
			'depth' => 0,
			'toplevel' => 2,
			'targetclass' => 'entry-content',
			'offset' => '',
			'duration' => 'normal'
		), $atts );

		$content = get_the_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 = $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 ( strtolower( $this->atts['toggle'] ) == 'true' ) {
				$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'] . '"';
		$targetclass = trim( $this->atts['targetclass'] );
		if ( $targetclass == '' ) {
			$targetclass = get_post_type();
		}
		$targetclass = ".$targetclass :header";
		$opentext = $this->atts['opentext'];
		$closetext = $this->atts['closetext'];
		?>
<script type="text/javascript">
(function ($) {
  var offset = <?php echo $offset; ?>;
  var idCounter = 0;
  $("<?php echo $targetclass; ?>").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 = $(".toc-list");
    if (tocList.is(":hidden")) {
      tocList.show();
      $(this).text("<?php echo $closetext; ?>");
    } else {
      tocList.hide();
      $(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”。

(2017年11月23日 追加)

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

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

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

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

toplevel 2 トップの階層のヘッダー(1 から 6)を指定します。
targetclass “entry-content” 目次を作成する対象のコンテンツのクラスを指定します。
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月10日 指定した階層よりも実際の階層が深い場合に正しく動作しませんでした。

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

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

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

コメント

  • 筥庭 蛙 より:

    石鷹さんが「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 にしてください。

コメントを残す

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

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