WordPressで目次を自動でつける方法を解説(プラグインなし)

PHP Programming Wordress

こんにちは。あきのりです。
フリーランスプログラマーとしてwebサイト・サービスを作っています。

今回は、Wordpressで目次を作る方法をプラグインなしで解説しようと思います。

目次を作る方法としては、「Table Of Contents Plus」というプラグインを使うのが有名なんですが、プラグインを使っていると不具合が起きたり、ページ表示速度が落ちてしまったりとデメリットが多いです。

なので、自作した方が良いです。

プログラミング知識のない方でもコピペで実装できますし、プログラマーの方のためにコードの解説もしていきます。

wordpressのファイルを編集することになるので、必ずバックアップを取ってから以降の作業を行ってください。

functions.phpにコードを挿入

まず、以下のコードをfunctions.phpというファイルに貼り付けてください。
「外観」-> 「テーマエディタ」で編集できます。


/**
 * 見出しの作成
 */

function add_heading_id($content) {
    if(is_single()) {
        // codeタグ内のh2,h3タグは無効する
        $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

        $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
        foreach($matches as $match) {
            preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
        }

        // 全てのh2, h3タグを取得
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
        
        $i = 0;
        $j = 0;
        $headings = [];
        // 各タグの書き換え&リストの作成
        foreach($matches as $element) {
            $isHeading = true;
            // codeタグ内のhタグは無効
            foreach($removes as $remove) {
                if($element[0] == $remove[0]) {
                    $isHeading = false;
                    break;
                }
            }
            if($isHeading) {
                if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                    $i++;
                    $id = 'heading' . $i;
                    $heading = preg_replace('/<h2>/', '<h2 id="'.$id.'">', $element[0]);
                    $j = 0;
                } else { //h3タグの場合
                    $j++;
                    $id = 'heading' . $i . '_'  .$j;
                    $heading = preg_replace('/<h3>/', '<h3 id="'.$id.'">', $element[0]);
                }
                $content = str_replace($element[0], $heading, $content);
                $headings[] = $heading;
                
            }
            // $content = preg_replace($pattern, $heading, $content, 1);
        }
    }
    return $content;
}
add_action('the_content', 'add_heading_id');

function add_index_of_contents() {
    global $post;
    $content = $post->post_content;
    // codeタグ内のh2,h3タグは無効する
    $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

    $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
    foreach($matches as $match) {
        preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
    }

    // 全てのh2, h3タグを取得
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
    
    $list = '<ul class="table-of-contents">';
    $hierarchy = 0;
    $i = 0;
    $j = 0;
    // 各タグの書き換え&リストの作成
    foreach($matches as $element) {
        $isHeading = true;
        // codeタグ内のhタグは無効
        foreach($removes as $remove) {
            if($element[0] == $remove[0]) {
                $isHeading = false;
                break;
            }
        }
        if($isHeading) {
            if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                if($hierarchy == 0) { //1つ前がh2タグの場合, liを閉じる
                    if($i > 1) {
                        $list .= '</li>';
                    }
                } else { // 1つ前がh3タグの場合, h3のul内のli, h3のul, h2のul内のliを閉じる
                    $list .= '</li></ul></li>';
                }
                $i++;
                $id = 'heading' . $i;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>';
                $hierarchy = 0;
                $j = 0;
            } else { //h3タグの場合
                if($hierarchy == 0) {//1つ前がh2タグの場合, h3用のulを追加
                    $list .= '<ul>' ;
                } else { //1つ前がh2タグの場合, h3用のliを閉じる追加
                    $list .= '</li>' ;
                }
                $j++;
                $id = 'heading' . $i . '_'  .$j;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>'; 
                $hierarchy = 1;
            }
        }
    }

    if($hierarchy == 0 ) {
        $list .= '</li></ul>';
    } else {
        $list .= '</li></ul></li></ul>';
    }
    return $list;
}
add_shortcode('mokuji', 'add_index_of_contents');

貼り付けたら保存します。

ショートコードで目次を作成

後は記事を作成するときに、目次を表示させたい場所に[mokuji]と打ってください。

するとプレビューで見たときに、箇条書きで目次が出てくると思います。

デザインは僕が独自でつけているので写真そのままのようになりません。この辺りはcssを自分で当てください(ごめんなさい!)。

プログラマー向けコード解説

ここからはプログラマー向けに、より中身を理解するための解説です。

目次の仕組み

まず、目次の仕組みをおさらいしておきます。
HTMLタグにはidが定義でき、これはaタグのhrefに入れることができます。


<a href="#heading1">「見出し」へ</a>

<h2 id="#heading1">見出し</h2>

このように書くと、「『見出し』へ」をクリックしたときに「見出し」まで画面を移動させることができるわけです。

見出しは基本的にh2やh3タグで囲まれます。

つまり、手順は以下のようになります。

  • h2,h3タグを取り出す
  • これらのタグにidをつける
  • 見出しidと対応させながら、目次を作成

これをプログラムで書いたのがさっきのやつです。

h2,h3タグを抽出する

では、最初の手順です。
まず、大枠がこちら。


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

    }
    return $content;
}
add_action('the_content', 'add_heading_id');

add_heading_idというh2,h3タグにidをつける関数を定義します。
そして、add_actionによって実際に呼び出します。

add_heading_id$contentには表示するwebページの文字列で入ってきます。つまりhtmlコードが一式入ってきます。
また、中身で条件分岐していますが、is_single()は「投稿ページならtrue、それ以外はfalse」です。
見出しは記事一覧ページなどでは表示させたくないので、このように「投稿ページのときのみ」という条件をつけています。

また、上記のadd_actionの意味は「投稿表示時にadd_heading_idを呼び出す」です。
では、関数の中身について見ていきます。

h2,h3タグを取り出すには正規表現を使う

正規表現とは、ざっくり言えば文字比較です。正規表現は若干とっつきにくいんですが、知っておきましょう。詳しくはこちらで


       $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
       // 全てのh2, h3タグを取得
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

$patternが正規表現です。上記のように書くと「h2,h3タグのいずれかを取得する」ことができます。
preg_match_allが実際に文字を抽出する関数で使い方はこんな感じです。
preg_match_all(正規表現, 検索する文字列, 条件を満たす文字列を格納する配列, オプション);

最後のオプションはPREG_SET_ORDERとしておけば問題ないです。
これは、正規表現で()を使った部分に当たる文字列もを順番に格納するというものです。

例を挙げると、今回の場合は

テキスト

というものがあった場合$matchesにはいる値はこのようになります。


$matches = [
  0 => [
      0 => '<h2>テキスト</h2>' // マッチしたもの全体
      1 => 'テキスト' // ()が使われた部分
   ]
]

マッチした分、このように配列で格納されていきます。

これらのタグにidをつける

h2,h3タグを取り出せたので、idを付け加えていきます。


        // 各タグの書き換え&リストの作成
        foreach($matches as $element) {
            if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                $i++;
                $id = 'heading' . $i;
                $heading = preg_replace('/<h2>/', '<h2 id="'.$id.'">', $element[0]);
                $j = 0;
            } else { //h3タグの場合
                $j++;
                $id = 'heading' . $i . '_'  .$j;
                $heading = preg_replace('/<h3>/', '<h3 id="'.$id.'">', $element[0]);
            }
            $content = str_replace($element[0], $heading, $content);
            $headings[] = $heading;
        }

idは以下のようにつけています。


見出し  (タグ) ->  id
-----------------------
見出し1 (h2) -> heading1
見出し2 (h2) -> heading2
  小見出し1 (h3) ->heading2_1
  小見出し2 (h3) ->heading2_2
見出し3 (h2) -> heading3
...

実際に置き換えが行われているのはこのコードの部分。


$heading = preg_replace('/<h2>/', '<h2 id="'.$id.'">', $element[0]);

preg_replace(置き換え対象の文字, 実際に置き換える文字, 対象の文字列)という形で書きます。

最終的には記事全体の文字列$contentに更新しないといけないので、最後に以下のようにしています。


$content = str_replace($element[0], $heading, $content);

codeタグは除く

ただ、これだと1つ問題があります。
この記事のように、コード説明をする際にcodeタグで囲った部分にh2タグが含まれる場合があります。
これは当然見出しではないんですが、このh2タグも抽出されてしまいます。

なので、codeタグの中にあるhタグは除外する処理をつけたします。


        // codeタグ内のh2,h3タグは無効する
        $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

        $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
        foreach($matches as $match) {
            preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
        }

まずはcodeタグ全体を取り出し、さらにその中のh2,h3タグを取り出します。これらは除外項目として$removesに配列として格納されています。

そして、実際に置き換えるループ処理に付け加えます。


        foreach($matches as $element) {
            $isHeading = true;
            // codeタグ内のhタグは無効
            foreach($removes as $remove) {
                if($element[0] == $remove[0]) {
                    $isHeading = false;
                    break;
                }
            }
            if($isHeading) { //以降置き換え処理

$elementはh2,h3タグを無条件でとったものなので、これとremoveと比較して一致したら置き換え処理を行わないようにしています。

これで、codeタグ以外のh2,h3タグにidを付与させることができます。

ここまでの処理のまとめはこんな感じ。


function add_heading_id($content) {
    if(is_single()) {
        // codeタグ内のh2,h3タグは無効する
        $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

        $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
        foreach($matches as $match) {
            preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
        }

        // 全てのh2, h3タグを取得
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
        
        $i = 0;
        $j = 0;
        $headings = [];
        // 各タグの書き換え&リストの作成
        foreach($matches as $element) {
            $isHeading = true;
            // codeタグ内のhタグは無効
            foreach($removes as $remove) {
                if($element[0] == $remove[0]) {
                    $isHeading = false;
                    break;
                }
            }
            if($isHeading) {
                if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                    $i++;
                    $id = 'heading' . $i;
                    $heading = preg_replace('/<h2>/', '<h2 id="'.$id.'">', $element[0]);
                    $j = 0;
                } else { //h3タグの場合
                    $j++;
                    $id = 'heading' . $i . '_'  .$j;
                    $heading = preg_replace('/<h3>/', '<h3 id="'.$id.'">', $element[0]);
                }
                $content = str_replace($element[0], $heading, $content);
                $headings[] = $heading;
                
            }
            // $content = preg_replace($pattern, $heading, $content, 1);
        }
    }
    return $content;
}
add_action('the_content', 'add_heading_id');

目次を作成する(ショートコード)

次は目次の作成です。大枠はこちら。


function add_index_of_contents() {

}
add_shortcode('mokuji', 'add_index_of_contents');

さっきとほぼ一緒です。違いは、今回はショートコードで登録したいのでadd_shortcodeで最後登録しています。
今回の場合、「[mokuji]と書くとadd_index_of_contents関数が呼ばれる」という意味になります。

先ほどと同様にh2,h3タグを抽出

ここも同じように、h2,h3タグを抽出します。
(ここはもう少し効率化できそうですが、力不足です。)


    global $post;
    $content = $post->post_content;
    // codeタグ内のh2,h3タグは無効する
    $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

    $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
    foreach($matches as $match) {
        preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
    }

    // 全てのh2, h3タグを取得
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

さっきのように引数に「記事全体の文字列$content」が取れないので、グローバル変数$postを使っています。

これはその記事の情報を持っていて、$post->post_contentで記事全体の文字列を取得できます。

実際に目次を作成

ここからがちょっと複雑です。


    $list = '<ul class="table-of-contents">';
    $hierarchy = 0;
    $i = 0;
    $j = 0;
    // 各タグの書き換え&リストの作成
    foreach($matches as $element) {
        $isHeading = true;
        // codeタグ内のhタグは無効
        foreach($removes as $remove) {
            if($element[0] == $remove[0]) {
                $isHeading = false;
                break;
            }
        }
        if($isHeading) {
            if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                if($hierarchy == 0) { //1つ前がh2タグの場合, liを閉じる
                    if($i > 1) {
                        $list .= '</li>';
                    }
                } else { // 1つ前がh3タグの場合, h3のul内のli, h3のul, h2のul内のliを閉じる
                    $list .= '</li></ul></li>';
                }
                $i++;
                $id = 'heading' . $i;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>';
                $hierarchy = 0;
                $j = 0;
            } else { //h3タグの場合
                if($hierarchy == 0) {//1つ前がh2タグの場合, h3用のulを追加
                    $list .= '<ul>' ;
                } else { //1つ前がh2タグの場合, h3用のliを閉じる追加
                    $list .= '</li>' ;
                }
                $j++;
                $id = 'heading' . $i . '_'  .$j;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>'; 
                $hierarchy = 1;
            }
        }
    }

    if($hierarchy == 0 ) {
        $list .= '</li></ul>';
    } else {
        $list .= '</li></ul></li></ul>';
    }
    return $list;

$listを最初に定義していますが、これが目次のhtml文字列となります。
リストで書きたいので$list = '</p> <ul class="table-of-contents">'とulタグを最初に入れています。

ループの前半はcodeタグ内のh2,h3タグの除外処理なので、飛ばします。

今回作成したいリストを図で表すとこんな感じです。


<ul>//リスト開始
   <li>見出し1 (h2)</li> //次が見出し(h2)なので</li>で閉じる
   <li>見出し2 (h2)      //次が小見出し(h3)なので</li>で閉じないで、
       <ul>             // <ul>タグを書き、小見出し用のリストを作成
           <li>小見出し1 (h3)</li> // 次が小見出し(h3)なので</li>で閉じる
           <li>小見出し1 (h3)</li> // 次が見出し(h2)なので</li>で閉じつつ、
       </ul>                      // </ul>で閉じて小見出し用のリストを閉じる
   </li>
  <li>見出し3 (h2)</li> //<- リストの最後なので、</li>で閉じ、
</ul>                   // <\ul>で閉じる

つまりループ処理をしていくときに

  • 1つ前が見出し(h2)のとき、次どうするか?
  • 1つ前が小見出し(h3)のとき、次どうするか?

という前のタグがカギになってくるわけです。そこで$hierarchyという変数を使って、「h2なら0, h3なら1」という情報を持たせておけば、処理ができるというわけです。

これを踏まえて、コメントも参考にしつつコードを見て理解をしてください。


function add_index_of_contents() {
    global $post;
    $content = $post->post_content;
    // codeタグ内のh2,h3タグは無効する
    $pattern = '/<code[^>]*>[\s\S]*?<\/code>/i';
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

    $pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
    foreach($matches as $match) {
        preg_match_all($pattern, $match[0], $removes, PREG_SET_ORDER);
    }

    // 全てのh2, h3タグを取得
    preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
    
    $list = '<ul class="table-of-contents">';
    $hierarchy = 0;
    $i = 0;
    $j = 0;
    // 各タグの書き換え&リストの作成
    foreach($matches as $element) {
        $isHeading = true;
        // codeタグ内のhタグは無効
        foreach($removes as $remove) {
            if($element[0] == $remove[0]) {
                $isHeading = false;
                break;
            }
        }
        if($isHeading) {
            if(strpos($element[0], '<h2') === 0) { // h2タグの場合
                if($hierarchy == 0) { //1つ前がh2タグの場合, liを閉じる
                    if($i > 1) {
                        $list .= '</li>';
                    }
                } else { // 1つ前がh3タグの場合, h3のul内のli, h3のul, h2のul内のliを閉じる
                    $list .= '</li></ul></li>';
                }
                $i++;
                $id = 'heading' . $i;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>';
                $hierarchy = 0;
                $j = 0;
            } else { //h3タグの場合
                if($hierarchy == 0) {//1つ前がh2タグの場合, h3用のulを追加
                    $list .= '<ul>' ;
                } else { //1つ前がh2タグの場合, h3用のliを閉じる追加
                    $list .= '</li>' ;
                }
                $j++;
                $id = 'heading' . $i . '_'  .$j;
                $list .= '<li><a href="#'.$id.'">' . $element[1] . '</a>'; 
                $hierarchy = 1;
            }
        }
    }

    if($hierarchy == 0 ) {
        $list .= '</li></ul>';
    } else {
        $list .= '</li></ul></li></ul>';
    }
    return $list;
}
add_shortcode('mokuji', 'add_index_of_contents');

というわけで、以上です。

良い一日を。

スポンサードサーチ

オススメ英語学習用SNS "Our Dictionary"

人気記事英語学習用SNSをLaravelで作ってみた【システム解説あり】