ブログ用タグ: WordPress

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

    概要

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

    背景

    当サイトではリスト用の投稿タイプを用意していないため、リストを投稿しようと思って投稿画面を開いたとき、いつもカテゴリを 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 にあります。

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

  • 投稿のタイトルをメール件名にすると起きる文字化けを防ぐ

    概要

    メールの件名に get_the_title() 関数の返り値をそのまま用いると、この関数内で行われている、文章を読みやすくするための処理が文字化けを起こすことがあります。これを回避する方法を3つ考えました。

    背景

    LOTD(日替わりリスト)をメールで配信する際には、 リスト投稿のタイトルをそのままメールの件名にしてメーリングリストに送信しています。メーリングリストはその件名の頭に [今日のリスト] という文字列を付けて配信します。

    ところが ‘-‘(半角マイナス)を含むリストが ‘–’ に変換されてしまっていました。

    内容

    get_the_title() 中のフィルターフック ‘the_title‘ にはいくつかデフォルトのフィルターがセットされています。その一つである wptexturize() が問題を起こしているとわかりました。

    wptexturize() は文章を読みやすくするための変換を施します。変換の結果は Web ページ(HTML文書)として表示されることを想定しており、いくつかの文字は HTML エンティティに変換されています。その代表的なものが ‘-‘ の ‘–’ への変換でした。

    ‘–’(または ‘–’ )はいわゆるダッシュ記号で、’-‘(半角マイナス)より少し長めにデザインされているフォントが多いようです。

    この文字化けを防ぐには、やり方が大きく3つあると考えました。

    一つめは get_the_title() を使わず 投稿オブジェクトの post_title メンバをそのまま使うこと。

    二つめは wptexturize() を無効化すること。無効化のニーズが高いせいか、そのためのフィルターフックも用意されています。次のようにすれば wptexturize() を通したくないときだけ無効化することが可能。

    add_filter( 'run_wptexturize', '__return_false' );
    $lotd_title = get_the_title( $lotd_post );
    remove_filter( 'run_wptexturize', '__return_false' );
    

    __return_false() は標準で用意されている関数で、文字通り return false; だけをしてくれます。

    三つめは、 wptexturize() が HTML エンティティに変換した結果を戻す(HTML デコードする)こと。先の例でいえば、せっかく半角マイナスをダッシュに変換してくれたので、それを HTML デコードすれば読みやすくなった状態で渡せます。そのためにはこんなふうにすればよいはず。

    $lotd_title = html_entity_decode( $lotd_title, ENT_COMPAT | ENT_HTML5, 'UTF-8' );
    図:メール一覧画面。上がダッシュ、下が半角マイナス

    実際にメーラー (GMail) での見え方を比較してみました。

    図の上の行は3つめ、つまり wptexturize() の変換結果を生かしたやり方で送ったメールで、ダッシュが表示されています。

    下は2つめ(と原理的には1つめ)、つまり変換なし。半角マイナスが表示されています。

    こうしてみると、ダッシュのほうが読みやすい。wptexturize() がおせっかい機能にしか思えなかったので2つめのやりかたでいこうと思っていたのですが、実験結果を見て3つめでいくことにしました。

    ※追記: 通常のメール送信はこれでOKでしたが、LOTD を配信させているメーリングリストが件名中のダッシュ(enダッシュ)を扱えず、「?」に変換してしまう問題があることがわかりました。エックスサーバーのメーリングリストは fml を使っているようですが、件名にセットする文字コードの指定などはなく、解決できていません。そこで現在は1つめの解決策を適用中。

  • フロントページの設定

    フロントページのカスタマイズ

    タイトルと抜粋部分にLOTDを掲載する

    フロントページには固定ページを使い、タイトルと抜粋にLOTD(日替わりリスト)のタイトルとまえがきを表示させています。

    固定ページの抜粋を表示させる

    標準では固定ページの抜粋フィールド (post_excerpt) は使われないので、使うよう設定します。

    add_action( 'init', 'kh_use_excerpt_on_pages' );
    function kh_use_excerpt_on_pages() {
        add_post_type_support( 'page', 'excerpt' );
    }

    タイトルと抜粋を設定する

    記事タイトルの変更は、フィルターフック ‘the_title’ を使うわけですが、このフックはサイト全体を通じて呼ばれまくっています。10記事のタイトル一覧があると10回呼ばれます。できるだけ負荷を減らすために、フロントページを表示させるときのみ登録するようにしてみました。

    ナビゲーションメニューも WP_Queryを使っているので、判定条件には is_main_query() も必要。これがないとナビゲーションメニューが消失してしまいました。

    add_action( 'wp_head', 'kh_wp_head_frontpage' );
    function kh_wp_head_frontpage() {
    
        if ( is_front_page() && is_main_query() ) {
            add_filter( 'the_title', 'kh_frontpage_title', 10, 2 );
        }
    }

    コールバック関数はこちら。フィルターフック ‘the_title’ はフロントページの表示中にも呼ばれまくっており、 is_front_page() では変えるべきタイトルが特定できませんでした。固定ページのIDが get_option( ‘page_on_front’) で取得できるフロントページのIDと同じであるという条件で判定をしています。

    そしてフロントページのタイトルを書き換える場合のみ、抜粋ページを書き換えるフィルターフックを登録します。

    function kh_frontpage_title( $title, $id ) {
        if( $id == get_option( 'page_on_front') ) {
            $lotd = lf_official_lotd();
            $title = '<a href="'.get_permalink( $lotd ).'">'.get_the_title( $lotd ).'</a>';
            add_filter( 'the_excerpt', 'kh_frontpage_excerpt', 10, 1 );
        }
        return $title;
    }
    
    function kh_frontpage_excerpt( $post_excerpt ) {
        $lotd = lf_official_lotd();
        return get_the_excerpt( $lotd );
    }

    Latest Posts ブロックをカスタマイズする

    フロントページの本文先頭にも LOTD を書影と共に表示させます。

    具体的には “Latest Posts” ブロックをカスタマイズしました。このブロックはカテゴリごとに新着投稿を表示できるので、LOTD専用カテゴリを選択すれば絞り込みは完了。

    書影については工夫が必要です。仕様では投稿のアイキャッチ画像を使うのですが、書影はほとんどアフィリエイトサイトからの引用で、アイキャッチ画像に登録しておくことができません。

    そこで、フロントページにLOTDを表示する際には、アイキャッチ画像が登録されているかのように偽装するカスタマイズを施しました。

    アイキャッチ画像の有無は、関数 has_post_thumbnail() によって判定されます。 “Latest Posts” ブロックでは gutenberg_render_block_core_latest_posts() という関数の中で呼ばれています。

    この関数内にあるフィルターフック ‘has_post_thumbnail’ にコールバック関数を登録しました。この関数の中で、さらにアイキャッチ画像のhtmlをレンダリングした後に呼ばれるフィルターフック ‘post_thumbnail_html’ にコールバック関数を登録し、trueを返しておきます。ここが偽装と呼んだ部分。

    add_filter( 'has_post_thumbnail', 'lf_has_post_thumbnail', 10, 3 );
    function lf_has_post_thumbnail( $has_thumbnail, $post, $thumbnail_id ) {
        if ( is_front_page() && has_category( 'lotd', $post ) && ! $has_thumbnail ) {
            add_filter( 'post_thumbnail_html', 'lf_post_thumbnail_html', 10, 5 );
            $has_thumbnail = true;
        }
        return $has_thumbnail;
    }

    html記述部分は次の通り。ひとつの投稿には複数の書籍が付いている場合がありますが、配列の先頭の書籍の書影を使います。 lf_get_asin_meta() は、書籍IDから書影URLなどの関連情報を取得する関数。

    メディアライブラリに登録すると、自動的に決まった大きさのサムネイルが作成・保存されます。しかし書影ファイルはサイズが不定。そこで表示領域をサムネイルのサイズで固定したうえで、CSS の object-fit プロパティを使って書影の中央部分を切り抜いて表示させています。

    function lf_post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
        $asins = get_the_terms( $post_id, 'asin' );
        $res = lf_get_asin_meta( $asins[0]->name );
    
        $thum_w = get_option( 'thumbnail_size_w' );
        $thum_h = get_option( 'thumbnail_size_h' );
        return '<img width="'.$thum_w.'" height="'.$thum_h.'" src="'.$res->img.'" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" style="width:'.$thum_w.'px;height:'.$thum_h.'px;object-fit:cover;" />';
    }
  • あるカテゴリ内の公開投稿を、付けられたタグの数で並べ替える

    共起タグが表示できるようになったので、タグ数の少ないリストを見直して、必要に応じてタグを増やそうと思います。

    そこで、list カテゴリに属する公開投稿を、投稿に付けられたタグの数が少ない順に並べて取り出そうとしたのですが、用意された関数では(やたらとループさせる以外に)うまい方法を見つけられず、SQL文を書いてみました。

    カテゴリもタグもタクソノミーなので、category タクソノミーの list タームに関連付けられた投稿のそれぞれについて、関連付けられている post_tag タクソノミーのターム数をカウントします。日本語で書いてもSQLで書いてもややこしい。

    SELECT p.ID, COUNT(tt2.term_taxonomy_id) AS count
    FROM wp_posts AS p
    JOIN wp_term_relationships AS r ON p.ID=r.object_id
    JOIN wp_term_taxonomy AS tt ON r.term_taxonomy_id=tt.term_taxonomy_id AND tt.taxonomy='category'
    JOIN wp_terms AS t ON tt.term_id=t.term_id AND t.name='list'
    JOIN wp_term_relationships AS r2 ON p.ID=r2.object_id
    JOIN wp_term_taxonomy AS tt2 ON r2.term_taxonomy_id=tt2.term_taxonomy_id AND tt2.taxonomy='post_tag'
    WHERE p.post_status='publish' AND p.post_type='post'
    GROUP BY p.ID
    ORDER BY count ASC

    5 までで list カテゴリーに関連付けられた投稿を取り出しています。6と7で、その投稿に関連付けられたタグを取り出し、投稿IDごとにカウントします。

  • 一緒に付けられることが多いタグ(共起タグ)を可視化する

    リスト投稿には1~5つのタグを付けるようにしています。同じタグが付いたリストは当然ながら性質が似ていて、類義語のようなタグが付くことが多くなります。

    たとえば「選択」というタグが付けられたリスト投稿には「決断」というタグも付けられがち。このような関係にあるタグを「共起タグ」と命名しておきます。

    共起タグを可視化したく、タグページの説明(description)欄に「このタグと共に使われていることが多いタグ」を付けてみました。

    フィルターフックの箇所

    TwentyTwenty テーマでは、タグページの説明は index.php から get_the_archive_description() を呼び出して表示しています。ですので、この関数内に用意されている ‘get_the_archive_description’ フィルターフックが使えます。

    add_filter( 'get_the_archive_description', 'lf_tags_description', 10, 1 );
    function lf_tags_description( $description ) {
    
        if( ! is_tag() ) {
            return $description;
        }
    
        /* 処理 */
    }

    上記の「処理」部分ではSQL文を使う場合と関数を使う場合の2通りのやり方を考えました。

    SQL文を使う場合

    「公開された」「投稿タイプがpostで」「listカテゴリーに属し」「表示しようとしているタグが付けられた」投稿群に付けられている他のタグを洗い出し、登場回数の多い順に5つを取り出すSQL文を書きました。

    たとえば「選択」タグがつけられた公開リストは19あり、それぞれのリストには「決断」や「意思決定」などのタグも付けられています。それらの共起タグを重複ありで洗い出し、登場回数の多い順に5つ取り出すということ。

    SELECT COUNT(t3.term_id) AS count, t3.term_id, t3.name
    FROM wp_terms AS t
    INNER JOIN wp_term_taxonomy AS tt ON t.term_id=tt.term_id AND taxonomy='post_tag'
    INNER JOIN wp_term_relationships AS r ON tt. term_taxonomy_id=r.term_taxonomy_id
    INNER JOIN wp_posts AS p ON r.object_id=p.ID AND p.post_status='publish' AND p.post_type='post'
    INNER JOIN wp_term_relationships AS r2 ON p.ID=r2.object_id
    INNER JOIN wp_term_taxonomy AS tt2 ON r2.term_taxonomy_id=tt2.term_taxonomy_id AND tt2.taxonomy='category'
    INNER JOIN wp_terms AS t2 ON tt2.term_id=t2.term_id AND t2.name='list'
    INNER JOIN wp_term_relationships AS r3 ON p.ID=r3.object_id
    INNER JOIN wp_term_taxonomy AS tt3 ON r3.term_taxonomy_id=tt3.term_taxonomy_id AND tt3.taxonomy='post_tag'
    INNER JOIN wp_terms AS t3 ON tt3.term_id=t3.term_id AND t3.term_id != (表示タグのID)
    WHERE t.term_id=(表示タグのID)
    GROUP BY t3.term_id
    ORDER BY count DESC
    LIMIT 5

    このSQL文を発行し、得られた結果を下記のようにHTMLに組み立てて返します。

        $coused_tags = $wpdb->get_results( $sql );
        foreach( $coused_tags as $c ) {
            $desc[] = '<a href="'.get_tag_link( $c->term_id ).'" title="'.$c->count.'">'.$c->name.'</a>';
        }
    
        return 'このタグと共に使われていることが多いタグ<br>'.implode( ' ', $desc );
    

    関数を使う場合

    関数を使う場合はいくつかのステップが必要です。

    1. 表示しようとしているタグが付けられている投稿群を取得する

    $wp_query がタグの付いた投稿群を最初のページ分だけは保持しているので、ページングが無ければ $wp_query から投稿IDの一覧を取得する。ページングがあれば新規にクエリを発行して投稿IDの一覧を取得する。

        $tag_id = get_query_var( 'tag_id' );
    
        global $wp_query;
    
        if( $wp_query->max_num_pages == 1 ) {
            foreach( $wp_query->posts as $p ) {
                $post_ids[] = $p->ID;
            }
        } else {
            $q = new WP_Query( array(
                'post_type'      => 'post',
                'post_status'    => 'public',
                'category_name'  => 'list',
                'tag_id'         => $tag_id,
                'posts_per_page' => -1, // 全行
                'fields'         => 'ids', // IDのみ
            ) );
            $post_ids = $q->posts;
        }
    

    2. それらの投稿群に付いている(当該タグ以外の)タグを「重複ありで」取得する

    共起している回数を数えるため、上記の投稿群に付いているタグを重複ありで取得する必要があります。ここがわからなくてSQL文を書いたのですが、 wp_get_object_terms()関数がそのようなオプションを提供していました。

    もし共起タグが見つからなければここで処理終了。

        $coused_tags_all = wp_get_object_terms( $post_ids, 'post_tag', array(
            'fields' => 'all_with_object_id',
            'exclude' => $tag_id,
        ) );
    
        if( ! count( $coused_tags_all ) ) {
            return $description;
        }

    3. 共起タグの出現回数トップ5を取り出す

    $coused_tags_all は Term オブジェクトの配列で、画面表示に必要なタグ名称を持っています。余計な呼び出しを減らすため、取得済みの Term オブジェクトをできるだけ活用。

    $coused_tags はタグIDをキーとする配列で、登場回数とTermオブジェクトを値に持たせています。

    // タグの出現回数を数える
    $coused_tags = array();
    foreach( $coused_tags_all as $c ) {
        if( ! isset( $coused_tags[$c->term_id] ) ) {
            $coused_tags[$c->term_id] = array( 'count' => 1, 'obj' => $c );
        } else {
            $coused_tags[$c->term_id]['count']++;
        }
    }
    
    // 出現回数の降順でソート
    uasort ( $coused_tags , function ($a, $b) {
        return $b['count'] - $a['count'];
    } );
    
    // 最大5つを取り出す
    $coused_tags = array_slice( $coused_tags, 0, 5, true );

    4. 表示するHTMLを作成する

    foreach( $coused_tags as $k => $v ) {
        $desc[] = '<a href="'.get_tag_link( $k ).'" title="'.$v['count'].'">'.$v['obj']->name.'</a>';
    }
    
    return '「'.urldecode( get_query_var( 'tag' ) ).'」と共に使われていることが多いタグ<br>'.implode( ' ', $desc );
  • あるカテゴリーの公開投稿で使われている、あるタクソノミー下のタームの総数を数える

    公開リストの中で引用している書籍の総数を数えた。

    • 公開リストとは
      • post_type が post
      • post_status が publish
      • カテゴリー が list
    • 引用している書籍とは
      • asin タクソノミー 下にあるターム

    標準関数を組み合わせると次のような手順で取得できる。

    $q = new WP_Query( array(
    	'post_type'      => 'post',
    	'category_name'  => 'list',
    	'post_status'    => 'publish',
    	'posts_per_page' => -1,
    	'fields'         => 'ids',
    	'tax_query' => array( // 不要だが母集団を小さくするため
    		array( 'taxonomy' => 'asin',
    			'operator' => 'EXISTS' ),
        ),
    ) );
    
    // ids: 投稿に付けられたASINタームの総数(重複無し)
    $terms = wp_get_object_terms( $q->posts, 'asin', [ 'fields' => 'ids' ]  );
    $term_count = count( $terms );	
    
    // all_with_object_id: 投稿に付けられたASINタームの総数(重複あり、延べ引用数)
    $terms = wp_get_object_terms( $q->posts, 'asin', [ 'fields' => 'all_with_object_id' ]  );
    $term_count_total = count( $terms );

    ここで $term_count がASINターム数つまり本の数、$term_count_total が延べ引用数ということになる。

    ただ、2つの数字を得るために千の要素を超える配列を3つも経由するのはスマートでない気がするので、SQL文で取得することを試みた。

    asinタクソノミー下のタームが関連付けられている公開投稿を探し出し、その投稿がlistカテゴリ下にあることを確認しなければならない。

    データベース構造(WordPress Codex 日本語版)を見ながら愚直に INNER JOIN していった。このSQL文の結果と上の関数の結果が同じで、いくつか書籍を絞ってテストしてみても正しい結果が取得できたので、きちんと動いている模様。

    SELECT COUNT(DISTINCT t.term_id) AS total, COUNT(t.term_id) AS c_total
    FROM wp_terms AS t
    INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id AND tt.taxonomy = 'asin'
    INNER JOIN wp_term_relationships AS r ON tt.term_taxonomy_id = r.term_taxonomy_id
    INNER JOIN wp_posts AS p ON r.object_id = p.ID AND p.post_status = 'publish' AND p.post_type = 'post'
    INNER JOIN wp_term_relationships AS r2 ON p.ID = r2.object_id
    INNER JOIN wp_term_taxonomy AS tt2 ON r2.term_taxonomy_id = tt2.term_taxonomy_id AND tt2.taxonomy = 'category'
    INNER JOIN wp_terms AS t2 ON tt2.term_id = t2.term_id AND t2.name = 'list'

    備忘のため解説。

    1. ターム(本)の数を数える。一冊の本が複数の投稿に属していることがあるので、本の数 (total) と延べ引用数 (c_total すなわち cumulative total) を取得する。
    2. タームが保存されているのは wp_terms テーブル。
    3. asin タクソノミーに属するタームだけを抽出する。
    4. それらのタームが関連付けられている投稿を探し出すために、wp_term_relationships テーブルを経由する。
    5. タームが関連付けられている投稿のうち、投稿ステータスが公開かつ投稿タイプが投稿のものだけを抽出する。
    6. それらの投稿のカテゴリを探し出すために、wp_term_relationships テーブルを経由する。
    7. それらの投稿に関連付けられたタームのうち category タクソノミー下にあるものだけを抽出する。
    8. その category タクソノミー下のタームが list であるものだけを抽出する。

    下は、このSQL文を組み立てるときに頭の整理に使った表。個人的にはわかりやすく作れたのでこれも備忘のためにメモ。# 列の数字が上のSQL文の行番号に相当する。新しいテーブルを ON(太字のセル=その直上のセル) で JOIN したうえで、太字セルの右セルのAND句で絞り込んで……を重ねていくイメージ。

    #ASJOINANDJOINJOINANDJOINJOINANDJOINAND
    wp_termstterm_id
    3wp_term_taxonomyttterm_idtaxonomy = asinterm_taxonomy_id
    4wp_term_relationshipsrterm_taxonomy_idobject_id
    5wp_postspIDpost_status = publish
    AND
    post_type = post
    ID
    6wp_term_relationshipsr2object_idterm_taxonomy_id
    7wp_term_taxonomytt2term_taxonomy_idtaxonomy = categoryterm_id
    8wp_termst2term_idname = list

    ちなみに、絞り込み部分をすべて WHERE句にまとめても同じ結果が得られるがずいぶんわかりづらい。

    SELECT COUNT(DISTINCT t.term_id) AS total, COUNT(t.term_id) AS c_total
    FROM wp_terms AS t
    INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id
    INNER JOIN wp_term_relationships AS r ON tt.term_taxonomy_id = r.term_taxonomy_id
    INNER JOIN wp_posts AS p ON r.object_id = p.ID
    INNER JOIN wp_term_relationships AS r2 ON p.ID = r2.object_id
    INNER JOIN wp_term_taxonomy AS tt2 ON r2.term_taxonomy_id = tt2.term_taxonomy_id
    INNER JOIN wp_terms AS t2 ON tt2.term_id = t2.term_id
    WHERE tt.taxonomy = 'asin' AND p.post_status = 'publish' AND p.post_type = 'post' AND tt2.taxonomy = 'category' AND t2.name = 'list'
  • WordPress: add_action() に変数を渡す

    LOTDの配信メールをテキストとHTMLの両方を含むマルチパートにするために検索したところ、 phpmailer_init というアクションフックが見つかった。

    do_action_ref_array( 'phpmailer_init', PHPMailer $phpmailer )

    $phpmailer のAltBody メンバーにテキストをセットすればよいらしい。$phpmailer は参照渡しされるので、返り値を送れないアクションフックでもセットできる。

    ただ、アクションは外からの変数を受け取れない。

    $text_body = (組み立てたテキスト)
    add_action( 'phpmailer_init', 'lf_altbody', 10, 1 );
    function lf_altbody( $phpmailer ) {
        // $text_body をどうやって受け取る?
    }

    これは次のようにアクションを無名関数とすることで渡せることがわかった。

    $text_body = (組み立てたテキスト)
    add_action( 'phpmailer_init', function( $phpmailer ) use( $text_body ) {
        $phpmailer->AltBody = $text_body;
    }, 10, 1 );
  • 定期/定時処理

    定期処理

    仮に「最近読まれているリスト」を1時間ごとに更新するとする。通常は次のように transient の寿命を1時間としておけばよい。

    function lf_get_poplists() {
        if ( ! $lists = get_transient( 'LF_POPLISTS' ) {
    
            // リスト取得処理
            $lists = ...
    
            set_transient( 'LF_POPLISTS', $lists, HOUR_IN_SECONDS );
        }
        return $lists;
    }

    この更新を、ユーザーアクセスに依存せず定期的に実行するようにしてみた。上記のリスト取得処理部分を lf_set_poplists() という関数に切り出したうえで、1時間おきに実行するためにスケジュール登録する。

    // フック名+引数がキーとなるので array()も明示しておく
    if( ! wp_next_scheduled( 'lf_event_poplists', array() ) ) {
        // 第5引数を true にするために第4引数に array() を設定している
        if( ! $ret = wp_schedule_event( time(), 'hourly', 'lf_event_poplists', array(), true ) ) {
            // エラー処理
        }
    }
    
    // スケジュール登録したフックとアクションを関連づける
    add_action('lf_event_poplists', 'lf_set_poplists', 10, 0);
    
    // アクションの実行
    function lf_set_poplists() {
    
        // リスト取得処理
        $lists = ...
    
        set_transient( 'LF_POPLISTS', $lists );
    }
    • wp_schedule_event() がエラー時に WP_Error オブジェクトを返すようにするためには、第5引数を true にしなければならない(デフォルトは false)。となると第4引数も何かをセットしなければならず、デフォルト値の array() をセット。イベントを識別するキーはフックと引数なので、wp_next_scheduled() にも敢えて array() をセットした。
    • フック(上の例では ‘lf_event_poplists’)というものは、対応する do_action( ‘lf_event_poplists’ ) が必要なのだと思っていた。しかしスケジューラー内にフックを起動する機構があるので不要。
    • アクションに渡す引数は array() にくるむ必要がある。以下はサンプル。
    // 引数あり(引数の数が決まっている場合)
    if ( ! wp_next_scheduled( 'lf_event', array( 'a', 'b' ) ) ) {
        if ( ! $ret = wp_schedule_event( time(), 'hourly', 'lf_event', array( 'a', 'b' ), true ) ) {
            // エラー処理
        }
    }
    add_action( 'lf_event', 'lf_action', 10, 2 );
    function lf_action( $arg1, $arg2) {
    }
    
    // 引数あり(引数の数が決められない場合)
    if ( ! wp_next_scheduled( 'lf_event', array( array( 'a', 'b' ) ) ) ) {
        if ( ! $ret = wp_schedule_event( time(), 'hourly', 'lf_event', array( array( 'a','b ') ), true ) ) {
            // エラー処理
        }
    }
    add_action( 'lf_event', 'lf_action', 10, 1 );
    function lf_action( $arg ) {
    }

    定時処理

    LOTD(今日のリスト)を入れ替えるプログラムは毎日0:01に走らせている。現在はサーバーの cron から起動しているが、これを WordPress のスケジュールシステムに乗せるためには次のようにして起動時刻を指定する必要がある(実際に起動するのは、この指定時刻以降に wp_cron() が起動された時刻になる)。

    $etime = new DateTimeImmutable( 'tomorrow 0:01', wp_timezone() );
    if ( ! wp_next_scheduled( 'lf_event', array() ) ) {
        if ( ! $ret = wp_schedule_event( $etime->getTimeStamp(), 'daily', 'lf_event', array(), true ) ) {
        }
    }
  • 個別投稿の上部と下部に表示させる情報を選択する (TwentyTwenty)

    個別投稿を表示させると、本文の上に著者名、投稿日、コメント数、固定表示マークが表示される。本文の下にはタグが表示される。これらの情報の表示の有無・位置・順序を調整する方法を調べた。

    結論としては、順序はソースコードを修正しない限り変更しがたい。表示するかしないか、上部と下部のどちらに表示させるかは容易に設定できる。

    表示処理は /inc/template-tags.php にある twentytwenty_get_post_meta() が司っていた。上部に表示させる項目は、下記のフィルターで調整できる。この例では投稿日時・タグ・固定表示マークを表示させている。

    add_filter( 'twentytwenty_post_meta_location_single_top', 'lf_2020_post_meta_location_single_top', 10, 1 );
    function lf_2020_post_meta_location_single_top ( $args ) {
    	return array( 'post-date', 'tags', 'sticky' );
    }

    下部に表示させる項目は以下のフィルターを使う。この例では表示させていない。

    add_filter( 'twentytwenty_post_meta_location_single_bottom', 'lf_2020_post_meta_location_single_bottom', 10, 1 );
    function lf_2020_post_meta_location_single_bottom ( $args ) {
    	return array();
    }