//AdSenseにリンク

ワードプレス「週/月」ランキングを自作表示

下記の場所に追加する

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だけで足せます。必要なら軽量デザインを追加でお渡しします。