【WordPres】目次をプラグイン無しで自動生成するコード【解説付き】

WordPressサイトで、ブログ記事や、企業サイト・サービスサイトにおけるコラムなどでよく使用されている「目次機能」。便利なプラグインもたくさんありますが、ベーシックなもので良いということであれば、設定を管理画面から度々変更していくものでもないので、プラグインに頼らず自作にトライするのもオススメです。

またコンテンツの目次の仕組みを理解することは、HTML・SEOの知見を深めることにも繋がります。

プラグインなしでつくる目次のコード全体

// 目次の中身
function generate_table_of_contents($headings) {

  $table_of_contents = '<div class="toc"><p class="tocTitle">目次</p><ol class="tocList">';
  $current_h2_item = '';
  $has_h3 = false;

  foreach ($headings as $heading) {
    if ($heading['tag'] == 'h2') {
      if ($current_h2_item !== '') {
        if ($has_h3) {
          $table_of_contents .= '</ol>';
        }
        $table_of_contents .= '</li>';
      }
      $current_h2_item = '<li class="item-h2"><a href="#' . $heading['id'] . '">' . $heading['text'] . '</a>';
      $has_h3 = false;
      $table_of_contents .= $current_h2_item;

    } else {
      if (!$has_h3) {
        $table_of_contents .= '<ol class="toc_sublist">';
        $has_h3 = true;
      }
      $table_of_contents .= '<li class="item-h3"><a href="#' . $heading['id'] . '">' . $heading['text'] . '</a></li>';
    }
  }

  if ($current_h2_item !== '') {
    if ($has_h3) {
      $table_of_contents .= '</ol>';
    }
    $table_of_contents .= '</li>';
  }
  $table_of_contents .= '</ol></div>';

  return $table_of_contents;
}

// 目次の挿入
function insert_table_of_contents($content) {
  if (is_singular('post')) {
    // $heading_pattern = '/<(h2)(.*?)>(.*?)<\/(h2)>/is';
    $heading_pattern = '/<(h2|h3)(.*?)>(.*?)<\/(h2|h3)>/is';
    $matches = array();
    $heading_count = preg_match_all($heading_pattern, $content, $matches, PREG_SET_ORDER);

    if ($heading_count >= 2) {
      $headings = array();

      $heading_counter = 0;
      foreach ($matches as $match) {
        $headings[] = array(
          'tag' => $match[1],
          'attributes' => $match[2],
          'text' => $match[3],
          'id' => 'heading-' . $heading_counter
        );
        $heading_counter++;
      }

      $content = preg_replace_callback($heading_pattern, function ($match) use ($headings) {
        static $index = 0;
        $id = $headings[$index]['id'];
        $index++;
        return sprintf('<%1$s%2$s><span id="%3$s">%4$s</span></%1$s>', $match[1], $match[2], $id, $match[3]);
      }, $content);

      $table_of_contents = generate_table_of_contents($headings);

      $first_h2_position = strpos($content, '<h2');
      if ($first_h2_position !== false) {
        $content = substr_replace($content, $table_of_contents, $first_h2_position, 0);
      }
    }
  }
  return $content;
}
add_filter('the_content', 'insert_table_of_contents');

コードの大まかな流れ

①目次の中身を作るgenerate_table_of_contents($headings) という関数を用意し、②記事などに目次を挿入させるinsert_table_of_contents($content) という関数内で①の関数を実行します。

目次の中身をつくる関数の解説

引数$headingsは記事内のHタグ情報を配列にしたものを想定しており、以下のような配列を②の関数内で$headingsに入れています。今回のスクリプトは目次に反映させるHタグは、H2、H3です。

Array(
    [0] => Array
        (
            [tag] => h2
            [text] => タイトルテキストが入ります
            [id] => heading-0
        )

    [1] => Array
        (
            [tag] => h3
            [text] => タイトルテキストが入ります
            [id] => heading-1
        )

    [2] => Array
        (
            [tag] => h3
            [text] => タイトルテキストが入ります
            [id] => heading-2
        )

    [3] => Array
        (
            [tag] => h2
            [text] => タイトルテキストが入ります
            [id] => heading-3
        )
)

$table_of_contentsに出力するHTMLを格納します。処理の過程で.=による文字列結合を行い目次の中身を作成しています。

H2配下にH3があるかないかで処理を変更するため、$current_h2_item$has_h3を最初に定義しておきます。

  $table_of_contents = '<div class="toc"><p class="tocTitle">目次</p><ol class="tocList">';
  $current_h2_item = '';
  $has_h3 = false;

8行目でforeach文で繰り返し処理を行って$headingの値を利用し目次を作成しています。

  foreach ($headings as $heading) {
    if ($heading['tag'] == 'h2') {
      if ($current_h2_item !== '') {
        if ($has_h3) {
          $table_of_contents .= '</ol>';
        }
        $table_of_contents .= '</li>';
      }
      $current_h2_item = '<li class="item-h2"><a href="#' . $heading['id'] . '">' . $heading['text'] . '</a>';
      $has_h3 = false;
      $table_of_contents .= $current_h2_item;

    } else {
      if (!$has_h3) {
        $table_of_contents .= '<ol class="toc_sublist">';
        $has_h3 = true;
      }
      $table_of_contents .= '<li class="item-h3"><a href="#' . $heading['id'] . '">' . $heading['text'] . '</a></li>';
    }
  }

今回は、H2とH3を②の関数内で拾うので、9行目のif($heading['tag']=='h2'){ }でH2タグの場合の処理を行い、20行目のelse {}でH3タグの場合の処理を記述します。

繰り返しおける最初の処理は16行目になります。$current_h2_itemはH2タグを目次に反映させるコードが入ります。H3タグは、H2タグの下に入るようにしたいので、その場合に備えてこの段階ではliタグが閉じられていません。$has_h3はH2の処理をしている最中なのでfalseにしておきます。

H3タグの際には、H2配下における1つ目のH3か、2つ目以降のH3かを21行目で$has_h3のフラグを利用して判別し処理を変えています。1つめの時はリストの開始タグ<ol class="toc_sublist">を入れ、2つ目以降はこの処理を行いません。

このH3のolの閉じタグは、次のH2タグが出る時です。それは、$heading['tag']=='h2'$current_h2_itemに値があり$has_h3trueの時、つまり21行目のことです。