子テーマの functions.php に関数を一つ書き足したら、目次が本文の一番上に出るようになりました。実質的に書き換えたのは2行です。
Cocoon の目次は、本文中の最初の H2 タグの直前に挿入されます。設定画面に位置を変える項目はありません。preg_replace で H2 を探している処理を、本文冒頭への単純な連結に置き換えれば、目次は記事の頭へ動きます。
そう書くと2分で終わる話ですが、ここへたどり着くまでに数日使いました。設定タブを何度も見直し、検索を続け、実装してからカテゴリーページを壊しています。同じところで止まっている方のために、遠回りのほうから先に書きます。
設定タブに項目があるはずだと思い込んでいた
記事を書いていて自分のサイトを開いたら、目次が本文の途中に出ていました。導入文を4段落ほど書いて、最初の H2 に入ったところです。読み手からすれば、目次にたどり着くまでにそれなりの分量を読み終えていることになります。
Cocoon には、プラグインを入れなくても使える目次機能が標準で付いています。「Cocoon 設定」→「目次」タブを開くと、表示・非表示の切り替え、タイトル文言、見出しの深さ、何個以上の見出しで出すか、開閉ボタンの有無といった項目が並びます。


上が実際の目次タブで、いま見ても位置に関する項目はありません。下は、デフォルトで目次がどこに入るかを描いたものです。私はこのタブを3回くらい行ったり来たりしました。位置、表示場所、挿入箇所、Position。それらしい語を探して何度もスクロールしたのですが、表示位置を変える項目はどこにもありませんでした。あるはずだという前提で見ていたので、見つからないことに納得できなかったわけです。
記事構成で書くと、デフォルトの挿入位置はこうなります。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | [アイキャッチ画像] 導入文 (p タグ) 導入文 (p タグ) 導入文 (p タグ) ## 最初の見出し ← ここの直前に目次 本文 ## 次の見出し 本文 |
導入文が1行で済む記事なら、目次は実質的にほぼ冒頭へ出ます。4〜5段落書く記事だと、目次は数スクロール分だけ下へずれます。記事の全体像を先に把握したい読者は、そこまでスクロールするか、目次に気づかないまま本文を読み進めることになります。私は導入文を書いてから H2 に入るスタイルなので、これがずっと気になっていました。H3 から見出しを始める記事だと、設定によっては目次そのものが出ないこともあります。
数日かけて検索して、何も見つからなかった
Cocoon 目次 位置、Cocoon 目次 冒頭、Cocoon 目次 カスタマイズ。この種のキーワードで数日検索しました。国内で利用者の多いテーマなので、同じ悩みを書いた人が必ずいるはずだ、という前提で探していたわけです。ところが、設定タブの使い方を説明した記事はいくらでも出てくるのに、表示位置を変える話にはたどり着けませんでした。
目次の位置という、売上にも表示速度にも関係のない一点に数日使っている自分に、途中で少し笑えてきました。それでも気になるものは気になります。
設定で変えられず、検索でも出てこない。読むしかありません。lib/toc.php を開いたのは、そこからです。
カテゴリーページに目次が出て、慌てて直した
先に、実装したあとで踏んだほうの失敗を書きます。
最初のバージョンでは、表示の判定を親テーマと同じ is_total_the_page_toc_visible() だけにしていました。挿入処理を「本文の先頭に連結する」という雑な形にしたので、カテゴリーアーカイブとタグアーカイブの本文、つまり各記事の抜粋がまとまっている部分の先頭にも、目次が出ました。
サイトを開いたら、カテゴリーページの一番上、本来は記事一覧があるはずの場所に、見出しらしきリンク群がドンと出ています。目次がアーカイブのデザインを乗っ取ったように見えて、ぎょっとしました。原因は単純で、アーカイブ用の本文フィルターでも同じ関数が動いていただけです。
lib/toc.php を読んだとき、the_content に加えて the_category_content と the_tag_content にも同じフィルターが登録されているのは目に入っていました。目に入っていたのに、最初のコードには反映していません。
いま動いているコードを、先に全部置きます
子テーマの functions.php に追記しているのは、これで全部です。長く見えますが、親テーマの関数をコピーしてきて、挿入処理と表示判定の2か所を書き換えただけです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /** * 目次を本文の先頭に表示する (最初の H2 タグの前ではなく) * 親テーマの add_toc_before_1st_h2 関数を上書き */ function add_toc_before_1st_h2($the_content){ global $_TOC_WIDGET_OR_SHORTCODE_USED; // Table of Contents Plus が有効な場合は何もしない if (class_exists( 'toc' )) { return $the_content; } // プラグインのフォーラムページでは無効 if (is_plugin_fourm_page()) { return $the_content; } // ページ側で目次を非表示にしていて、ショートコードも使っていない場合 if (!is_total_the_page_toc_visible() && !$_TOC_WIDGET_OR_SHORTCODE_USED && !is_active_widget( false, false, 'toc', true )) { return $the_content; } $harray = array(); $depth = intval(get_toc_depth()); $set_depth = $depth; if (intval($set_depth) == 0) { $set_depth = 6; } $html = get_toc_tag($the_content, $harray); // 目次タグが空 (= 目次が不要) の場合は本文をそのまま返す if (!$html) { return $the_content; } // 見出しに id を振っていく処理 (元のまま残す) $res = preg_match_all('/(<('.implode('|', $harray).')[^>]*?>)(.*?)(<\/h[2-6]>)/is', $the_content, $m); $tag_all_index = 0; $tag_index = 1; $h_index = 2; $h_content_index = 3; $tag_end_index = 4; if ($res && $m[0]) { $i = 0; $count = 1; foreach ($m[$tag_all_index] as $value) { $tag_all = $m[$tag_all_index][$i]; $tag = $m[$tag_index][$i]; $h = $m[$h_index][$i]; $h_content = get_h_inner_content($m[$h_content_index][$i]); $tag_end = $m[$tag_end_index][$i]; $now_depth = intval(str_replace('h', '', $h)); // 設定の深さより深い見出しはスキップ if ($set_depth < $now_depth) { $i++; continue; } $new = $tag.'<span id="toc'.strval($count).'">'.$h_content.'</span>'.$tag_end; $the_content = preg_replace('/'.preg_quote($value, '/').'/', $new, $the_content, 1); $i++; $count++; } } // ここが今回の核心 // is_singular() を追加して投稿・固定ページ以外には出さないようにしている if (is_total_the_page_toc_visible() && is_singular()) { $html = str_replace('<div class="toc ', '<div id="toc" class="toc ', $html); // 本文の先頭に連結 (H2 を探さない) $the_content = $html . PHP_EOL . PHP_EOL . $the_content; } return $the_content; } |
lib/toc.php を読んだら、正体は1行だった
書き換える前に、Cocoon がどうやって目次の位置を決めているのかを読みました。仕組みを分からないまま触ると、関係のない場所まで壊しかねません。目次まわりは、Cocoon の中でこう分かれていました。
| 1 2 3 4 5 6 7 8 | cocoon-master/ ├── lib/ │ ├── toc.php ← メインの目次機能 │ ├── page-settings/ │ │ ├── toc-forms.php ← 目次設定フォーム │ │ └── toc-funcs.php ← 目次設定関数 │ └── widgets/ │ └── toc.php ← 目次ウィジェット |
いじりたいのは挿入位置なので、本体は lib/toc.php です。311行目あたりに、こんな登録がありました。
| 1 2 3 4 | //最初のH2タグの前に目次を挿入する add_filter('the_content', 'add_toc_before_1st_h2', get_toc_filter_priority()); add_filter('the_category_content', 'add_toc_before_1st_h2', get_toc_filter_priority()); add_filter('the_tag_content', 'add_toc_before_1st_h2', get_toc_filter_priority()); |
the_content フィルターで、本文が表示される前に add_toc_before_1st_h2 を通しています。関数名がそのまま挙動です。ここで the_category_content と the_tag_content にも同じ登録があるのが、さきほどの事故の伏線でした。
同じファイルの389行目あたりに、実際の挿入処理があります。
| 1 2 3 4 5 | if (is_total_the_page_toc_visible()) { $h2result = get_h2_included_in_body( $the_content ); $html = str_replace('<div class="toc ', '<div id="toc" class="toc ', $html); $the_content = preg_replace(H2_REG, $html.PHP_EOL.PHP_EOL.$h2result, $the_content, 1); } |
効いているのは最後の行です。
| 1 | $the_content = preg_replace(H2_REG, $html.PHP_EOL.PHP_EOL.$h2result, $the_content, 1); |
分解すると、H2_REG が H2 タグを検出する正規表現、$html が生成された目次の HTML、$h2result がマッチした H2 タグそのもの、最後の引数 1 が「最初にマッチした1つだけ置換する」という指定です。正規表現で本文の最初の H2 を探し、その位置に目次を割り込ませている。これがデフォルト動作の正体でした。
H2_REG の定義も見ておくと、lib/ad.php の166行目にありました。
| 1 2 | if ( !defined('H2_REG') ) define('H2_REG', apply_filters('insertion_heading_regexp', '/<h2/i')); |
<h2 という文字列にマッチするだけの、素朴な正規表現です。/i フラグで大文字小文字は区別していません。
親テーマを触らずに、関数だけ差し替える
Cocoon は更新頻度の高いテーマです。親テーマのファイルを書き換えても動きはしますが、アップデートのたびに消えます。それは続けられないので、子テーマ「Cocoon Child」を使います。公式サイトでダウンロードできます。

コードを読んでいて気づいたのが、関数定義の書き方でした。
| 1 2 3 4 5 | if ( !function_exists( 'add_toc_before_1st_h2' ) ): function add_toc_before_1st_h2($the_content){ // 処理内容 } endif; |
function_exists で「この名前の関数がまだ定義されていなければ」と条件を付けて、その中で定義しています。先に同名の関数があれば、こちらは何もしない、というロジックです。WordPress は子テーマの functions.php を親テーマより先に読み込むので、子テーマで同じ名前の関数を定義しておけば、親テーマ側の判定が「もうあるから定義しない」になり、こちらの関数が使われます。
カスタマイズしやすいように、あえてこの書き方にしてあるようです。ここは素直にありがたかった。



上の3枚は、順に preg_replace で H2 を探して挿入する流れ、親テーマと子テーマの読み込み順と function_exists の判定、そして変更前と変更後の処理の違いです。
書き換えたのは2か所だけ
ひとつ目。親テーマでは、最初の H2 を正規表現で探して、その位置に目次を挿入していました。
| 1 | $the_content = preg_replace(H2_REG, $html.PHP_EOL.PHP_EOL.$h2result, $the_content, 1); |
子テーマでは、これを単純な文字列連結に変えました。
| 1 | $the_content = $html . PHP_EOL . PHP_EOL . $the_content; |
H2 タグを探さなくなったので、$h2result も get_h2_included_in_body() の呼び出しも要りません。
ふたつ目。親テーマの表示判定は、これだけでした。
| 1 | if (is_total_the_page_toc_visible()) { |
子テーマでは is_singular() を足しています。
| 1 | if (is_total_the_page_toc_visible() && is_singular()) { |
is_singular() は、個別の投稿・固定ページが表示されているかを判定する WordPress の関数です。これを足すと、目次が出るのは個別記事だけになります。最初のバージョンには、この1行が入っていませんでした。カテゴリーページを壊してから足しています。
1行だけ書けばいいのに、関数まるごとコピーした理由
挿入の1行だけ書き換えたいなら、その1行だけ書けばいいのでは。私も最初はそう考えました。
ただ、この関数の中には、挿入処理のほかにも残したい処理が入っています。Table of Contents Plus プラグインとの競合を避けるチェック、ページ単位の表示・非表示の判定、それに各見出しへ id="toc1" id="toc2" のような ID を振る処理。最後の ID 付与がないと、目次のリンクをクリックしても何も起きません。挿入位置だけ書き換えて元の関数を呼ぶ形も検討しましたが、それなら関数ごと差し替えるほうが素直だと判断しました。
動かしてから、ここを見た
変更前と変更後です。


導入文より前、本文の一番上に目次が出ています。H2 で始まる記事でも、p タグで始まる記事でも、同じ位置に出ます。
あとは順に、目次の各項目をクリックして対応する見出しまでスクロールするか。ID 付与処理を残しているので、ここが動かないならコードの貼り付けに漏れがあります。目次タブで設定したタイトル文言、見出しの深さ、開閉ボタン、表示条件が反映されているか。投稿編集画面の「目次を表示しない」にチェックを入れた記事で、目次が消えるか。そしてカテゴリーアーカイブとタグアーカイブを開いて、目次が出ていないこと。最後のひとつは、自分で壊した場所なので念入りに見ました。
うまく動かないときに見る場所

検証中に引っかかった順に書きます。目次が出てこないときは、ブラウザのキャッシュを消して Ctrl+F5 で強制リロード。キャッシュプラグインが入っていれば、そちらもクリアします。Cocoon 設定で目次が表示するになっているか、記事に表示条件を満たすだけの H2 / H3 があるかも見ます。
PHP エラーが出るときは、コピペで全角スペースが混ざっていないか。functions.php の先頭にすでに <?php があるのに、もう一つ書いていないか。波括弧の対応が合っているか。
目次が2つ出るなら、子テーマの関数定義が親テーマより後に読み込まれています。外観 → テーマで、子テーマが有効になっているか確認します。
カテゴリーページにも目次が出るなら、is_singular() が抜けています。条件式が if (is_total_the_page_toc_visible() && is_singular()) になっているか見直してください。&& の位置を間違えても文法上は通ってしまうので、閉じ括弧の位置ごとエディタで確認するのが確実です。
ショートコードでもできる、けれど選ばなかった
Cocoon には [ toc ] ショートコードもあります。自動表示をオフにして、好きな位置に書く方法です。記事ごとに目次の位置を変えたいなら、こちらのほうが向いています。
私の目的は全記事で本文冒頭に出すことだったので、毎回ショートコードを書く形は選びませんでした。書き忘れます。記事数が増えるほど、運用は雑になります。
遠回りのほうが長かった
差分そのものは小さい話でした。preg_replace の1行を、文字列連結の1行に置き換えただけです。
いちばん時間を使ったのは、設定タブに位置の項目があるはずだと信じて、画面を何度も見直していた時間でした。便利なテーマならこういう項目はあるはずだ、という思い込みが、画面を素直に読むのを邪魔していました。次に取った行動が検索で、これも数日続けて、表示位置のカスタマイズを扱った記事には行き当たりませんでした。ソースを開いてみれば、関数名を見ただけで挙動が想像できる構造です。function_exists でラップされていることに気づくのも、ファイルを開いてしまえばすぐでした。
検索して答えが見つからない問題は、ある意味では当たりだとも思いました。同じところで困っている人がいて、まだ誰も書いていないということだからです。だからこの記事を残しています。Cocoon は function_exists でラップされた関数が多いので、目次に限らず、設定では届かない挙動を子テーマで書き換えられるはずです。
ただ、設定タブを諦めてからソースを開くまでの数日を、どこまで詰めればよかったのかは、いまも決めかねています。設定で変えられないと分かった時点で、すぐコードを読みに行くべきだったのか。それとも、数日検索してから読みに行くくらいが、ちょうどよかったのか。あなたなら、どのあたりで見切りをつけますか。



コメント