WordPressから投稿したコンテンツの見出し情報から自動で目次を制作する方法を解説します。
今回制作する目次はこちら。
見出しレベル2からレベル3までのタイトルを対象に目次を生成します。
目次をクリックでその見出しまで飛ぶことができます。
対象にるす見出しのレベルは引数から変更することが可能です。
目次のモックを作り、htmlの構造を決める
まず、処理を考えるまえに、どのようなコードをアウトプットしたいのか決めます。
デザインからhtml,cssを組んでモックを制作しましょう。
html
<div class="p-index"> <div class="p-index__title">目次</div> <div class="p-index__list"> <ul class="c-list -first"> <li class="c-list__item -lv_1"> <a class="c-list__item-link -lv_1" href="#area-1">見出し-2</a> </li> <li class="c-list__item -lv_1"> <a class="c-list__item-link -lv_1" href="#area-2">見出し-2</a> <ul class="c-list -second"> <li class="c-list__item -lv_2"> <a class="c-list__item-link -lv_2" href="#area-3">見出し-3</a> </li> <li class="c-list__item -lv_2"> <a class="c-list__item-link -lv_2" href="#area-4">見出し-3</a> </li> </ul> </li> <li class="c-list__item -lv_1"> <a class="c-list__item-link -lv_1" href="#area-5">見出し-2</a> </li> </ul> </div> </div>
cssは書かなくてもui,liのデフォルトのスタイルで理解できるのでhtmlのみ記載しておきます。
cssはお好みで組んでデザインを再現しましょう。
htmlの構造もこの構造でなくてもいいと思います。php処理の内容を変更する必要が出てきてしまいますが、自身のcss設計に合わせてhtmlの構成やclass名を変更してください。
functionsに処理を書く
上記のhtml構造をページのコンテンツから自動で生成してくれる処理をfunctions.phpに記載します。
function add_index( $content, $f, $e ) { if ( !$content || !is_single() ) return ; preg_match_all( '/<h[1-6]>(.+?)<\/h[1-6]>/', $content, $match_array, PREG_SET_ORDER );//参考 https://www.php.net/manual/ja/function.preg-match-all.php if ( count( $match_array ) <= 1 ) { return array( 'index'=> '', 'content'=> $content, ); }; $id = 1; $prevLv = 2;//前回の見出しレベル $html = ''; $html .= '<div class="p-index">'; $html .= '<div class="p-index__title">目次</div>'; $html .= '<div class="p-index__list">'; $html .= '<ul class="c-list -first">'; $pattern = '/<(h['.$f.'-'.$e.'])>(.+?)<\/(h['.$f.'-'.$e.'])>/'; foreach ( $match_array as $el ) { $replace_title = preg_replace( '/<(h[1-6])>(.+?)<\/(h[1-6])>/', '<$1 id="area-' . $id . '">$2</$3>', $el[0] );//参考 https://www.php.net/manual/ja/function.preg-replace.php $content = preg_replace( '{' . $el[0] . '}', $replace_title, $content, 1 );//重複を避けるため、ヒットした最初の文字のみ置換する if ( !preg_match($pattern, $el[0]) ) continue ; //現在の見出しのレベル $lv = ( strpos( $el[0], '<h2' ) !== false ) ? 2 : 3 ; //目次の生成 if ( $id === 1 ) { // 初回のみ別処理 if ( $lv === 2 ) { $html .= '<li class="c-list__item -lv_1"><a class="c-list__item-link -lv_1" href="#area-' . $id . '">' . $el[1] . '</a>'; } elseif ( $lv === 3 ) { $html .= '<li>'; $html .= '<ul class="c-list -second">'; $html .= '<li class="c-list__item -lv_2"><a class="c-list__item-link -lv_2" href="#area-' . $id . '">' . $el[1] . '</a>'; } } else { // 2回目以降 if ( $prevLv === 2 && $lv === 2 ) { /* heading-2 heading-2 */ $html .= '</li>'; $html .= '<li class="c-list__item -lv_1"><a class="c-list__item-link -lv_1" href="#area-' . $id . '">' . $el[1] . '</a>'; } elseif ( $prevLv < $lv ) { /* heading-2 heading-3 */ $html .= '<ul class="c-list -second">'; $html .= '<li class="c-list__item -lv_2"><a class="c-list__item-link -lv_2" href="#area-' . $id . '">' . $el[1] . '</a></li>'; } elseif ( $prevLv === 3 && $lv === 3 ) { /* heading-3 heading-3 */ $html .= '<li class="c-list__item -lv_2"><a class="c-list__item-link -lv_2" href="#area-' . $id . '">' . $el[1] . '</a></li>'; } elseif ( $prevLv > $lv ) { /* heading-3 heading-2 */ $html .= '</ul>'; $html .= '</li>'; $html .= '<li class="c-list__item -lv_1"><a class="c-list__item-link -lv_1" href="#area-' . $id . '">' . $el[1] . '</a>'; } } $prevLv = $lv; $id += 1; }; // 閉じタグ if ( $lv === 2 ) { $html .= '</li>'; } elseif ( $lv === 3 ) { $html .= '</ul></li>'; } $html .= '</ul></div></div>'; //出力 return array( 'index'=> $html, 'content'=> $content, ); }
仕様
add_index( $content: string, $f: number, $e: number );
$contentの内容を元に見出し$fから見出し$eの間の目次を生成します。
返り値はresult[‘index’]に目次、result[‘content’]に見出しにアンカーリンクが付いたコンテンツを返します。
2行目:場合によって処理をしない
記事のコンテンツがない場合、ページがsingleページでない場合は目次を生成する必要なないのでreturnして処理を終了します。
もし固定ページも使いたいという場合はis_page()を加えるなどして対応してください。
4行目:preg_match_allでコンテンツを取得する
preg_match_all関数を使用して正規表現にマッチする文字列を取得します。
preg_match_allの使い方は関数リファレンスpreg_match_allを参照してください。
preg_match_allをPREG_SET_ORDERオプションで使用すると以下のような値が返ってきます。
正規表現は、WordPressのエディタに用意されている見出しが1から6なので、h1からh6にマッチする形で制作しています。
echo '<pre>'; print_r($match_array); echo '</pre>'; //result Array ( [0] => Array ( [0] => <h2>見出し-2</h2> [1] => 見出し-2 ) [1] => Array ( [0] => <h2>見出し-2</h2> [1] => 見出し-2 ) [2] => Array ( [0] => <h3>見出し-3</h3> [1] => 見出し-3 ) [3] => Array ( [0] => <h3>見出し-3</h3> [1] => 見出し-3 ) [4] => Array ( [0] => <h2>見出し-2</h2> [1] => 見出し-2 ) [5] => Array ( [0] => <h4>見出し-4</h4> [1] => 見出し-4 ) [6] => Array ( [0] => <h5>見出し-5</h5> [1] => 見出し-5 ) )
このデータをもとに目次を制作していきます。
7~12行目:見出しが1つ以下なら処理を終了する
コンテンツに見出しがない、もしくは見出しが1つだけなら目次を制作する意味はありませんので、returnでコンテンツをそのまま返して終了です。
返す値はindexに目次(カラ)、contentにコンテンツが入る形になります。
22行目:目次の対象に含める見出しにマッチする正規表現を作ります
引数の$fと$eの数字を使ってh〇~h〇にマッチする正規表現を作ります。
引数に好きな数字を入力する事で目次に含める範囲を決める事ができます。
例えば、$fに2、$eに4を入れるとh2~h4にマッチする正規表現になり、目次にh2~h4のタイトルが表示されます。
26,27行目:コンテンツの見出しにリンクのアンカーを設置する
目次のタイトルをクリックした際に各見出しにジャンプできるようにコンテンツの中の各見出しにidを振って、アンカーリンクのとび先をマークします。
26行目でpreg_replaceを使用して置き換える値を作ります。preg_replaceの詳細は関数リファレンスpreg_replaceを参照してください。
27行目で記事コンテンツの中から4行目で制作したデータをもとに見出しを置換します。
29行目:22行目で制作した正規表現にマッチしない場合、処理をスキップする
目次の制作は4行目で制作したデータを元にループを回して各見出しから作っていきます。
目次の対象にしたい見出し以外は目次に含める必要がりませんので、22行目で制作した正規表現を元に、この処理から下の処理はスキップします。
32行目:cssでデザインを再現するために見出しのレベルを分ける
ここはhtmlの構造によって変化しますが、今回のようにh3以下は全て第2階層と見なす場合、こうなります。
35行目~88行目:32行目の情報を元に目次を制作していきます
32行目で振り分けた見出しレベルの情報を元に条件分岐を使用して、適切なhtml構造いなるように見出しを制作していきます。
今回の場合、見出しのパターンは以下のようなパターンが発生します。
- h2とh2の順で隣接する場合
- h2とh3の順で隣接する場合
- h3とh3の順で隣接する場合
- h3とh2の順で隣接する場合
ただ、初回のみhtml構造を変えたいので、初回のみの処理を別いしています。
91~94行目:値を返す
制作したデータを返す処理です。
データの形式はindexに目次を、contentに記事の内容を入れて返します。
singleページで呼び出して使用する
制作した関数を使用したい場所で呼び出して使用します。
single-hoge.php
$result = add_index($content, 2, 3); // 目次 echo $result['index']; // 記事本文 echo apply_filters( 'the_content', $result['content'] );
こうする事で、この場合は引数に2,3を入れているのでh2とh3の見出しの目次を表示してくれます。
5行目ですが、$result[‘content’]でコンテンツを表示する事が出来るのですが、このままだとWordPressの恩恵を受ける事ができません。
例えばショートコードがただの文字列として表示されたり、WordPressのエディタから自動で入るpタグが入らなかったりします。
the_contentで出力したような挙動をさせるためには
filters( ‘the_content’, $sontent)を使う必要があります。