カテゴリー
ブログ

「最近読まれているリスト」機能の実装

最近アクセスの多いリストを一覧表示するウィジェットとアーカイブページを作る。

人気のあるリストを紹介する場合、総アクセス数で測ると表示されるリストが固定的になるうえ、露出が高まることでアクセス数が増えるのでアクセス数の差が助長される。逆に直近の24時間など短い時間のアクセス数では変動が大きく、人気のあるリストとは言い難くなる場合もある。そこで直近7日間のアクセス数の多いリストを表示させることにした。


直近7日間のアクセス数を記録する

WordPress は投稿ごとのアクセス数を記録しないので、count_history というカスタムフィールドを作り、直近1週間のアクセス数を保存する。たとえば今日が 2021/6/2 とすると次のような配列として保存される。

Array
(
    [20210602] => 10
    [20210601] => 20
    [20210531] => 30
    [20210530] => 40
    [20210529] => 30
    [20210528] => 20
    [20210527] => 10
)

そのうえで lf_counter という別のカスタムフィールドに合計値(上の例でいえば 160)を保存する。具体的な処理は次の通り。

$count_history = get_post_meta( get_the_ID(), 'count_history', true );

// アクセス履歴あり
if( is_array($count_history) ) {
	// 7日前の日付を取得
	$expdate = (int)wp_date( 'Ymd', strtotime( '-7 days' ) );
	foreach( $count_history as $date => $count ){
		// 7日前以前の記録を削除
		if( $date <= $expdate ) {
			unset( $count_history[$date] );
		}
	}
// アクセス履歴なし
} else {
	$count_history = array();
}

// カウンタをインクリメント
$today = (int)wp_date( 'Ymd' );
( isset($count_history[$today]) ) ? $count_history[$today]++ : $count_history[$today] = 1;

// カウント履歴をカスタムフィールドに保存
update_post_meta( get_the_ID(), 'count_history', $count_history );

// 合計カウント数をカスタムフィールドに保存
update_post_meta( get_the_ID(), 'lf_counter', array_sum( $count_history ) );

カウント処理は、ページ表示処理の終盤に低い優先度で実行させる。

add_action( 'wp_footer', 'kh_access_count', 30 );
function kh_access_count() {

    /*
     * 除外するアクセスを識別する
     * and は最初の式がfalseなら次を評価しないのでorより高速
     */
    if ( is_single() // 投稿
      && has_category( 'list' ) // list カテゴリに属している
      && and ! current_user_can( 'administrator' ) // 管理者のアクセスでない
    ) {
        // 通過
    } else {
        return;
    }

    // 簡易 Crawler 除外
    // bot, bots, spider で終わる単語が入っていれば Crawler と判断
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if ( preg_match( '/.*(bots?|spider)\b/i', $ua ) ) {
        return;
    }

    /*
     * カウントする
     */
    (上述の処理)

}

直近7日間のアクセス数が多いリスト10個をキャッシュし、1時間ごとに更新する

上述のように、リスト投稿ごとのアクセス数はカスタムフィールドに持たせている。全リスト投稿をカスタムフィールドの値でソートする処理は重そうなのでキャッシュし、1時間ごとに更新することにした。

キャッシュの更新(スケジュール処理)と取得(ウィジェット表示)で同じ関数を使えるようにするため、下記のような関数を作成。

function lf_get_popular_lists( $use_cache = true ) {
	
	if ( $use_cache ) {
		$out = get_transient( 'LF_POPULAR_LISTS' );
		if( false !== $out ) {
			return $out;
		}
	}

	// アクセスカウントを多い順に10取得
	$q = new WP_Query( array(
		'post_type'      => 'post',
		'category_name'  => 'list',
		'post_status'    => 'publish',
		'posts_per_page' => 10,
		'meta_key'       => 'lf_counter',
		'orderby'        => 'meta_value_num',
		'order'          => 'DESC',
		'fields'         => 'ids',
	) );

	$lists = '<ul>';
	foreach( $q->posts as $id ){
		$lists .= '<li><a href="'.get_post_permalink( $id ).'">'.get_the_title( $id ).'</a></li>'.PHP_EOL;			
	}
	$lists .= '<li><a href="'.home_url( '/category/list?orderby=popthisweek' ).'">(もっと見る)</a></li>'.PHP_EOL;			
	$lists .= '</ul>';

	// Transientにキャッシュ
	set_transient( 'LF_POPULAR_LISTS', $lists );
}

この関数を1時間ごとに $use_cache = false で呼び出すようスケジュール登録した。

if ( ! wp_next_scheduled( 'lf_event_popular_lists', array( false ) ) ) {
    if ( ! $ret = wp_schedule_event( time(), 'hourly', 'lf_event_popular_lists', array( false ), true ) ) {
        // エラー処理
    }
}

add_action( 'lf_event_popular_lists', 'lf_get_popular_lists', 10, 1 );

キャッシュした10リストを表示するウィジェットを作成する

カスタムウィジェットは WP_Widget クラスを継承して作成する必要があったが、最近ではブロックエディタから登録できるようになったので、ショートコードを作った。

add_shortcode( 'lf_popular_lists' , 'lf_popular_lists' );
function lf_popular_lists( $args ){

    $use_cache = true;
    $out = lf_get_popular_lists( $use_cache );

    return $out;
}

アーカイブページをアクセスカウント順に表示する

ウィジェットは10個だけだが、すべてのリストをアクセス数の多い順に並べて見られる機能もつけた。これはカテゴリアーカイブページを流用した。具体的には、

  • <ROOT>/category/list → 通常の新着順
  • <ROOT>/category/list?orderby=popthisweek → 直近7日間のアクセス数順

とする。以下は Code Snippet に登録したスニペット。

// /category/list で始まる場合のみ読み込む
if ( 0 !== strpos( $_SERVER['REQUEST_URI'], '/category/list' ) ) {
    return;
}

add_action( 'pre_get_posts', 'lf_pre_get_posts_list', 10, 1 );
function lf_pre_get_posts_list( $query ) {

    if ( $query->is_main_query() ) {

        if ( 'popthisweek' == get_query_var( 'orderby', '' ) ) {

            $query->set( 'meta_key', 'lf_counter' );
            $query->set( 'orderby',  'meta_value_num date' );
            $query->set( 'order',    'DESC');
        }
    }
}

Code Snippet は Snippet 全体を eval() するので、最初の if 文のように return するとそれ以降のコードは実行されない。アクションフック ‘pre_get_posts’ はほぼ全ページで実行され得るので、当該ページの表示時のみコールバックを登録するようにした。

コメントを残す