monologue

No genius over the effort

Menu
  • contact
  • ブログ(ショートコードテスト用)
Menu

【WordPress】目次の作り方(プラグインなし)

Posted on 2022年3月20日2022年3月24日 by LEN

WordPressでウェブサイトやブログを作成していると、ページ内に目次を作成したい場合に毎回、自分で目次を作成するのは面倒なので、自動的に振りたい!と思いました。
でもプラグインを入れたくないなぁと調べていたら自分で作れる!ということを知り、早速自作。
プラグインを使わずに、記事の見出しから目次を作成するソースコードの例です。

この記事の目次

  • 1 目次を作成しておくと何がよいのか
  • 2 今回作りたい目次
  • 3 WordPressで投稿ページの目次を自動出力する
    • 3-1 準備する物
  • 4 functions.phpの編集
    • 4-1 コードの解説
  • 5 stayle.cssの編集
  • 6 あとがき

目次を作成しておくと何がよいのか

記事の概要が一目で分かったり、必要な場所に一瞬でとべたりと読む側によっては重要な役割を果たします。
SEO的には、Googleは目次と検索順位の関係性について公式に言及していないためあくまで推測ですが、検索順位を直接左右する「ランキング要因」としてではなく、間接的な要因として働くのではないかと思われます。

よく聞くのは「Googleが理解しやすいサイト(=ユーザーが理解しやすいサイト)はランキングの上位にきやすいということです。要するにユーザーが理解しやすく読みやすいサイトはランキングの上位に上がってきます。
目次は設置することでユーザーが記事を読みやすくなる(=Googleがサイトを理解しやすくなる)ため間接的にランキングを上げることに役立ちます。

今回作りたい目次

必要な時だけショートコードで目次を呼び出す。

全ての記事に自動で目次が出てくる必要ないと思うのでショートコードで必要な時だけ呼び出せるコードにします。

目次として出力されるHTMLはli要素で出力され、目次の項目にはページ内のそれぞれのアンカーリンクを作成します。
また、ページ内でh2要素の次にh3要素がある場合には、目次が階層化されli要素がネスト(入れ子)として出力されます。こちらはそれぞれのページによって変更してくださいね。

WordPressで投稿ページの目次を自動出力する

準備する物

テーマの「functions.php」と「style.css」を使います。
テーマを編集するので、バックアップをしたり、子テーマでの編集するのをお忘れなく。

functions.phpの編集

下記のコードをコピーして「functions.php」に記述します。

※こちらのコードはXakuro Blog さんを参考にさせていただきました。
<?php
/**
 * 目次の作成
 * ショートコードで目次を挿入.
 */
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' => 'この記事の目次',
			'close' => false,
			'showcount' => 2,
			'depth' => 0,
			'toplevel' => 2,
			'scroll' => 'smooth',
		), $atts );

		$content = $post->post_content;

		$headers = array();
		preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
  <?php //この行は不要なのでコピーした後に削除してください。
		$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;
			}
			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"' : '';
					$toc_list .= "<ul{$class}>";
					$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;

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

		return $html;
	}

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

		$var = wp_json_encode( array(
			'scroll' => isset( $this->atts['scroll'] ) ? $this->atts['scroll'] : 'smooth',
		) );

		?>
<script type="text/javascript">
var xo_toc = <?php echo $var; ?>;
let xoToc = () => {
  const entryContent = document.getElementById('toc_content');
  if (!entryContent) {
    return false;
  }

  /**
   * スムーズスクロール関数
   */
  let smoothScroll = (target, offset) => {
    const targetRect = target.getBoundingClientRect();
    const targetY = targetRect.top + window.pageYOffset - offset;
    window.scrollTo({left: 0, top: targetY, behavior: xo_toc['scroll']});
  };

  /**
   * アンカータグにイベントを登録
   */
  const wpadminbar = document.getElementById('wpadminbar');
  const smoothOffset = (wpadminbar ? wpadminbar.clientHeight : 0) + 2;
  const links = document.querySelectorAll('.toc-list a[href*="#"]');
  for (let i = 0; i < links.length; i++) {
    links[i].addEventListener('click', function (e) {
      const href = e.currentTarget.getAttribute('href');
      const splitHref = href.split('#');
      const targetID = splitHref[1];
      const target = document.getElementById(targetID);

      if (target) {
        e.preventDefault();
        smoothScroll(target, smoothOffset);
      } else {
        return true;
      }
      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));
  }

};
xoToc();
</script>
		<?php
	}

}

new Toc_Shortcode();

コードの解説

17行目:投稿内容に「toc_content」というidをつける

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

27行目:変数に値を代入

$this->atts = shortcode_atts( array( 
  'id' => '',
  'class' => 'toc',
  'title' => 'この記事の目次',//目次名を変更する場合はここを変更
  'close' => false,
  'showcount' => 2,
  'depth' => 0,//depthはトップレベルを基準にHタグを取得する範囲を設定する。toplevel=2でdepth=2ならばH2〜H4まで取得する。
  'toplevel' => 2,//H2をトップレベルにする場合はtoplevel=2、H3をトップレベルにする場合はtoplevel=3に設定する。
  'scroll' => 'smooth',
), $atts );

41行目:本文の中から全てのh要素を検索

preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );

PHPマニュアル(preg_match_all)

https://www.php.net/manual/ja/function.preg-match-all.php

57行目:取得したhタグをカウントし、hタグによってdepthのレベルを変えていく

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

64行目:現在の階層によって挿入するタグを分岐させる。
$depthが1より大きく、max_depthよりも小さく、現在のdepthが$depthと同じ場合は</li>を挿入。
リストにはaタグでアンカーリンクを挿入。

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 ) {  //現在のdepthと一つ前のdepthが等しくない場合は</li>を挿入
    $toc_list .= '</li>';
  }
  if ( $current_depth < $depth ) {  //現在のdepthより$depthが大きい場合 $toc_listに<ul class="toc-list">を挿入し、カウントをプラス
    $class = $current_depth == 0 ? ' class="toc-list"' : '';
    $toc_list .= "<ul{$class}>";
    $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>';//作ったリストに「contentstable-number」というクラスを付与
  $prev_depth = $depth;
  }
}

91行目:$depthが1より大きい場合はリストタグを閉じる(</li></ul>を入れる)

while ( $current_depth >= 1 ) { 
  $toc_list .= '</li></ul>';
  $current_depth--;
}

96行目:目次のリスト(出力用)を作成し、返す

$html = '';
if ( $counter >= $this->atts['showcount'] ) {  
  $this->add_script = true;
  $html .= '<div' . ( $this->atts['id'] != '' ? ' id="' . $this->atts['id'] . '"' : '' ) . ' class="' . $this->atts['class'] . '">';
  $html .= '<p class="toc-title">' . $this->atts['title']  . '</p>';
  $html .= $toc_list;
  $html .= '</div>' . "\n";
}
return $html;

残りは、、jQueryでスクロールのスムーズさだったり、アンカータグにイベントを入れたり、アンカーのためのIDを付与したり。

stayle.cssの編集

style.cssに下記のコードを追記します。
お好みによって色や枠は変更してくださいね。

/* 目次デザイン */
ul li, ol li {
  list-style-type: none;
  padding: 0.1em 0 0.1em 1.5em;
}

.toc {
  color: #696969;
  border: solid #696969 1px;
  margin-bottom: 1.5em;
}

.toc-title{
  background: #000000;
  color: #FFFFFF;
  padding: 0;
  border: solid 2px #696969;
  padding: 0.5em 0 0.5em 0;
  text-align: center;
  font-size: 1.2em;
  margin: -1px !important;
}

.toc-list{
  margin-top: 0.5em;
}

あとがき

これ作るのに色々調べて、苦労しました。
なぜって、、、自分のこだわりが多いからw
そして一つ一つのコードを理解するために調べながら作ったから。
コピペするのは簡単ですが、それじゃいつまでたっても覚えないんだよなぁと思って一念発起。結構疲れました。そして良い勉強になりました。
プラグインを使うという方法もありましたが、こんな小さなページでも極力プラグインを使わずにやりたいというこだわりがあるので、頑張ってみました。
ほぼ、自分の覚書みたいなものですがいつか誰かのお役に立つ日がくるかもしれない。。。。

ABOUT ME

   メンドクサイことは自動化しよう!を合言葉に色々と効率化を求めて非効率なことをやっています

最近の投稿

  • 【WordPress】目次の作り方(プラグインなし)②
  • 【WordPress】目次の作り方(プラグインなし)
2025年5月
日 月 火 水 木 金 土
 123
45678910
11121314151617
18192021222324
25262728293031
« 3月    

カテゴリー

© 2025 独り言 | Powered by Superbs Personal Blog theme