ブログ用タグ: WordPress

  • WordPress: イベントのスケジュールとcron

    イベントのスケジュール

    全体で8つの定期イベントがあり、バラバラに登録しているとデバッグが面倒なので、1ファイルにまとめている。

    class LF_Events {
        private static $events = [
            [ // LOTD更新(毎日0:01)
                'hook'       => 'lf_event_renew_lotd',
                'args'       => [],
                'timing'     => 'tomorrow 0:01',
                'recurrence' => 'daily',
                'callback'   => [ 'LF_LOTD', 'renew' ],
            ],
            [ // 以下同様
            ],
        ]
    
        public static function init() {
            foreach ( self::$events as $event ) {
    
                if ( ! wp_next_scheduled( $event['hook'], $event['args'] ) ) {
            
                    $etime = new DateTimeImmutable( $event['timing'], wp_timezone() );
            
                    $ret = wp_schedule_event(
                        $etime->getTimeStamp(),
                        $event['recurrence'],
                        $event['hook'],
                        $event['args'],
                        true
                    );
            
                    if ( ! $ret || is_wp_error( $ret ) ) {
                        $errmsg['location'] = __METHOD__.' '.__LINE__;
                        $errmsg['$event']   = $event;
                        $errmsg['$ret']     = $ret;
                        error_log( print_r( $errmsg, true ) );
                        continue;
                    }
                }
            
                add_action( $event['hook'], $event['callback'], 10, count( $event['args'] ) );
            }
        }
    }
    

    cron

    ユーザーアクセスによる疑似cronではなくサーバーのcronを使う。10分おきに wp-cron.php を起動。

    (/wp-config.php)
    define( 'DISABLE_WP_CRON', true );
    
    (crontab -l)
    */10 * * * * /usr/bin/php7.x /home/(...snip...)/wp-cron.php
  • WordPress: 現在のプラグインディレクトリ名を取得する

    WordPress のカスタマイズファイルはすべて KHCustom という自作プラグインに収めています。ディレクトリ名は kh-custom。

    /wp-contents/plugins/kh-custom/src/class/test.php
    から 'kh-custom' を取得しようとしたのですが、意外に簡単ではありませんでした。

    plugins_url() などそれらしい標準関数はあるのですが、プラグインディレクトリだけを取り出すことができません。出力させてみると次のようになりました。

    print_r( [
    	plugins_url(),
    	plugins_url( 'test.css', __FILE__ ),
    	plugin_dir_url( __FILE__ ),
    	plugin_dir_path( __FILE__ ),
    	plugin_basename( __FILE__ ),
    ] ,true ) );
    
    Array (
        [0] => https://listfreak.com/wp-content/plugins
        [1] => https://listfreak.com/wp-content/plugins/kh-custom/src/class/test.css
        [2] => https://listfreak.com/wp-content/plugins/kh-custom/src/class/
        [3] => /home/(...snip...)/wp-content/plugins/kh-custom/src/class/
        [4] => kh-custom/src/class/LF_Custom_CSS.php
    )

    plugin_basename() の先頭にプラグインディレクトリが来ているので、'/' で分割して先頭を取得すればよいことになります。

    current( explode( '/', plugin_basename( __FILE__ ) ) )

    あるいはプラグインのメインファイルで次のように定義しておくか。

    define( 'KH_PLUGIN_SLUG', basename(__DIR__) );
  • 「このページを共有する」dialogタグ版

    iOS 15.4からSafariで<dialog>タグがサポートされたのを機に、「このページを共有する」機能をjQueryから<dialog>に移し替えた。

    動作イメージ

    ソーシャルメニューの 🔗 をクリックすると、下記のようなモーダルダイアログが表示される。[COPY]をクリックすると、ページ情報をクリップボードにコピーしてダイアログが閉じられる。[CANCEL]をクリックすると、ただダイアログが閉じられる。

    ダイアログの出力 (PHP)

    上記のダイアログは次のように出力した。

    function echo_sharethis_dialog() {
    
        // 現在表示しているページのURL
        $url = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
    
        // 現在表示しているページのタイトルと合わせてページ情報を生成
        $pageinfo = wp_get_document_title().PHP_EOL.$url.PHP_EOL;
    
        echo '
        <dialog id="lf-sharethis-dialog">
            <p><b>このページを共有する</b><br>([COPY]でクリップボードにコピー)</p>
            <textarea id="lf-sharethis-area" readonly>',$pageinfo,'</textarea>
            <button id="lf-sharethis-btn-copy">Copy</button>
            <button id="lf-sharethis-btn-cancel">Cancel</button>
        </dialog>';
    }

    イベント処理 (JavaScript)

    // ダイアログ
    const shareThisDialog = document.getElementById( 'lf-sharethis-dialog' );
    
    // ソーシャルメニューのアイコン(クリックしてダイアログを表示)
    document.getElementsByClassName( 'lf-sharethis-menu' )[1].addEventListener( 'click', {
        object: shareThisDialog,
        handleEvent: function( e ) {
            this.object.showModal();
        }
    } );
    
    // グローバルメニュー(クリックしてダイアログを表示)
    document.getElementsByClassName( 'lf-sharethis-menug' )[0].addEventListener( 'click', {
        object: shareThisDialog,
        handleEvent: function( e ) {
            this.object.showModal();
        }
    } );
    
    // Copy ボタン(共有ダイアログ内)
    document.getElementById( 'lf-sharethis-btn-copy' ).addEventListener( 'click', {
        object: shareThisDialog,
        handleEvent: function( e ) {
            navigator.clipboard.writeText(
                document.getElementById( 'lf-sharethis-area' ).value
            );
            this.object.close();
        }
    } );
    
    // Cancel ボタン(共有ダイアログ内)
    document.getElementById( 'lf-sharethis-btn-cancel' ).addEventListener( 'click', {
        object: shareThisDialog,
        handleEvent: function( e ) {
            this.object.close();
        }
    } );

    L5-10: Twenty Twenty テーマのソーシャルメニューの一つをクリックするとダイアログを表示する。メニュー項目には id 属性を割り当てることができず、class 属性しか割り当てられない。

    L13-18: L5-10 と同様の処理をグローバルメニューについて行う。

    L21-29: COPYボタンがクリックされたら、テキストエリアの内容をクリップボードに書き出してダイアログを閉じる。

    L32-37: CANCELボタンがクリックされたらダイアログを閉じる。

    JavaScriptを登録する

    上記の JavaScript を登録する。ファイルの更新時刻をバージョン番号とすることで最新のファイルが読み込まれることを保証する。

    function enqueue() {
    
        $jsfile = '/'.KH_PLUGIN_SLUG.'/js/lf_sitewide_setting.js';
    
        wp_enqueue_script(
            'lf_sitewide_setting',
            plugins_url( $jsfile ),
            array(),                            // No Dependency
            filemtime( WP_PLUGIN_DIR.$jsfile ), // Version
            true                                // In Footer
        );
    }

    上記の処理をアクションフックのコールバックとして登録する

    上記の関数を一つのクラスにまとめた。

    
    class LF_SiteWide_Setting {
    
        public static function init() {
            add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ], 11 );
            add_action( 'wp_footer', [ __CLASS__, 'echo_sharethis_dialog' ], 19 );
        }
    
        public static function enqueue() {
        }
    
        public static function echo_sharethis_dialog() {
        }
    }

    ダイアログのHTMLを書き出す関数は、’wp_footer’ へのコールバックとして優先順位19で登録した。これは、JavaScriptを書き出す処理(wp_print_footer_scripts())が ‘wp_footer’ へのコールバックとして優先順位20で登録されているため、それよりも先に読み込ませるためである。

    そして、個人的なカスタマイズをまとめたプラグインのメインPHPファイルから以下の要領で呼び出している。

    LF_SiteWide_Setting::init();
  • 「このページをシェア」(jQuery版)

    Twenty Twenty テーマのソーシャルメニュー付けていた「このページをシェア」機能をjQuery版からdialogタグ版に移行した。以下はjQuery版の実装。

    ダイアログのイメージ

    PHP / HTML

    class LF_Share_This {
    
    	/**
    	 * コールバックを登録する
    	 */
    	public static function init() {
    
    		// コアが登録しているスクリプトよりも優先順位を下げるために11とした
    		add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue' ], 11 );
    
    		// シェアメニューの🔗がクリックされたら表示するダイアログの内容
    		add_action( 'wp_footer', [ __CLASS__, 'echo' ], 21 );
    	}
    
    	/**
    	 * jQuery UI Dialog と自作のjavascriptを登録する
    	 */
    	public static function enqueue() {
    
    		// jQuery UI Dialog の読み込み(bodyの下で)
    		wp_enqueue_script( 'jquery-ui-dialog', false, [ 'jquery' ], true );
    
    		// jQuery UI Dialog 用 CSSの読み込み(CSSは$srcの指定が必要)
    		wp_enqueue_style( 
    			'jquery-ui-dialog-min-css', 
    			includes_url().'css/jquery-ui-dialog.min.css' 
    		);
    
    		$version = filemtime( WP_PLUGIN_DIR.'/'.KH_PLUGIN_SLUG.'/js/lf_sharethis_button.js' );
    
    		// 共有リンクがクリックされたらダイアログを表示するスクリプトの読み込み
    		wp_enqueue_script(
    			'lf_sharethis_button',
    			plugins_url( 'js/lf_sharethis_button.js', dirname( __FILE__, 2 ) ),
    			[ 'jquery-ui-dialog' ],
    			$version,
    			true
    		);
    	}
    
    	/**
    	 * シェアメニューの🔗がクリックされたら表示するダイアログの内容
    	 */
    	public static function echo() {
    
    		// 現在表示しているページのURL
    		$url = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
    
    		// 現在表示しているページのタイトルと合わせてページ情報を生成
    		$pageinfo = wp_get_document_title().PHP_EOL.$url.PHP_EOL;
    
    		echo '
    <div id="lf-sharethis-box" title="ページの共有">
      <textarea id="lf-sharethis-text" readonly>'.$pageinfo.'</textarea>
      <p>[COPY]をクリック → (クリップボードにコピーされる → )共有先でペースト</p>
      <button id="lf-sharethis-copy" title="ページ情報をクリップボードにコピーして閉じる">Copy</button>
    </div>
    ';
    	}
    }

    JavaScript

    // https://api.jquery.com/jQuery/#jQuery3
    jQuery(function($) {
    
        // Open dialog on clicking the link menu
        $(".lf-sharethis-link").on('click', function() {
            $("#lf-sharethis-box").dialog();
        } );
    
        // copy info to share and close dialog
        $("#lf-sharethis-copy").on( 'click', function() {
            navigator.clipboard.writeText( $("#lf-sharethis-text").text() );
            $("#lf-sharethis-box").dialog("close");
        } );
    } );

    CSS

    /*
     * ソーシャルメニュー
     */
    div#lf-sharethis-box {
    	display: none;
    }
    /* ソーシャルメニュー → 共有ボタン→ Dialog → Pタグの文字の大きさ */
    #lf-sharethis-box p {
    	font-size: smaller;
    }
  • WordPress: フックを外す簡単な方法

    フィルターフックに登録したコールバック関数の中で自分自身の登録を解除したいときには、ハードコードせずに済む方法がある。

    function lf_my_filter( $title, $id ) {
    
        /* do some stuff */
    
        // equal to remove_filter( 'the_content', 'lf_my_filter' );
        remove_filter( current_filter(), __FUNCTION__ );
    
        return $title;
    }
    add_filter( 'the_content', 'lf_my_filter' );

    アクションフックも同様。

    function lf_my_action( $args ) {
    
        /* do some stuff */
    
        // equal to remove_action( 'wp', 'lf_my_action' );
        remove_action( current_action(), __FUNCTION__ );
    }
    add_action( 'wp', 'lf_my_action' );

    ただし、第3引数 ($priority) にデフォルト (1) 以外の優先度を設定している時は、remove する際にも同じ優先度を設定する必要がある。

  • タグ一覧ページにページ内検索&ハイライト機能を付ける

    タグ一覧ページでは、現在1700個近いタグが1ページに表示される。ページネーションをしないのはブラウザのページ内検索機能ですばやく検索したいから。

    だけどタグがずらずら並んでいるのもぶっきらぼうなので、フォームに入力した言葉を含むタグをハイライトしたうえで、画面上部にもコピーされるようにした。

    下は初期表示時。

    こちらが「決」を入力して「タグ検索」をクリックした結果。

    おおまかな仕組み

    タグクラウドは下記のように段落タグでくるまれている。入力フォームを画面上部に置いて、入力された言葉を含むタグをハイライトする。

    <p class="alignwide listfreak_tagcloud" id="lf-alltags">
        <a href="https://listfreak.com/tag/EQ" class="tag-cloud-link tag-link-75 tag-link-position-1" style="font-size: 1.6em;" aria-label="EQ (89個の項目)">EQ</a>
    
        <a ... ></a>
        ......
    </p>

    HTML(入力フォーム)

    検索語句を入力する部分はブロックエディタで作成。フォームといっても表示されているHTMLのDOMをいじるだけなので<form>タグは使わない。

    JavaScript

    lf_highlight_words.js というファイルを作った。このファイルを読み込ませる手順は後述。

    // タグ検索ボタンのクリックと処理関数を結びつける
    document.getElementById('highlight-btn')
        .addEventListener('click', lfHighlight);
    
    function lfHighlight() {
        1. 前処理
        2. 本処理
    }

    前処理

    ページを読み込み直さないので、前回のハイライト結果や見つかった語句数の表示を消す必要がある。

    フォームが空であれば処理終了。フォームに語句が入っていたら、本処理へ。

    // タグクラウドが格納された <p> を取得
    const alltags = document.getElementById( 'lf-alltags' );
    
    // その中で highlight クラスを持つオブジェクトの集合を取得
    const highlighted = alltags.getElementsByClassName( 'highlight' );
    
    // 前回の表示結果が残っていたらhighlight クラスを削除
    while ( highlighted.length ) {
        highlighted[0].classList.remove( 'highlight' );
    }
    
    // 見つかった検索語句の個数を表示する div
    const found = document.getElementById( 'lf-found' );
    
    // 前回の表示結果が残っている場合があるのでクリア
    while ( found.firstChild ){
        found.removeChild( found.firstChild );
    }
    
    // 検索語句を取得(valueはHTMLInputElementのプロパティ)
    const searched = document.getElementById( 'search-tag' ).value;
    
    // 検索語句がなければ終了
    if ( '' == searched ) {
        return;
    }
    
    // 検索語句を正規表現オブジェクトに
    const searchedR = new RegExp( searched, 'i' );
    
    // ヒットしたタグを格納する
    const foundTags = document.createElement( 'p' );
    
    // 全タグを1つずつ
    alltags.childNodes.forEach( function( tag ) {
    
        // 検索語句が含まれていたら
        if ( searchedR.test( tag.innerText ) ) {
    
            // 検索窓の下に表示するためにコピー
            foundTags.appendChild( tag.cloneNode() );
    
            // ハイライト
            tag.classList.add( 'highlight' );
    
        }
    } );
    

    L5-L18: タグ検索ボタンが2回め以降に押された場合、前回のハイライトやヒットタグ一覧をクリアする必要がある。自分の子孫をすべて削除するメソッドが見つからなかったので一つずつ削除。highlighted は HTMLCollection インターフェイス、found は Element オブジェクトを返すので削除のやり方が違う。いずれにせよ逆ダルマ落し的に最初の要素から一つずつ削除した。

    本処理

    タグはノードの配列として取得できているので、innerText に検索語句を含むノードを抽出してハイライト用のクラスをまとめて追加する……ようなやりかたがあればと思ったが、見つからなかったので forEach で一つずつ。

    // 検索語句を正規表現オブジェクトに
    const searchedR = new RegExp( searched, 'i' );
    
    // ヒットしたタグを格納する
    const foundTags = document.createElement( 'p' );
    
    // 全タグを1つずつ
    alltags.childNodes.forEach( function( tag ) {
    
        // 検索語句が含まれていたら
        if ( searchedR.test( tag.innerText ) ) {
    
            // 検索窓の下に表示するためにコピー
            foundTags.appendChild( tag.cloneNode( true ) );
    
            // ハイライト
            tag.classList.add( 'highlight' );
    
        }
    } );
    
    // 見つかった個数を表示
    const foundMsg = document.createElement( 'p' );
    foundMsg.textContent = foundTags.childNodes.length + ' FOUND.';
    found.appendChild( foundMsg );
    
    // 検索語句を含むタグへのリンクを検索窓の下に表示
    if( 0 < foundTags.childNodes.length ) {
        found.appendChild( foundTags );
    }
    

    CSS

    ハイライト用のクラスには下記のように背景色を変える設定をした。

    .highlight {
        background-color: yellow;
    }

    PHP

    作成した JavaScript ファイルを登録する。

    add_action( 'wp_enqueue_scripts', 'lf_enqueue_highlight_in_page', 11 );
    function lf_enqueue_highlight_in_page() {
    
        // slug が tags でなければリターン
        if ( 'tags' != get_post_field( 'post_name',  get_the_ID() ) ) {
            return;
        }
    
        wp_enqueue_script(
            'lf_highlight_words',
            home_url().'/ <snip> /lf_highlight_words.js',
            array(), // No Dependency
            '1.0.0', // Version
            true     // In Footer
        );
    }
    
  • カテゴリ、タグ、カスタムタクソノミーで絞り込む

    たとえば「選択」でタグづけされた公開「リスト」投稿で引用されている「本」を一覧したいとします。これは投稿に関連付けられた3つのタクソノミーを使うことになります。

    1. 公開された投稿のなかで、タクソノミー ‘category’ のタームが ‘list’ である投稿を抽出する
    2. そのなかでタクソノミー ‘post_tag’ のタームが ‘選択’ である投稿を抽出する
    3. それらの投稿に関連づけられているタクソノミー ‘asin’ のタームの一覧を取得する
    SELECT DISTINCT t.name
    FROM wp_posts AS p
    INNER JOIN wp_term_relationships AS tr ON tr.object_id = p.ID
    INNER JOIN wp_term_taxonomy AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'asin'
    INNER JOIN wp_terms AS t ON t.term_id = tt.term_id
    WHERE p.ID IN (
      SELECT p2.ID
      FROM wp_posts AS p2
      INNER JOIN wp_term_relationships AS tr2 ON tr2.object_id = p2.ID
      INNER JOIN wp_term_taxonomy AS tt2 ON tt2.term_taxonomy_id = tr2.term_taxonomy_id AND tt2.taxonomy = 'post_tag'
      INNER JOIN wp_terms AS t2 ON t2.term_id = tt2.term_id AND t2.name = '選択'
      WHERE p2.ID IN (
        SELECT p3.ID
        FROM wp_posts AS p3
        INNER JOIN wp_term_relationships AS tr3 ON tr3.object_id = p3.ID
        INNER JOIN wp_term_taxonomy AS tt3 ON tt3.term_taxonomy_id = tr3.term_taxonomy_id AND tt3.taxonomy = 'category'
        INNER JOIN wp_terms AS t3 ON t3.term_id = tt3.term_id AND t3.name = 'list'
        WHERE p3.post_type = 'post' AND p3.post_status = 'publish'
      )
    )
    • L13-18: 公開された(post_status が publish)投稿(post_type が post)のなかで、タクソノミー ‘category’ のタームが ‘list’ である投稿を抽出する
    • L7-12: L13-18 によって抽出された投稿の中で、カテゴリが list (category タクソノミーのタームが list)のものを抽出する
    • L1-L6: それらの投稿に関連づけられた本(asinタクソノミーのターム)を取得する。
    • L1: 1冊の本から複数のリストが作成され、それらが同じタグを有している場合もあるので DISTINCT を付して重複を除く

    p、p2などと別名を付けなくてもSQL文としては通りましたが、EXPLAIN 文での分析結果がわかりづらくなるので別名を付けました。

    上記を検証するための PHPコードは下記。

    $q = new WP_Query( array(
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => -1, // 全件
        'fields'         => 'ids', // IDのみ
        'tax_query'      => array(
            'relation' => 'AND',
            array(
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => 'list',
            ),
            array(
                'taxonomy' => 'post_tag',
                'field'    => 'name',
                'terms'    => '選択',
            ),
        ),
    ) );
    
    $asins = wp_get_object_terms( $q->posts, 'asin', $args );
  • WordPress: 複数の投稿に対してブロックを一括編集

    概要

    複数の投稿の内容を一括で置換する際、ブロックを使ってみた。

    背景

    千本を超える投稿の数百に対して編集をかける際、これまでは正規表現でコンテンツ (post_contents) を検索・置換 (preg_match_all) していましたが、ブロック単位で実施するやり方を試してみました。

    内容

    例として、複数の投稿に存在している次に示すような段落ブロック

    (参考文献)
    (1) ~

    を、次に示す見出しブロックと段落ブロックに変えました。(参考文献)だけでなく〈参考文献〉としている箇所もあるのでまとめて。

    参考文献

    (1) ~

    これはHTMLレベルでいえば、次の段落ブロック

    <!-- wp:paragraph {"className":"lf_postscript"} -->
    <p class="lf_postscript">(参考文献)<br>(1) ~</p>
    <!-- /wp:paragraph -->

    を次のように変えることです。

    <!-- wp:heading {"level":4,"className":"lf_refernces"} -->
    <h4 class="lf_refernces">参考文献</h4>
    <!-- /wp:heading -->
    
    <!-- wp:paragraph {"className":"lf_postscript"} -->
    <p class="lf_postscript">(1) ~</p>
    <!-- /wp:paragraph -->

    だいたい次のようなコードでいけました(クエリのパラメーターやエラー処理は省略して引用)。

    $q = new WP_Query( array(
      'post_type'      => 'post',
      'post_status'    => 'publish',
      's'              => '参考文献',
      'posts_per_page' => -1,
    ) );
    
    $block_h4 = array(
      array(
        'blockName'    => 'core/heading',
        'attrs' => array(
          'level'     => 4,
          'className' => 'lf_refernces',
        ),
        'innerBlocks'  => array(),
        'innerHTML'    => '',
        'innerContent' => array(
          PHP_EOL.'<h4 class="lf_refernces">参考文献</h4>'.PHP_EOL
        ),
      ),
      array(
        'blockName'    => null,
        'attrs'        => array(),
        'innerBlocks'  => array(),
        'innerHTML'    => '',
        'innerContent' => array( PHP_EOL.PHP_EOL ),
      ),
    );
    
    $pattern = '#((|〈)参考文献()|〉)\<br\>(.+)#';
    
    foreach ( $q->posts as $p ) {
      $blocks = parse_blocks( $p->post_content );
    
      for ( $i = 0; $i < count( $blocks ); $i++ ) {
    
        if ( 'core/paragraph' == $blocks[$i]['blockName'] ) {
          if ( 1 == preg_match( $pattern, $blocks[$i]['innerContent'][0], $matches ) ) {
            $blocks[$i]['attrs']['className'] = 'lf_postscript'; 
            $blocks[$i]['innerContent'][0] = PHP_EOL.'<p class="lf_postscript">'.$matches[3].PHP_EOL;
            array_splice( $blocks, $i, 0, $block_h4 );
            $content = serialize_blocks( $blocks );
            $mypost  = array(
              'ID'           => $p->ID,
              'post_content' => $content,
            );
            wp_update_post( $mypost );
            break;
          }
        }
      }
    }

    L1-6: 編集対象となる投稿をざっくり抜き出します。

    L8-28: 挿入するブロック。ブロックを post_content に格納するために HTML に変換する serialize_block() を見ると、blockName, attrs, innerContent しか使っていないので他の要素は要らないのですが、一応。

    L12: level は文字 ‘4’ でなく 数字 4 でなければなりません(ブロックエディターで読み込むとエラーが出ます)。

    L18: ブロック要素の前後に改行を入れておきます(標準でそのようになっているため)。

    L21-27: なぜか標準ではブロック間に空ブロックが挟まっているので踏襲しました。

    L22: blockName は ‘’ でなく null でなければなりません。空ブロックかどうかを、serialize_block() で is_null() で判定しているため。

    L30: 置換対象となる文字列。(参考文献)と〈参考文献〉を同時に置換するため、結局正規表現を使いました。

    L32-52: 候補の投稿を順次処理していきます。

    L33: 投稿をブロック化します。

    L35-51: ブロックを順次処理していきます。ブロック自体を書き換えるので foreach() でなく for() を選択。

    L37: 段落ブロックなら……

    L38: 段落が、L30 で指定したパターンだったら……

    L39-40: その段落を書き換えます。

    L41: その段落ブロックの直前に、L8-28 で用意した見出しブロックを挿入します。

    L42: ブロックを HTML 化します。

    L43-47: その HTML で投稿を更新します。

    L48: 次の候補投稿へ。

    従来やっていた、投稿コンテンツ全体を正規表現検索・置換するよりはミスが少なそうな気がします。手間はあまり変わらないかな。

  • 新規投稿時にデフォルトのカテゴリやブロックパターンを設定する

    概要

    リストを投稿するべく投稿画面を開いたときに、あらかじめカテゴリが選ばれ、テンプレートが設定され、ブロックパターンが流し込まれた状態にする。

    背景

    当サイトではリスト用の投稿タイプを用意していないため、リストを投稿しようと思って投稿画面を開いたとき、いつもカテゴリを list にセットし、リスト投稿用のブロックパターンを選択しています。これが面倒なので自動化を試みました。

    内容

    add_filter( 'default_content', 'lf_default_content', 10, 2 );
    function lf_default_content( $post_content, $post ) {
    	$is_newlist = $_GET['newlist'] ?? '';
    	if ( $is_newlist ) {
    		// カテゴリを list に設定する
    		$list_catid = get_category_by_slug( 'list' )->term_id;
    		wp_set_post_categories( $post->ID, $list_catid, true );
    
    		// リスト用テンプレートを設定する
    		update_post_meta( $post->ID, '_wp_page_template', 'templates/template-cover.php' );
    
    		// リスト投稿のブロックパターンを読み込む
    		$bpr = WP_Block_Patterns_Registry::get_instance();
    		$bp  = $bpr->get_registered( 'kh/new-list' );
    		$post_content = $bp['content'];
    	}
    	return $post_content;
    }

    L1: ‘default_content’ は編集画面が開く直前に実行されるフックで、本文に予めコンテンツを流し込んでおくことができます。ちなみにタイトルと抜粋についても類似のフィルターフックが用意されています。

    L3: 当サイトではリスト用の投稿タイプを作ったりしていないので、「リストを投稿するべく」編集画面を開いたかどうかを別の方法で検知する必要があります。そこで新規投稿を作成するURLに &newlist=yes のようなパラメータを付与しました。そのリンクからのアクセスであれば、リスト投稿の意思ありとみなします。

    L7: 投稿オブジェクトが作られた直後なので第3引数 ($append) はデフォルトの false でよいのですが、将来この処理の手前に何かの処理を挟む可能性もあるので、念のため true に。

  • 特定のブロックをキャッシュする

    概要

    フロントページに置いている、データベースクエリをたくさん発行するブロックをキャッシュする。

    背景

    フロントページに、新しい公開リストのタイトルと抜粋を6つ表示しています。これは WordPress 標準の「最新の投稿」ブロックを使っています。

    新しい公開リストを投稿したときにのみ更新されればよい情報なので、キャッシュしてみようと思います。

    内容

    キャッシュには Transient API を使用

    WordPress が標準で提供している永続的なキャッシュシステムである Transient API を使います。

    キャッシュからの取得

    add_filter( 'pre_render_block', 'lf_pre_render_block', 10, 2 );
    function lf_pre_render_block( $pre_render, $parsed_block ) {
    
        $post    = get_post();
        $post_id = $post->ID ?? 0;
    
        if ( $post_id == get_option( 'page_on_front')
            && 'core/latest-posts' == $parsed_block['blockName'] ) {
    
            $slug = &$parsed_block['attrs']['categories'][0]['slug'] ?? '';
            $transient = '';
    
            // 今日のリスト
            if ( 'lotd' == $slug ) {
                if ( $transient = get_transient( 'LF_BLOCK_LATESTPOSTS_LOTD' ) ) {
                    $pre_render = $transient;
                }
    
            // 新着リスト
            } elseif ( 'list' == $slug ) {
                if ( $transient = get_transient( 'LF_BLOCK_LATESTPOSTS_LIST' ) ) {
                    $pre_render = $transient;
                }
            }
    
            // キャッシュから取得できなかったら
            if ( ! $transient ) {
    
                // ブロックをキャッシュする
                add_filter( 'render_block_core/latest-posts', 'lf_render_block_latest_posts', 10, 2 );
            }
        }
    
        return $pre_render;
    }
    
    • L1: ‘pre_render_block’ フックは、フックした関数が返す文字列をそのまま出力します。
    • L4-5: 表示しようとしている投稿を知るための処理。L5 は 404 ページなど $post が作成されないケースへの対応。
    • L7: フロントページかつ「最新の投稿」ブロックの場合のみキャッシュからの取得を試みます。
    • L10: 読みやすさのためと、もしキーが存在していなくてもエラーにならないようにするための処理。
    • L30: キャッシュから取得できなければ、ブロック書き出し後に発火するフィルターフック “render_block_{$this->name}” にコールバック関数を追加します。’render_block_core/latest-posts’ とすれば「最新の投稿」ブロックををレンダリングした直後にのみ起動します。

    キャッシュへの保存

    ブロックに表示するデータが作られた直後に実行されるフィルターフックに、次のような処理を挟みました。

    function lf_render_block_latest_posts( $block_content, $parsed_block ) {
    
        // For readability and safety
        $slug = &$parsed_block['attrs']['categories'][0]['slug'] ?? '';
    
        // 今日のリスト
        if ( 'lotd' == $slug ) {
            set_transient( 'LF_BLOCK_LATESTPOSTS_LOTD', $block_content, WEEK_IN_SECONDS );
    
        // 新着リスト
        } elseif ( 'list' == $slug ) {
            set_transient( 'LF_BLOCK_LATESTPOSTS_LIST', $block_content, WEEK_IN_SECONDS );
        }
    
        return $block_content;
    }
    • L7,11: フロントページでは「今日のリスト」カテゴリの投稿についても「最新の投稿」ブロックを使っているので、両者を識別しないといけません。そこでパースされたブロック情報を調べて、カテゴリスラッグで識別することにしました。
    • L8,12: 第三引数を外せば transient の寿命が無限になるうえにautoload(メモリ上に保存)されます。しかし autoload するような内容でもないので、公開リストの投稿間隔の見積もり以上に長い期間を見込んで寿命を設定しておきました。

    キャッシュの更新(削除)

    「今日のリスト」を入れ替える、リスト投稿が公開されるなど、ブロックの内容を書き換えるべきイベントによって発火するフィルターフックに、Transient の削除処理を登録します。以下は新着リストブロックを書き換える例。

    add_action( 'transition_post_status', 'lf_post_status', 20, 3 );
    
    function lf_post_status( $new_status, $old_status, $post ) {

    // (投稿タイプ、カテゴリ、投稿ステータスなどで絞り込む)
    delete_transient( 'LF_BLOCK_LATESTPOSTS_LIST' );
    }

    この方法を用いれば、どんなにデータベースクエリの多いブロックでも、2回のクエリ( transient とその寿命情報)で済むようになります。

    複数ブロック、あるいは the_content() の出力をまとめてキャッシュできないか?

    簡単な方法が見つかりませんでした。the_content() は次のようにしてブロックを表示しています。

    • テンプレートファイル
      • the_content(): テンプレートファイル中で呼び出している本文表示関数
        • ‘the_content’: 上記関数内のフィルターフック
          • do_blocks(): /wp-includes/default-filters.php で上記にフックされている関数
            • render_block(): 上記関数内で呼ばれている関数

    do_blocks() 内の冒頭&最後にフィルターフックがあればブロックエディタの出力をまるごとキャッシュできそうですが、存在せず。

    the_contents() も同様です。この関数はすべての処理を ‘the_content’ フックに登録したフィルターを使って実施しています。デフォルトのフィルターは default-filters.php にあります。

    すべてのフィルターの前に、つまり最高の優先順位でキャッシュ取得フィルターをフックしておき、もしキャッシュが取得できたら以降のフックを解除してしまう、といった操作を行えばできそうですが、そこまでするのは面倒なので断念。