ワードプレス「週/月」ランキングを自作表示
下記の場所に追加する
wp-content/mu-plugins/tt-pv-ranking.php
<?php
/**
* Title: TT PV Ranking (weekly/monthly) for SWELL
* Desc : AJAXでPVを日別に記録し、直近7/30日の合計でランキングを表示
* Place: wp-content/mu-plugins/tt-pv-ranking.php
*/
if (!defined('ABSPATH')) exit;
class TT_PV_Ranking {
const META_DAILY = 'tt_pv_daily'; // 連想配列: 'Ymd' => count
const META_7 = 'tt_pv_7'; // 直近7日の合計
const META_30 = 'tt_pv_30'; // 直近30日の合計
const NONCE_KEY = 'tt_pv_rank_nonce';
const ACTION = 'tt_pv_rank_increment';
public function __construct() {
add_action('wp_enqueue_scripts', [$this, 'enqueue'], 20);
add_action('wp_ajax_nopriv_' . self::ACTION, [$this, 'ajax_increment']);
add_action('wp_ajax_' . self::ACTION, [$this, 'ajax_increment']);
add_shortcode('tt_ranking', [$this, 'shortcode_ranking']);
}
/** 単一投稿ページのみ、極小JSを読み込み */
public function enqueue() {
if (!is_singular('post')) return;
if ($this->is_bot()) return;
$post_id = get_queried_object_id();
wp_register_script('tt-pv-rank', '', [], null, true);
// ★ 先にローカライズ変数を登録
wp_localize_script('tt-pv-rank', 'TT_PV_RANK', [
'ajax' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce(self::NONCE_KEY),
'pid' => $post_id,
]);
// ★ その後にインラインスクリプト
wp_add_inline_script('tt-pv-rank', $this->js_snippet($post_id));
wp_enqueue_script('tt-pv-rank');
}
/** 連打防止付きの最小JS(約500B) */
private function js_snippet($post_id) {
$action = self::ACTION; // ★← クラス定数を変数に入れる
return <<<JS
(function(){
if(!window || !document) return;
try{
var k = 'ttpv_'+(TT_PV_RANK && TT_PV_RANK.pid ? TT_PV_RANK.pid : '{$post_id}');
if(localStorage.getItem(k)) return; // 同一端末の連打防止
var fd=new FormData();
fd.append('action','{$action}'); // ★← ここが定数の実値になる
fd.append('nonce',TT_PV_RANK.nonce);
fd.append('post_id',TT_PV_RANK.pid);
// ビューポートに入って数百ms後に送信(滞在意図を仮想判定)
var sent=false;
var send=function(){
if(sent) return; sent=true;
fetch(TT_PV_RANK.ajax,{method:'POST',body:fd,credentials:'same-origin'})
.then(function(){localStorage.setItem(k,'1');})
.catch(function(){});
};
var t=setTimeout(send,800);
window.addEventListener('beforeunload',function(){ if(!sent) send(); });
}catch(e){}
})();
JS;
}
/** AJAX: PVインクリメント */
public function ajax_increment() {
check_ajax_referer(self::NONCE_KEY, 'nonce');
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
if (!$post_id || $this->is_bot() || is_user_logged_in() && current_user_can('edit_post', $post_id)) {
wp_send_json_success(['skipped'=>true]); // Bot/編集権限持ち除外
}
// 日別配列を取得・更新
$daily = get_post_meta($post_id, self::META_DAILY, true);
if (!is_array($daily)) $daily = [];
$today = current_time('Ymd'); // WPのタイムゾーン準拠(サイト設定に依存)
$daily[$today] = isset($daily[$today]) ? intval($daily[$today]) + 1 : 1;
// 直近30日だけ保持してメモリ節約
$daily = $this->trim_days($daily, 30);
update_post_meta($post_id, self::META_DAILY, $daily);
// 直近7/30日合計を計算→meta更新
$sum7 = $this->sum_days($daily, 7);
$sum30 = $this->sum_days($daily, 30);
update_post_meta($post_id, self::META_7, $sum7);
update_post_meta($post_id, self::META_30, $sum30);
wp_send_json_success(['post_id'=>$post_id,'w'=>$sum7,'m'=>$sum30]);
}
private function trim_days($daily, $keep=30) {
krsort($daily, SORT_STRING); // 新しい順
$daily = array_slice($daily, 0, $keep, true);
ksort($daily, SORT_STRING); // 古い順に戻す(見やすさ)
return $daily;
}
private function sum_days($daily, $range) {
$keys = array_keys($daily);
$sum = 0;
$cut = (int) date_i18n('Ymd', strtotime('-'.($range-1).' days', current_time('timestamp')));
foreach ($daily as $ymd => $cnt) {
if ((int)$ymd >= $cut) $sum += (int)$cnt;
}
return $sum;
}
private function is_bot() {
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '';
if (!$ua) return true;
$bots = ['bot','crawl','spider','slurp','facebookexternalhit','mediapartners-google','bingpreview','crawler','semrush','ahrefs'];
foreach ($bots as $b) { if (strpos($ua, $b)!==false) return true; }
return false;
}
/** [tt_ranking period="week|month" limit="10" cat=""] */
public function shortcode_ranking($atts) {
$a = shortcode_atts([
'period' => 'week', // week|month
'limit' => 10,
'cat' => '', // カテゴリID or スラッグ
'schema' => 'on', // ★追加:on/off切替用
], $atts, 'tt_ranking');
$meta_key = ($a['period'] === 'month') ? self::META_30 : self::META_7;
$tax_query = [];
if (!empty($a['cat'])) {
$tax_query = [[
'taxonomy' => 'category',
'field' => is_numeric($a['cat']) ? 'term_id' : 'slug',
'terms' => $a['cat'],
]];
}
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => intval($a['limit']),
'ignore_sticky_posts' => true,
'meta_key' => $meta_key,
'orderby' => 'meta_value_num',
'order' => 'DESC',
'tax_query' => $tax_query,
'no_found_rows' => true,
]);
if (!$q->have_posts()) return '';
// 軽量HTML+ItemList構造化(JSON-LD)を同時出力
ob_start();
$list = [];
echo '<ol class="tt-ranking-list tt-'. esc_attr($a['period']) .'">';
$pos = 1;
while ($q->have_posts()) { $q->the_post();
$pid = get_the_ID();
$url = get_permalink($pid);
$title = esc_html(get_the_title($pid));
$thumb = get_the_post_thumbnail($pid, 'medium', ['loading'=>'lazy', 'decoding'=>'async', 'class'=>'tt-thumb']);
$list[] = ['position'=>$pos, 'url'=>$url, 'name'=>$title];
echo '<li class="tt-item">';
echo '<a href="'.esc_url($url).'" class="tt-link">';
if ($thumb) echo $thumb;
echo '<span class="tt-title">'. $pos .'位:'. $title .'</span>';
echo '</a></li>';
$pos++;
}
echo '</ol>';
wp_reset_postdata();
// JSON-LD
$itemList = [
"@context" => "https://schema.org",
"@type" => "ItemList",
"itemListElement" => array_map(function($i){
return [
"@type" => "ListItem",
"position" => $i['position'],
"url" => $i['url'],
"name" => $i['name'],
];
}, $list)
];
if (strtolower($a['schema']) === 'on') {
echo '<script type="application/ld+json">' .
wp_json_encode($itemList, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) .
'</script>';
}
return ob_get_clean();
}
}
new TT_PV_Ranking();
/** すごく薄いスタイル(必要なら調整) */
add_action('wp_head', function(){
echo '<style>
/* TT ランキング:コンパクト&角丸なし(サムネ少し大きめ) */
.tt-ranking-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
.tt-ranking-list .tt-item{display:flex;align-items:flex-start}
.tt-ranking-list .tt-link{display:flex;gap:10px;align-items:flex-start;text-decoration:none;color:#111}
.tt-ranking-list .tt-title{font-weight:600;font-size:13px;line-height:1.55;color:#111;letter-spacing:.01em}
/* サムネ:角丸なし&少し大きめ */
.tt-ranking-list img.tt-thumb{
width:auto;
height:64px; /* ★ここを大きくしました */
aspect-ratio:4/3;
object-fit:cover;
border-radius:0;
flex:0 0 auto;
background:#f3f4f6;
}
/* 行間と区切り線で整える */
.tt-ranking-list .tt-item + .tt-item{
border-top:1px solid #eee;
padding-top:8px;
margin-top:4px;
}
/* ホバー時は下線のみ */
.tt-ranking-list .tt-link:hover .tt-title{text-decoration:underline}
/* スマホ最適化(超小型端末) */
@media (max-width:360px){
.tt-ranking-list img.tt-thumb{height:58px} /* スマホ時も少し大きめ */
.tt-ranking-list .tt-title{font-size:12.5px}
}
/* サムネに存在感(PCのみ70px) */
@media (min-width:769px){
.tt-ranking-list img.tt-thumb{height:70px}
}
</style>';
}, 99);
使い方(本文にショートコードを挿すだけ)
週間ランキング:
[tt_ranking period="week" limit="10"]
月間ランキング:
[tt_ranking period="month" limit="10"]
カテゴリ限定(IDでもスラッグでも可):
[tt_ranking period="week" limit="8" cat="cafe"]
Q. すぐ順位が出ない
A. 新規導入直後はデータが無いので、表示は出るが並びは弱いです。数日~で安定します。
Q. 管理者の閲覧はカウントされる?
A. 標準では編集権限ユーザーは除外。Bot/クローラも簡易除外。
Q. キャッシュでJSも遅延される?
A. 遅延系プラグインを使っていても、DOM読込後800msで1回だけ送信するだけなので軽量。必要なら遅延除外に tt-pv-rank を追加。
Q. 既存の「SWELLのPV計測機能」と競合しない?
A. 競合しません(別メタキーに保存)。既存カウンタはそのままでOK。
Q. 見た目をもっとリッチにしたい
A. 数字バッジや王冠アイコンをCSSだけで足せます。必要なら軽量デザインを追加でお渡しします。
ディスカッション
コメント一覧
まだ、コメントがありません