記事の途中に、目次がもう 1 つ出ていた
管理しているサイトで、テーマのページビルダーで作った記事を見ていたときのことです。目次は Table of Contents Plus というプラグインで出しているのですが、記事の初めの目次とは別に、記事の途中にもう 1 つ目次が出ていました。
調べてみると、テーマのページビルダーは、部品ごとに the_content のフィルターを通していました。Table of Contents Plus は the_content を通るたびに見出しを数えて目次を入れるので、見出しのある部品が 2 つあると、それぞれの頭に目次が入ってしまいます。
うまくいかなかったこと
2 つ目の部品に を入れる
Table of Contents Plus には、その記事で目次を出さないための というショートコードがあります。2 つ目の部品にだけ入れれば、そこの目次だけが消えるかと思ったのですが、実際には記事全体の目次が消えてしまいました。部品ごとに分けて効くものではなく、記事全体に効くものだったようです。
the_content のフィルターで 2 つ目を外す
次に、the_content のフィルターを優先度 1000 で足して、2 つ目からの目次を外そうとしました。ところが、作業環境で数えてみると、the_content は関連記事などからも呼ばれていて、1 ページを出すあいだに 200 回近く通っていました。どの呼び出しが画面に出る本文の目次なのかを見分けられず、この方法では効きませんでした。
直し方:出力の直前の HTML で、最初の目次だけを残す
1 回ずつの the_content では見分けられないので、ページの HTML がすべてできあがったところで直すことにしました。template_redirect のタイミングで出力のバッファ(ob_start)を始めておくと、ページを出し終える直前に、HTML 全体を 1 つの文字列として受け取れます。そこで目次を順に探し、最初の 1 つだけを残します。
add_action( 'template_redirect', function () {
if ( is_singular() && ! is_feed() ) {
ob_start( 'my_toc_keep_first' );
}
}, 1 );
function my_toc_keep_first( $buffer ) {
// 目次が 2 つ未満なら何もしない
if ( substr_count( $buffer, 'id="toc_container"' ) < 2 ) {
return $buffer;
}
$count = 0;
$result = preg_replace_callback(
'#<div id="toc_container"[^>]*>.*?</ul>\s*</div>#s',
function ( $matches ) use ( &$count ) {
$count++;
return ( 1 === $count ) ? $matches[0] : '';
},
$buffer
);
// 正規表現が失敗したときは、元の HTML をそのまま返す
return ( null === $result ) ? $buffer : $result;
}
| 書き方 | 役目 |
|---|---|
template_redirect(優先度 1) |
テンプレートを出し始める前に動く。ここでバッファを始めれば、ページ全体を受け取れる |
is_singular() && ! is_feed() |
記事や固定ページを 1 つ出すときだけ動かす。一覧やフィードでは何もしない |
ob_start( 'my_toc_keep_first' ) |
出力をためておき、出し終える直前に関数に渡す |
substr_count( … ) < 2 |
目次が 0 か 1 つのページは、正規表現を動かさずにそのまま返す |
preg_replace_callback |
目次を見つけるたびに関数を呼ぶ。1 つ目はそのまま、2 つ目からは空にする |
null === $result |
正規表現が失敗すると null が返るので、そのときは元の HTML を返して、ページが真っ白にならないようにする |
正規表現は、目次の外側の div から、</ul> のすぐあとに </div> が来るところまでを 1 つの目次として見ています。目次の中に h3 の入れ子の ul があっても、入れ子の </ul> のあとは </li> が続くので、そこでは止まりません。s の指定で、改行をはさんでも 1 つの目次として扱えるようにしています。
結果
作業環境で、目次が 2 つ出ていた記事と、もともと目次が 1 つの記事を見比べました。
| 記事 | 直す前 | 直したあと |
|---|---|---|
| 見出しのある部品が 2 つある記事 | 目次が 2 つ | 目次が 1 つ(記事の初めの目次だけ残った) |
| 目次が 1 つの記事 | 目次が 1 つ | 変わらない |
気をつけること
- 出力のバッファを使うので、ほかにも出力を書き換えるプラグイン(キャッシュや圧縮など)があるときは、表示が変わらないかを確かめます
- Table of Contents Plus の目次の HTML の形が変わると、正規表現が合わなくなります。プラグインを更新したあとは、目次が 1 つ出ているかを見ておきます
- 残るのは、HTML の中で最初に出てくる目次です。記事の初めより前に別の目次が出る作りのテーマでは、残したい目次と違うものが残ることがあります(今回検証したサイトでは、記事の初めの目次が残ることを確認しました)
まとめ
- テーマのページビルダーは部品ごとに
the_contentを通すので、Table of Contents Plus の目次が部品の数だけ出ることがある は記事全体に効き、the_contentのフィルターでは 1 ページに何度も呼ばれて見分けられないtemplate_redirectでob_startを始め、出力の直前の HTML で最初の目次だけを残すと、2 つ目からを外せる- 目次が 1 つのページはそのまま返し、正規表現が失敗したときも元の HTML を返す


