【WordPress・PHP】投稿ページの目次を自動で出力する例|プラグインなし

WordPressでウェブサイトやブログを作成していると、ページ内に目次を作成したい場合があるかもしれませんが、毎回、自分自身で目次を作成するのは面倒だと思います。
今回は、そのような場合に使用できるソースコードの例となり、プラグインを使わずPHPでページ内の目次を自動で作成し出力します。
また、PHPではなく、jQueryを使い目次を出力することも可能です。詳しくは以下のリンク先を参考にしてみてください。
WordPressで投稿ページの目次を自動出力する例
目次が作成される条件
今回の例では、投稿ページの本文内に、属性を持たないh2・h3要素が3つ以上ある場合、それらの要素から自動で目次を作成して出力します。
また、目次の先頭の項目がh2要素となる場合のみに使用できます。
作成する目次の特徴
目次として出力されるHTMLはol要素で出力され、目次の項目にはページ内のそれぞれの箇所へのアンカーリンクを追加します。
また、ページ内でh2要素の次にh3要素がある場合には、目次が階層化されol要素がネスト(入れ子)として出力されます。
ソースコードの例
以下がソースコード例となり、functions.phpに記述します。
function my_add_content( $content ) {
if ( is_single() ) {
// 属性を持たないh2・h3要素を正規表現で表すパターン
$pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
// 本文の中から、すべてのh2・h3要素を検索
preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );
// ページ内のh2・h3要素が3つ以上の場合に目次を出力
if( count( $matches ) > 3 ){
// 目次の出力に使用する変数
$toc = '<h2>目次</h2><ol>';
// 目次の階層の判断に使用する変数
$hierarchy = NULL;
// ループ回数を数える変数
$i = 0;
// 本文内のh2・h3要素を上から順番にループで処理
foreach( $matches as $element ){
// ループ回数を1加算
$i++;
// h2・h3に指定するIDの属性値を作成
$id = 'chapter-' . $i;
// h2・h3タグにIDを追加
$chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/', '<$1 id ="' . $id . '">$2</$3>', $element[0] );
// ページ内のh2・h3要素を、IDが追加されているh2・h3要素に置換
$content = preg_replace( $pattern, $chapter, $content, 1);
// 現在のループで扱う要素を判断する条件分岐
if( strpos( $element[0], '<h2' ) === 0 ){
$level = 0;
}else{
$level = 1;
}
//現在の状態を判断する条件分岐
if( $hierarchy === $level ){ // h2またはh3がそれぞれ連続する場合
$toc .= '</li>';
}elseif( $hierarchy < $level ){ // h2の次がh3となる場合
$toc .= '<ol>';
$hierarchy = 1;
}elseif( $hierarchy > $level ){ // h3の次がh2となる場合
$toc .= '</li></ol></li>';
$hierarchy = 0;
}elseif( $i == 1 ){ // ループ1回目の場合
$hierarchy = 0;
}
// 目次の項目で使用する要素を指定
$title = $element[1];
// 目次の項目を作成。※次のループで<li>の直下に<ol>タグを出力する場合ががあるので、ここでは<li>タグを閉じていません。
$toc .= '<li><a href="#' . $id . '">' . $title . '</a>';
}
// 目次の最後の項目をどの要素から作成したかによりタグの閉じ方を変更
if( $level == 0 ){
$toc .= '</li></ol>';
}elseif( $level == 1 ){
$toc .= '</li></ol></li></ol>';
}
// 本文に目次を追加
$content = $toc . $content;
}
}
return $content;
}
add_filter( 'the_content', 'my_add_content' );
ソースコードの説明
例のソースコードでは、投稿や固定ページの本文を扱えるフィルターフックのthe_contentを使用して目次を作成します。
そのフィルターフックの関数内では、preg_match_allで検索したh2・h3要素を、foreachループで扱い目次を作成し、投稿ページの本文の直前に出力します。
以下は、ソースコードのポイントとなる箇所の説明となります。
本文の中からh2・h3要素を検索
以下の箇所では、投稿ページの本文内にあるh2・h3要素から目次を作成するため、まずはpreg_match_all()を使用して本文内にある属性を持たないh2・h3要素を検索します。
例のソースコード3行目からの箇所
// 属性を持たないh2・h3要素を正規表現で表すパターン
$pattern = '/<h[2-3]>(.*?)<\/h[2-3]>/i';
// 本文の中から、すべてのh2・h3要素を検索
preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );
preg_match_all関数は、第2引数に指定した文字列から、第1引数にしていした正規表現のパターンを検索し、第3引数にマッチした文字列を格納します。第4引数は、取得する配列の順序を指定します。
上記のpreg_match_all関数では引数は以下となります。
- 第1引数は、属性を持たないh2・h3要素を正規表現で表すパターン$pattern(検索する正規表現のパターン)を指定
- 第2引数は、記事の本文$content(検索の対象となる文字列)を指定
- 第3引数は、$matchesとし、検索でマッチした「h2要素」「h3要素」と「h2要素の内容」「h3要素の内容」をすべてを多次元配列で格納。
- 第4引数は、 PREG_SET_ORDERを指定
preg_match_all関数の詳細については、以下のリンク先を参考にしてみてください。
目次を出力する条件・変数を定義
以下の箇所では、ページ内のh2・h3要素が3つ以上の場合に、目次を出力する条件分岐を作成します。そして、ループ内で使用する変数を定義します。
例のソースコード8行目からの箇所
// ページ内のh2・h3要素が3つ以上の場合に目次を出力
if( count( $matches ) > 3 ){
// 目次の出力に使用する変数
$toc = '<h2>目次</h2><ol>';
// 目次の階層の判断に使用する変数
$hierarchy = NULL;
// ループ回数を数える変数
$i = 0;
// 中略
}
h2・h3タグにIDを追加
作成する目次では、目次内の項目をクリックするとページ内のh2・h3要素の箇所へ移動させるため、ページ内のh2・h3タグにIDを追加します。
そのため、以下の箇所では、ID属性を付与したh2・h3要素を、ページ内の属性を持たないh2・h3要素と置換します。
例のソースコード17行目からの箇所
// 本文内のh2・h3要素を上から順番にループで処理
foreach( $matches as $element ){
// ループ回数を1加算
$i++;
// h2・h3に指定するIDの属性値を作成
$id = 'chapter-' . $i;
// h2・h3タグにIDを追加
$chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/', '<$1 id ="' . $id . '">$2</$3>', $element[0] );
// ページ内のh2・h3要素を、IDが追加されているh2・h3要素に置換
$content = preg_replace( $pattern, $chapter, $content, 1);
// 中略
}
上記、foreachループで扱うpreg_match_all関数の引数$matchesは、検索してヒットした「要素」と「要素の内容」が格納されている多次元配列となります。
$matches内の$element[0]には「要素」が格納され、$element[1]には「要素の内容」が格納されます。
上記のforeachループ内の箇所では、preg_replaceを使い、$element[0]内のh2・h3要素に、id属性を付与したものを作成して$chapterに格納します。
そして、もう一度preg_replaceを使い、今度は本文内から$element[0]のh2・h3要素を検索して、id属性を付与したh2・h3要素に置換します。
また、preg_replace関数については以下のリンク先を参考にしてみてください。
現在のループで扱う要素を判断
以下の箇所では、現在のループで扱っている要素がh2かh3を判断して変数$levelの値を変更します。
例のソースコード28行目からの箇所
// 現在のループで扱う要素を判断する条件分岐
if( strpos( $element[0], '<h2' ) === 0 ){
$level = 0;
}else{
$level = 1;
}
現在の状態を判断する条件分岐
例では、foreachループでh2・h3要素を扱い目次を作成しますが、h2・h3要素の順番によって目次を入れ子にします。
そのため、以下の箇所では、現在の目次の階層を判断し、目次で使用するHTMLタグや変数$hierarchyの値を変更しています。
例のソースコード35行目からの箇所
//現在の状態を判断する条件分岐
if( $hierarchy === $level ){ // h2またはh3がそれぞれ連続する場合
$toc .= '</li>';
}elseif( $hierarchy < $level ){ // h2の次がh3となる場合
$toc .= '<ol>';
$hierarchy = 1;
}elseif( $hierarchy > $level ){ // h3の次がh2となる場合
$toc .= '</li></ol></li>';
$hierarchy = 0;
}elseif( $i == 1 ){ // ループ1回目の場合
$hierarchy = 0;
}
目次の項目を作成
以下の箇所では、「要素の内容」が格納されている$element[1]から、目次の項目を作成します。また、次のループで扱う要素によっては、HTMLタグを変更し目次の入れ子の状態を変更したいため、ここでは<li>タグを閉じていません。
例のソースコード48行目からの箇所
// 目次の項目で使用する要素を指定
$title = $element[1];
// 目次の項目を作成。※次のループで<li>の直下に<ol>タグを出力する場合ががあるので、ここでは<li>タグを閉じていません。
$toc .= '<li><a href="#' . $id . '">' . $title . '</a>';
目次の最後の項目をどの要素から作成したかによりタグの閉じ方を変更
foreachループから抜けると目次の項目はHTMLで作成された状態になりますが、目次の最後の項目をどの要素から作成したかによってHTMLタグの状態が変わります。
そのため、以下の箇所では、条件分岐を使い最後に閉じるHTMLタグを変更します。
例のソースコード54行目からの箇所
// 目次の最後の項目をどの要素から作成したかによりタグの閉じ方を変更
if( $level == 0 ){
$toc .= '</li></ol>';
}elseif( $level == 1 ){
$toc .= '</li></ol></li></ol>';
}