HOPES

WordPressの投稿記事の目次を自動作成する機能を自作で実装

WordPressの投稿記事の目次を自動作成する機能を自作で実装

オリジナルの関数を作成し、プラグインなしで、WordPressの投稿記事の文章を解析し、見出しタグをもとに、リンク付きの目次を自動作成して、投稿記事に追加する機能を自作して実装したのでその方法を紹介します。

設計と前提条件

投稿記事の目次を自動作成するあたって、今回は以下のことを前提としています。

  • プラグインなしで、自作の関数等をつかって実装する。
  • 見出しタグを基準にし、これを元に目次を生成する。
  • 見出しタグにid属性がない場合には、id名を付加して、それをハッシュリンク名とする。
  • 目次を表示する位置は、指定するショートコードが記載された位置に挿入する。

投稿記事を解析して目次を自動作成

投稿記事を解析して、見出しを収集して、投稿内容を必要に応じて変更し、目次のHTMLを自動生成して、そのデータを保存する処理を作成します。

概ね以下の処理の流れになります。

  1. 記事の作成、更新の度に内容を確認して目次を作成、更新する。
  2. 見出しタグにid属性がない場合には、id名を付加して、変更した内容を投稿登録する。
  3. 生成した目次のHTMLデータを投稿と紐付けて保存する。

記事の作成、更新のアクションフック

WordPressには、記事の作成や更新が実行された時に実行されるアクションが用意されています。これをフックに処理を行うようにします。

//---------
// 目次設定
//---------
function table_of_contents_create($id,$post,$update){

}
add_action('save_post','table_of_contents_create', 10, 3);

見出しの収集とリンク先の設定

HTMLデータの解析には、DOMDocumentクラスを利用します。HOPESブログでは、投稿記事の見出しは、h3とh4タグのみとなっているので、パース処理もその前提となっています。また、名前リストが定義されており、idがない場合にid名を付与して、そのid名がハッシュリンクのリンク先となるよう設定します。

$name_list = array("first","second","third","fourth","fifth","sixth","seventh","eighth","ninth","tenth","eleventh","twelfth","thirteenth","fourteenth","fifteenth","sixteenth","seventeenth","eighteenth","nineteenth","twentieth");

// 見出しの収集
$lists = array();
$dom = new DOMDocument;
libxml_use_internal_errors(true);
$dom->loadHTML(mb_convert_encoding($post->post_content, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();

$section_tags = $dom->getElementsByTagName('section');

for ($i=0; $i<$section_tags->length; $i++)
{
    $data  = array();

    // h3の取得
    $h3_data = array();
    $h3_tags  = $section_tags->item($i)->getElementsByTagName('h3');

    for ($j=0; $j< $h3_tags->length; $j++)
    {
        if($h3_tags->item($j)->hasAttribute('id'))
        {
            $h3_name = $h3_tags->item($j)->getAttributeNode('id')->textContent;

        }else{
            $h3_name = $name_list[$i];
            $h3_tags->item($j)->setAttribute('id',$h3_name);
        }

        $h3_item["url"]   = "#" .$h3_name;
        $h3_item["title"] = $h3_tags->item($j)->nodeValue;

        $h3_data[] = $h3_item;
    }
    if($h3_data){
        $data["h3"] = $h3_data;
    }

    // h4の取得
    $h4_data = array();
    $h4_tags  = $section_tags->item($i)->getElementsByTagName('h4');
    for ($j=0; $j< $h4_tags->length; $j++)
    {
        if($h4_tags->item($j)->hasAttribute('id'))
        {
            $h4_name = $h4_tags->item($j)->getAttributeNode('id')->textContent;

        }else{
            $h4_name = $h3_name ."-" .$name_list[$j];
            $h4_tags->item($j)->setAttribute('id',$h4_name);
        }

        $h4_item["url"]   = "#" .$h4_name;
        $h4_item["title"] = $h4_tags->item($j)->nodeValue;

        $h4_data[] = $h4_item;
    }
    if($h4_data){
        $data["h4"] = $h4_data;
    }

    // リンクデータを格納
    $lists[] = $data;
}
var_dump($lists);

リンクデータのリストをダンプすると、リンク情報が、配列に格納されていることが確認できます。

投稿データの更新

投稿されたデータに、自動的にid属性等をつけるように改変することがあるので、処理で改変したデータを投稿データとして登録します。

この際に気をつけることとして、自動で更新処理を行うことで、無限ループが発生することになるので、これを避けるために、一旦フックを解除し、終わってから改めてフックするようにします。

wp_update_post() のようにフック save_post を含む関数を呼び出すと、フックした関数が無限ループを引き起こします。

プラグイン API/アクションフック一覧/save post – WordPress Codex 日本語版

以下が改変した投稿データを登録する処理になります。

// 改変投稿データの登録
$dom_html = $dom->saveHTML($dom->documentElement);
preg_match("/<html><body>(.*)<\/body><\/html>/is", $dom_html, $matches);
$post->post_content = $matches[1];
remove_action('save_post','table_of_contents_create', 10, 3);
$result = wp_update_post($post);
add_action('save_post','table_of_contents_create', 10, 3);

HTMLデータの形成

先の処理で格納したリンク情報をもとに、HTMLを自動形成する処理を設定します。

// HTMLの形成
$toc = null;
$toc .= "<nav class=\"toc\">\n<h3>目次</h3>\n<ol class=\"h3link\">\n";
foreach($lists as $list)
{
    $toc .= "<li>";
    foreach($list["h3"] as $h3)
    {
        $toc .= "<a href=\"{$h3["url"]}\">{$h3["title"]}</a>";
    }

    if(isset($list["h4"]) && is_array($list["h4"]))
    {
        $toc .= "\n<ol class=\"h4link\">\n";

        foreach($list["h4"] as $h4)
        {
            $toc .= "<li ><a href=\"{$h4["url"]}\">{$h4["title"]}</a></li>\n";
        }
        $toc .= "</ol>\n";
    }
    $toc .= "</li>\n";
}
$toc .= "</ol>\n</nav>";
var_dump($toc);

処理されたデータをダンプすると、目次のHTMLが生成されていることが確認できます。

カスタムフィールドとして登録

自動形成したHTMLデータを投稿IDと紐付けて、カスタムフィールドのデータとして登録します。

add_post_metaでは、パラメータの$uniqueをtrueにすることで、すでにカスタムフィールドが存在していれば追加されません。また、このデータは、コントロール画面等から編集を行うデータではないので、見えないカスタムフィールドとします。

キーが存在しない場合は新しくカスタムフィールドを追加し、キーが存在する場合はカスタムフィールドを更新します。

“_”(アンダースコア)で始まるキーを持つカスタムフィールドは、投稿や固定ページの編集画面に表示されず、the_meta() では取得されないので、そのような内部的な 見えない パラメータを扱うのに適しています。

関数リファレンス/add post meta – WordPress Codex 日本語版

それらを前提として以下のように設定します。

// tocデータの登録
$result =  add_post_meta($id, "_table_of_contents", $toc, true);
if(!$result)
{
    update_post_meta($id, " _table_of_contents", $toc);
}

目次の表示

目次データの自動生成と更新の仕組みが完了したので、実際に投稿のページに表示するように設定を行います。

ショートコードの設定

ショートコード機能を使って、ショートコードの設定を行います。「table_of_contents」というショートコードが呼ばれたら登録してある目次データを取り出して表示するようにします。

// 目次の表示
function table_of_contents_print() {

    $id = get_the_ID();
    $table_of_contents = get_post_meta($id, "_table_of_contents", true);

    return $table_of_contents;
}
add_shortcode('table_of_contents', 'table_of_contents_print');

動作の確認

コントロールパネルの記事の投稿欄に以下のように、目次を表示したい位置に「table_of_contents」とショートコードを記載します。

「table_of_contents」ショートコードの記載

すると、その記事の目次のHTMLが表示されます。

例えば、この記事の目次の自動生成されたHTMLは以下のようになっています。

<nav class="toc">
<h3>目次</h3>
<ol class="h3link">
<li><a href="#first">設計と前提条件</a></li>
<li><a href="#second">投稿記事を解析して目次を自動作成</a>
<ol class="h4link">
<li ><a href="#second-first">記事の作成、更新のアクションフック</a></li>
<li ><a href="#second-second">見出しの収集とリンク先の設定</a></li>
<li ><a href="#second-third">投稿データの更新</a></li>
<li ><a href="#second-fourth">HTMLデータの形成</a></li>
<li ><a href="#second-fifth">カスタムフィールドとして登録</a></li>
</ol>
</li>
<li><a href="#third">目次の表示</a>
<ol class="h4link">
<li ><a href="#third-first">ショートコードの設定</a></li>
<li ><a href="#third-second">動作の確認</a></li>
</ol>
</li>
<li><a href="#fourth">実装ソースコードの全体とまとめ</a></li>
</ol>
</nav>

あとは、CSSを設定すれば、以下のように表示されます。

目次の表示

実装ソースコードの全体とまとめ

ここまでで、目次の自動作成の実装から、表示までの方法は確認できました。

まとめとして、実際にfuncions.phpには、どのように設定されているのか全体通してののソースコードを以下に記載します。

説明を確認しながら、全体のソースコードを読むと分かりやすいかと思います。

//---------
// 目次設定
//---------
function table_of_contents_create($id,$post,$update){

    if(empty($post->post_content))
    {
        return;
    }

    $name_list = array("first","second","third","fourth","fifth","sixth","seventh","eighth","ninth","tenth","eleventh","twelfth","thirteenth","fourteenth","fifteenth","sixteenth","seventeenth","eighteenth","nineteenth","twentieth");

    // 見出しの収集
    $lists = array();
    $dom = new DOMDocument;
    libxml_use_internal_errors(true);
    $dom->loadHTML(mb_convert_encoding($post->post_content, 'HTML-ENTITIES', 'UTF-8'));
    libxml_clear_errors();

    $section_tags = $dom->getElementsByTagName('section');

    for ($i=0; $i<$section_tags->length; $i++)
    {
        $data  = array();

        // h3の取得
        $h3_data = array();
        $h3_tags  = $section_tags->item($i)->getElementsByTagName('h3');

        for ($j=0; $j< $h3_tags->length; $j++)
        {
            if($h3_tags->item($j)->hasAttribute('id'))
            {
                $h3_name = $h3_tags->item($j)->getAttributeNode('id')->textContent;

            }else{
                $h3_name = $name_list[$i];
                $h3_tags->item($j)->setAttribute('id',$h3_name);
            }

            $h3_item["url"]   = "#" .$h3_name;
            $h3_item["title"] = $h3_tags->item($j)->nodeValue;

            $h3_data[] = $h3_item;
        }
        if($h3_data){
            $data["h3"] = $h3_data;
        }

        // h4の取得
        $h4_data = array();
        $h4_tags  = $section_tags->item($i)->getElementsByTagName('h4');
        for ($j=0; $j< $h4_tags->length; $j++)
        {
            if($h4_tags->item($j)->hasAttribute('id'))
            {
                $h4_name = $h4_tags->item($j)->getAttributeNode('id')->textContent;

            }else{
                $h4_name = $h3_name ."-" .$name_list[$j];
                $h4_tags->item($j)->setAttribute('id',$h4_name);
            }

            $h4_item["url"]   = "#" .$h4_name;
            $h4_item["title"] = $h4_tags->item($j)->nodeValue;

            $h4_data[] = $h4_item;
        }
        if($h4_data){
            $data["h4"] = $h4_data;
        }

        // リンクデータを格納
        $lists[] = $data;
    }

    // 改変投稿データの登録
    $dom_html = $dom->saveHTML($dom->documentElement);
    preg_match("/<html><body>(.*)<\/body><\/html>/is", $dom_html, $matches);
    $post->post_content = $matches[1];
    remove_action('save_post','table_of_contents_create', 10, 3);
    $result = wp_update_post($post);
    add_action('save_post','table_of_contents_create', 10, 3);

    // HTMLの形成
    $toc = null;
    $toc .= "<nav class=\"toc\">\n<h3>目次</h3>\n<ol class=\"h3link\">\n";
    foreach($lists as $list)
    {
        $toc .= "<li>";
        foreach($list["h3"] as $h3)
        {
            $toc .= "<a href=\"{$h3["url"]}\">{$h3["title"]}</a>";
        }

        if(isset($list["h4"]) && is_array($list["h4"]))
        {
            $toc .= "\n<ol class=\"h4link\">\n";

            foreach($list["h4"] as $h4)
            {
                $toc .= "<li ><a href=\"{$h4["url"]}\">{$h4["title"]}</a></li>\n";
            }
            $toc .= "</ol>\n";
        }
        $toc .= "</li>\n";
    }
    $toc .= "</ol>\n</nav>";

    // tocデータの登録
    $result =  add_post_meta($id, "_table_of_contents", $toc, true);
    if(!$result)
    {
        update_post_meta($id, "_table_of_contents", $toc);
    }
}
add_action('save_post','table_of_contents_create', 10, 3);

// 目次の表示
function table_of_contents_print() {

    $id = get_the_ID();
    $table_of_contents = get_post_meta($id, "_table_of_contents", true);

    return $table_of_contents;
}
add_shortcode('table_of_contents', 'table_of_contents_print');

2022年01月16日に投稿されました。

2022年11月10日に更新されました。