Table of Contents Plus の目次が 2 つ出る:最初の 1 つだけ残す

広告WordPress・PHP を学べる本

Amazonのアソシエイトとして、WordPress functions.phpは適格販売により収入を得ています。

記事の途中に、目次がもう 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 つだけを残します。

functions.php に足すコード
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 を返す

参考

関連機能

  1. WordPress PDFや配布ファイルのダウンロード数を確認できるようにするプラグイン Download Monitor

  2. WordPress ダウンロード販売機能を Stripe + WP Simple Pay プラグインで実装する方法

  3. WordPress のサイトを閉じる手順:バックアップを取って消し、すべての URL に 410 と閉鎖のページを返す

サイドバー

よく使うカスタマイズ

最近の記事

Profile

PAGE TOP