ブログ用タグ: WordPress

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

    概要

    メールの件名に 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();
    }
  • CSSを圧縮 (minify) する

    WordPress のテーマカスタマイザーの追加CSS機能を使って追加したスタイルシートは、コメントを含めてHTMLにそのまま出力される。コメントを削除するついでに余分なスペースなども削除するようにした。

    具体的には次のような関数を作った。

    1. コメント除去
    2. 改行&タブ除去
    3. 2つ以上連続したスペース除去
    4. 記号の前後の(1文字)スペース除去

    すべてのケースを網羅できていないかもしれないが、自分の書き方においては問題なく動作した。2を先にやれば1のsフラグが不要かもしれない。

    function kh_minify_css( $css ) {
    
    	$pattern = array(
    		'|/\*.*?\*/|s',		// コメントを削除(複数行対応)
    		'/\s/',				// [\t\n\r\f]を1個のスペースに
    		'/ {2,}/',			// 2個以上のスペースを1個に
    		'/ ?([:;,{}]) ?/',	// 記号前後の1文字空白を削除
    	);
    	$replace = array(
    	    '',
    	    ' ',
    	    ' ',
    	    '\1',
    	);
    
    	// 文字列先頭に空白が1個残るケースがあるのでtrim()している
        $css = trim( preg_replace( $pattern, $replace, $css ) );
    
    	return $css;
    }
    

    この関数を、カスタムCSS読み込み後に呼ばれる ‘wp_get_custom_css’ フィルターにフックする。

    この処理は全ページの表示時に行われるので、処理負荷が高いといわれる正規表現を使った置換を毎回行うのは気が引ける。それにCSSは更新頻度が低いので、キャッシュすることとした。

    add_filter( 'wp_get_custom_css', 'kh_get_custom_css', 10, 2 );
    function kh_get_custom_css( $css, $stylesheet ){
    	
    	if( $cache = get_transient( 'LF_CUSTOM_CSS' ) ){
    		return $cache;
    	}
    
    	$css = kh_minify_css( $css );
    	set_transient( 'LF_CUSTOM_CSS', $css, DAY_IN_SECONDS );
    	return $css;
    }

    追加CSSが編集されたときにキャッシュを削除する処理も追加する。これでキャッシュも最新に保たれるはずだが、想定外を想定してキャッシュの寿命を1日にセットしておいた。

    add_action( 'customize_save_after', 'kh_customize_save_after' );
    function kh_customize_save_after( $manager ) {
    	delete_transient( 'LF_CUSTOM_CSS' );
    }
  • WordPressでAPCuを使う


    はじめに

    レンタルサーバー(エックスサーバー)で APCu というデータキャッシュが使えるようになっているので、これを WordPress で使うために何をすればよいかを調べた(結論からいうととても簡単だった)。

    1. APC/OPcacheについて(エックスサーバー)
    2. APC User Cache(PHP マニュアル)
    3. Class Reference/WP Object Cache (WordPress Codex)
    4. l3rady/WordPress-APC-Object-Cache (GitHub)

    3. は WordPress Codex 内で唯一APCuに言及しているページ。4. はそのなかで「APCuを用いてWordPress オブジェクトキャッシュに永続的なバックエンドを提供する」と紹介されていたページ。

    キャッシュ関連関数 wp_cache_*() をAPCu版に置き換える

    4. には object-cache.php と README 文書しかない。README 文書を読むと、object-cache.php を /wp-content フォルダーに置けとだけ書いてある。

    これだけでなぜ動くのか。調べてみると、wp_start_object_cache() 内で/wp-content/object-cache.php があれば読み込むようになっていた。

    if ( $first_init ) {
        if ( ! function_exists( 'wp_cache_init' ) ) {
            if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
                require_once WP_CONTENT_DIR . '/object-cache.php';

    この仕組みによって wp_cache_add() などの標準関数を外部関数に置き換えることができる。この仕組みは drop-in と命名されているらしい。_get_dropins() を呼び出すと一覧できる模様。シングルサイトでは8種のドロップインがあるようだ。

    APCu を動かしてしばらくすると管理画面でおかしな挙動が見受けられるようになったので、ファイルの冒頭に以下のコードを挿入した。

    if ( is_admin() ) {
        return false;
    }

    APCuの使われ具合を確認する

    APCuを使うだけならば、以上。APCuの働き具合を確認するためのファイルも簡単に設置できる。

    APCu のソースコード krakjoe/apcu: APCu – APC User Cache (GitHub) に同梱されている apc.php というファイルを Web サーバーのどこかに置けばよい。

    効果を確認する

    キャッシュの効果を Query Monitor というプラグインで測定した。テスト対象はサイトのトップページ。

    ページ生成時間 [S]最大メモリ使用量 [kB]データベースクエリー [S]クエリー数[Q]
    10.1616,5310.031997
    20.1215,5760.012429

    SQLを発行する回数が97から29と3割以下になっている。キャッシュされないクエリーを調べてみると、多くがget_posts()だった。get_posts()はオブジェクトキャッシュの対象外らしい。