WordPress:目次をプラグインなしで自動作成、自動表示する

このブログはまだはてなブログですが、WordPress に移行したサイトがあり、プラグインなしで目次を自動作成、自動表示する機能を作成しました。



設計方針

  • 投稿の新規追加、更新時に目次を作成する
  • 目次は h2, h3 要素から作成する(h4〜も可)
  • 目次はカスタムフィールドに保存する
  • 目次の表示は投稿ページ表示時にカスタムフィールドを呼び出す
  • すでに目次が挿入されている記事があることを想定する


functions.php

アクションフック save_post を使って、記事の投稿、更新時に目次を作成してカスタムフィールドに保存します。


/*
 目次作成 登録、更新時にカスタムフィールド toc に保存する
 記事内に {toc} を書いておくと single.php から呼び出される
*/
add_action( 'save_post', 'create_table_of_contents', 10, 3 );
function create_table_of_contents( $post_ID, $post, $update ) {

    // class-wp-widget-text.php のコードをを利用
    $doc = new DOMDocument();

    // Suppress warnings generated by loadHTML.
    $errors = libxml_use_internal_errors( true );
    // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    @$doc->loadHTML(
        sprintf(
            '<!DOCTYPE html><html><head><meta charset="%s"></head><body>%s</body></html>',
            esc_attr( get_bloginfo( 'charset' ) ),
            $post->post_content
        )
    );
    libxml_use_internal_errors( $errors );
    // ここまでclass-wp-widget-text.php のコードをを利用

    $dom = new DOMXPath($doc);
    $children = $dom->query( '//body/*' );
    $toc = '<ul class="table-of-contents">';
    $h2 = false;
    $h3 = false;
    foreach($children as $child){
        $tag = $child->tagName;
        if($tag == 'h2' && !$h2){
            $id = $child->nodeValue;
            $child->setAttribute('id', $id);
            $toc .= '<li><a href="#' . $id . '">' . $child->nodeValue . '</a>';
            $h2 = true;
        }else if($tag == 'h3'){
            if(!$h3){
                $toc .= '<ul>';
                $h3 = true;
            }
            $id = $child->nodeValue;
            $child->setAttribute('id', $id);
            $toc .= '<li><a href="#' . $id . '">' . $child->nodeValue . '</a></li>';
        }else if($tag == 'h2' && $h2){
            if($h3){
                $toc .= '</ul>';
                $h3 = false;
            }
            $id = $child->nodeValue;
            $child->setAttribute('id', $id);
            $toc .= '</li><li><a href="#' . $id . '">' . $child->nodeValue . '</a>';
        }
    }
    if($h3){
        $toc .= '</ul>';
    }
    $toc .= '</li></ul>';

    $html = $doc->saveXML();

    preg_match('/<body>(.*)<\/body>/is', $html, $matches);
    $post->post_content = $matches[1];
    remove_action('save_post','create_table_of_contents', 10, 3);
    wp_update_post($post);
    add_action('save_post','create_table_of_contents', 10, 3);

    $result =  add_post_meta($post_ID, 'toc', $toc, true);
    if(!$result){
        update_post_meta($post_ID, 'toc', $toc);
    }
}


アクションフック save_post は投稿が保存された直後に実行されますので、保存されたデータの h2, h3 要素を DOMDocument と DOMXPath を使ってパースし目次用のリストを作成します。同時に、h2, h3 要素に要素の内容を id として追加し、目次からのページ内リンクを貼ります。


h2, h3 要素の id を追加していますので投稿内容を再保存する必要があります。ただ、そのまま wp_update_post($post); としますと無限ループに陥りますので、いったんアクションフックを削除し、保存後に再設定しています。


カスタムフィールドへの保存は、新規保存をし、すでに同名キーが存在すれば追加保存しています。

なお、カスタムフィールドのキー名の先頭にアンダーバー _ を使いますとそのカスタムフィールドはスティルスになります。


single.php

目次の表示は、記事内の目次を表示したい位置に {toc} を挿入しておき、single.php でカスタムフィールドの目次を呼び出して置き換えます。

ショートコードを使う方法もありますが、私は AdSense を挿入するためにフィルターフックを使っていますので下のようにこの方法でやっています。


add_filter('the_content', 'add_toc');
function add_toc($content){

    global $post;

    if( strpos( $content, '{toc}') !== false ){
        $meta_value = get_post_meta( $post->ID, 'toc', true );
        $content = str_replace( '{toc}', $meta_value, $content );
    }
}


あとは目次 class="table-of-contents" をすきに装飾するだけです。


  • この方法で目次を表示しているサイト

movieimpressions.com


ライセンス等

ご使用の場合は以下の注意事項をお守りください。

  • ライセンスは IMUZA.com にあります。
  • 紹介は歓迎ですが、バグ対応ができなくなりますので転載はしないでください。
  • 紹介していただく場合は、当記事へのリンクをお願いします。
  • 自己責任でお使いください。
  • お問い合わせ、バグの報告、仕様変更のご要望等は Contact Us までお願いします。