【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

はじめに

去年(2023年)夏頃のGoogleアナリティクスのGA4完全移行に伴い、Google Analytics Data APIを使った人気記事ランキングをWordPressプラグインとして自作しました。実装からかなり期間が空いてしまったのですが、振り返ってポイントをまとめてメモしておくことにしました。

人気記事プラグイン「Simple GA Ranking」が終了

2023年7月以降、弊社でよく導入させていただいていたWordPressの人気記事ランキング表示用の無料プラグイン「Simple GA Ranking」が使えなくなりました。

Google Analyticsの仕様がユニバーサルアナリティクスからGA4に完全に切り替わったことで、プラグインが対応できなくなった様です。

「Simple GA Ranking」は動作が軽く導入も簡単でWordPress初心者の頃から長らくお世話になりました。

その一方で不具合に悩まされたことも多かったので、今回思い切ってGoogle Analytics Data APIを使った人気記事ランキングをPHPで自作してみることにしました。

ざっくり調べてみると、以前ビジネスプロフィールAPIで使ったGoogleのPHPクライアントを使えば、案外あっさりと実装できそうだったのでやってみました。今回はその内容を主にメモしています。

Simple GA Rankingの後継はある?

ちなみに「Simple GA Ranking」をそのまま使いたい場合は「Simple GA4 Ranking」という後継プラグインがあるみたいなのでこちらに切り替えると良いと思います。筆者は利用したことがないので導入する場合は下記の記事などからご利用方法をお調べください。

https://labworks.digitalcube.jp/blog/technology_20220907_simple-ga4-ranking/

WordPressで人気記事ランキングを自作する

※この記事ではWordPressのクラシックテーマへの導入を想定しています。ブロックテーマへの導入をお考えの方は、この記事のコードだけでは不十分かもしれませんので、テーマに合わせてコードの追加や改良が必要です。ご注意ください。

まず実装するにあたり必須なのは下記です。

  • Composer(Google Analytics Data for PHPをインストール)
  • Google Analyticsの管理者権限(サービスアカウントの作成と権限追加)

ComposerはPHPのライブラリ管理ツールです。PHPを普段から使っている場合は導入している場合がほとんどだと思いますので、Composerそのもののインストール方法についてはこの記事では割愛します。

一応、参考記事を置いておきます。

https://www.webdesignleaves.com/pr/php/php_composer.php

Google Analyticsの管理者権限に関しては、Google Analytics Data APIを使うために必要です。
正確には後述しますが、API接続用に作成したGoogleの「サービスアカウント」にアカウント権限を付与するために必要になります。

まずは「Composerが使える」「ランキング表示したいアナリティクスアカウントの管理者権限を持っている」という状態にしておいてください。

Google Analytics Data APIを有効化する

まずはAPIを使えるようにするためにコード以外の設定周りの作業を行います。

最初にGoogle CloudコンソールからAPIを有効化します。

Google Cloudコンソール

APIとサービス→ライブラリから「Google Analytics Data API」を検索して有効化します。

サービスアカウントを作成する

続けてGoogle Cloudコンソール上でサービスアカウントを作成します。

IAM と管理 → サービスアカウント → サービスアカウントを作成

サービスアカウント名だけ入力してあとは省略できるので後から設定でも構いません。

サービスアカウントが作成できればそのままそのアカウントの認証情報を作成します。

操作 → 鍵を管理 → 「キー」タブ → 鍵を追加 → 新しい鍵を作成 → キーのタイプ「JSON」で作成

鍵を作成したらJSONファイルをダウンロードして保存します。
JSONファイル内のclient_emailをコピーしておきます。下記のようなメールアドレスです。

[サービスアカウント名]@[GCPプロジェクト名].iam.gserviceaccount.com

サービスアカウントをGA4アカウントに追加する

続いてアナリティクスの設定。上記でコピーしたサービスアカウントをアナリティクスに追加します。

GAの管理ページにある「アカウントのアクセス管理」から追加します。

これでコード以外のところでAPIを利用するための準備は完了です。

ここから先はPHPでコードを実装していきます。

PHPクライアントライブラリをインストールする

公式のPHPクライアントライブラリがあるのでこれを使うのが手っ取り早いです。

https://github.com/googleapis/php-analytics-data#installation

下記コマンドでインストールします。

$ composer require google/analytics-data

インストールする場所については、プラグインとして作成したい場合はプラグインのディレクトリに。テーマで使いたい場合はテーマディレクトリでもいいと思います。とりあえずこの記事ではプラグイン化するのでプラグインディレクトリにインストールしている体で進めます。

人気記事を取得するコードを書いてみる

ひとまずここまででAPIが使えるようになっているはずなので、プラグイン化する前にAPIを使ってGAから人気記事を取得するコードを書いてみます。

<?php
require_once 'vendor/autoload.php';

use \Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
use \Google\Analytics\Data\V1beta\DateRange;
use \Google\Analytics\Data\V1beta\Dimension;
use \Google\Analytics\Data\V1beta\FilterExpression;
use \Google\Analytics\Data\V1beta\Filter;
use \Google\Analytics\Data\V1beta\Filter\StringFilter;
use \Google\Analytics\Data\V1beta\Filter\StringFilter\MatchType;
use \Google\Analytics\Data\V1beta\Metric;
use \Google\ApiCore\ApiException;


$credentials = './path/to/credentials.json'; //先ほどダウンロードした認証情報のJSONファイルのパスを指定
$property_id = 'MY-GA4-PROPERTY-ID'; //GA4アカウントのプロパティID
$dimension_filter_dir = '/articles/'; //URL(パス)フィルター。どのディレクトリ以下のランキングを取得するか

try {
  $client = new BetaAnalyticsDataClient([
    'credentials' => $credentials,
  ]);

  // API取得処理
  $response = $client->runReport([
    'property'   => 'properties/' . $property_id,
    'limit'      => 20, //取得件数の上限
    //期間の指定
    'dateRanges' => [
      new DateRange([
        'start_date' => '30daysAgo',
        'end_date' => 'yesterday',
      ]),
    ],
    //ディメンションの指定
    'dimensions' => [
      new Dimension([
        'name' => 'pagePath', //ここではページパスを指定
      ]),
    ],
    //上記で設定したディメンションをフィルタリングする
    'dimensionFilter' =>
    !empty($dimension_filter_dir) ? new FilterExpression([
      'filter' => new Filter([
        'field_name' => 'pagePath',
        'string_filter' => new StringFilter([
          'match_type' => MatchType::PARTIAL_REGEXP,
          'value' => $dimension_filter_dir . '[^\?]+' //ページパスが$dimension_filter_dir以下のページと一致する場合のみを取得する
        ]),
      ]),
    ]) : [],
    //指標
    'metrics' => [
      new Metric([
        'name' => 'screenPageViews', //表示回数を基準にランキングを取得
      ]),
    ],
  ]);
}
//例外処理
catch (ApiException $e) {
  //例外処理
} finally {

  foreach ($response->getRows() as $row) {
    $dimension_values = $row->getDimensionValues();
    $metric_values = $row->getMetricValues();

    //記事のパス
    $page_path   = $dimension_values[0]->getValue();
    //ページビュー数
    $pv          = $metric_values[0]->getValue();

    echo 'pagePath : ' . $page_path . '<br/>';
    echo 'pv : ' . $pv . '<br/>';
    echo "<br/>";
  }
}

こちらの出力結果が下記です。

pagePath : /articles/134/
pv : 95

pagePath : /articles/390/
pv : 61

pagePath : /articles/50/
pv : 55

pagePath : /articles/243/
pv : 41

pagePath : /articles/1217/
pv : 15

pagePath : /articles/290/
pv : 11

pagePath : /articles/826/
pv : 11

pagePath : /articles/1242/
pv : 2

pagePath : /articles/602/

ここまでデータが取れれば、あとは煮るなり焼くなりって感じになります。WordPressの関数でURLから記事IDを取得すれば、普通にループで記事を表示するのと扱いは同じようになります。

下記のような感じですね。

foreach ($response->getRows() as $row) {
    $dimension_values = $row->getDimensionValues();
    $metric_values = $row->getMetricValues();

    //記事のパス
    $page_path   = $dimension_values[0]->getValue();
    //ページビュー数
    $pv          = $metric_values[0]->getValue();

    //URLから記事IDを取得
    $post_id       = url_to_postid($page_path);
    //記事情報
    $post          = get_post($post_id);
    //パーマリンク
    $permalink     = get_permalink($post_id);
    //カスタムフィールド
    $custom_field  = get_post_meta($post_id, 'custom_field', true);
}

あとはrunReport関数で取得するデータを調整するぐらいなので、APIを使ってランキングを表示する処理に関しては、ここまでで完了です。

ここまでの内容は検索するとすぐにヒットする内容ですし、英語の公式ドキュメント眺めたらわかるものなので特にメモとして残す必要もないのですが、筆者としてはここから後が今回の記事の本題で、ここまでの内容を自作のWPプラグインに落とし込んでみたのでその際のポイントを書いていきます。

WordPressプラグイン化する

ランキング表示の処理自体は関数化してfunctions.phpに書けば事足りるのですが、

  • 認証情報のJSONファイルをテーマ内に置きたくない
  • composerでインストールしたファイルをテーマ内で管理したくない
  • 1つのサイトだけでなく他のサイトにもすぐ導入したり停止できるようにしておきたい

こんな感じで、テーマ内に置いておくのは個人的にはちょっと嫌だなぁと思うポイントがあり、それを解消するためにプラグイン化することにしました。

管理画面

下記のような簡単な画面を作りました。

他のサイトにインストールした時に使いやすいように、管理画面上で設定できる値をいくつか入力エリアとして設けておきます。必要最低限の設定のみで簡単にランキング表示できることが理想なので設定は下記3項目のみにしています。

JSONキーサービスアカウント作成時にダウンロードしたJSONファイルの中身を貼り付けて設定する
GA4プロパティID該当のアナリティクスのプロパティIDを設定
URLフィルター記事を取得したいディレクトリを設定

JSONファイルをテーマ内やアクセスできるディレクトリに配置する必要がないので、個人的にはこちらの方がスッキリ管理できて良いかなと。他のテーマで使う時もインストールしてこの3つを設定するだけで使えるようになるので導入も楽です。

管理画面の作り方

管理画面の作成方法に関しては、おおまかに説明します。

基本的には新たにテーブルは作らずに設定ページを増設するような形で作成しています。入力した値はwp_optionsテーブルに保存されるような仕組みです。保存した値を使うときはget_option()関数で呼び出します。

add_menu_page()関数で管理画面に独自メニューを追加し、そのページ内でフォームを設置してDBに保存するように作成します。

add_menu_page(
  '人気記事(GA4)設定', //メニューページのタイトル
  '人気記事(GA4)設定', //メニュー表示名
  'manage_options', //権限
  'original_ga4_ranking', //メニュースラッグ
  'setting_page_html', //コールバック関数。ここで管理画面のソースを自由に作成できる
  'dashicons-admin-settings', //メニュー横に表示されるアイコン
  5
);

設定画面の保存方法は、PHPでPOST処理やSQLを書かなくてもWordPressの仕組みがあるためそれを使います。

下記のWP組み込み関数を使います。

register_settingオプションの設定情報を登録する
add_settings_section画面にセクションを追加する
add_settings_field画面にフィールドを追加する
settings_fieldsnonce等の情報を出力する
do_settings_sectionsオプションの設定情報を出力する
submit_button保存ボタンを出力する

使い方としてはregister_setting()で設定情報を登録します。

register_setting(
  'ga4-ranking-settings-group',  //オプショングループ名
  'ga4_ranking_credentials' //オプション名
);

次にadd_menu_page()のコールバック関数setting_page_html()の中にフォームを書いていきます。

function setting_page_html()
{
?>
  <form method="post" action="options.php">
    <?php settings_fields('ga4-ranking-settings-group') ?>
    <?php do_settings_sections('ga4-ranking-settings-group') ?>
    <?php submit_button() ?>
  </form>
<?php
}

WordPressのオプションに保存するので、POST先はoptions.phpにします。
settings_fields('オプショングループ名')でnonce情報を挿入、do_settings_sections('オプショングループ名')でグループに登録されたセクション・フィールドを出力します。

出力するフィールドは下記のように追加します。

//セクションを追加
add_settings_section(
  'ga4_ranking_section', //セクションスラッグ
  '', //セクション名(ここでは不要だったので空欄)
  [], //セクションのコールバック関数
  'ga4-ranking-settings-group' //セクションを表示するオプショングループ名
);

//フィールドを追加
add_settings_field(
  'ga4_ranking_credentials', //フィールドスラッグ
  'JSONキー', //フィールド名
  'render_credentials_field', //コールバック関数
  'ga4-ranking-settings-group', //フィールドを表示するオプショングループ名
  "ga4_ranking_section", //フィールドを表示するセクション
);

//フィールドの中身 register_settingで登録したオプション名をname属性に設定する
function render_credentials_field()
{
  ?>
  <textarea class="large-text code" id="ga4_ranking_credentials" name="ga4_ranking_credentials" cols="160" rows="7"><?php echo esc_attr(get_option('ga4_ranking_credentials')); ?></textarea>
<?php
}

これでJSONキーの入力項目を画面に追加でき、更新できるようになりました。この作業を繰り返してGA4プロパティID、URLフィルターを追加すればひとまず管理画面の作成は完了です。

キャッシュについては次で説明します。

キャッシュについて

「キャッシュ」に関しては、短時間の表示毎に最新の情報をAPIから取得する必要はないと思いますので、基本的に一度APIからデータを取得したらWordPressにキャッシュします。(Transients API)

短時間の表示というのは、例えばサイトを回遊してページ遷移している時の数分〜数十分などです。もっと言うと、GAにデータが蓄積されるまでの期間を考慮すると、人気記事が1時間毎や2〜3時間毎に順位がコロコロ置き換わるなんてことはほぼないので、1日1回更新するぐらいで充分です。そのため今回作ったプラグインは半日に1回ぐらいのタイミングで自動的にキャッシュが切れるように実装しています。

ただ、個人的にそのキャッシュを手動で強制的にクリアしたいことが何度かあったため「キャッシュをクリア」ボタンも設置しています。

ちなみに、キャッシュはAPIとの通信を必要最低限に抑えられるためページ表示の高速化やAPIのトークン消費を必要最低限に抑えられるメリットがあります。

キャッシュの部分のコードの説明は長くなるので省略します。内容としてはAPIを取得するときにキャッシュして、12時間経過するか、管理画面のキャッシュをクリアを押すと強制的にクリアするように実装しています。

プラグインのコードはgithubに上げているので省略した部分も含めてコードを手元で見たい方はこちらをどうぞ。

https://github.com/inos3910/original-ga4-ranking

テンプレートでの記述方法について

テンプレート側では、下記の専用クラスの関数を使ってランキングデータを取得します。

class Ga4RankingPlugin
{
    // 省略

  public static function get_ranking_data($limit = 1000)
  {

    require_once plugin_dir_path(__FILE__) . 'vendor/autoload.php';

    //Transient cache
    $transient_name = "original_ga4_ranking_data_{$limit}";
    $rank_data      = get_transient($transient_name);

    //キャッシュありの場合
    if (!empty($rank_data)) {
      return $rank_data;
    }


    //キャッシュなしの場合
    $rank_data = [];
        
    //管理画面で保存した値を取得
    $json                 = get_option('ga4_ranking_credentials');
    $property_id          = get_option('ga4_ranking_property_id');
    $dimension_filter_dir = get_option('ga4_ranking_dimension_filter');

    if (empty($json) || empty($property_id)) {
      return $rank_data;
    }

    $credentials = json_decode($json, true);

    try {
      $client = new BetaAnalyticsDataClient([
        'credentials' => $credentials,
      ]);

      // API取得処理
      $response = $client->runReport([
        'property'   => 'properties/' . $property_id,
        'limit'      => 1000,
        'dateRanges' => [
          new DateRange([
            'start_date' => '30daysAgo',
            'end_date' => 'yesterday',
          ]),
        ],
        'dimensions' => [
          new Dimension([
            'name' => 'pagePath',
          ]),
        ],
        'dimensionFilter' =>
        !empty($dimension_filter_dir) ? new FilterExpression([
          'filter' => new Filter([
            'field_name' => 'pagePath',
            'string_filter' => new StringFilter([
              'match_type' => MatchType::PARTIAL_REGEXP,
              'value' => $dimension_filter_dir . '[^\?]+'
            ]),
          ]),
        ]) : [],
        'metrics' => [
          new Metric([
            'name' => 'screenPageViews',
          ]),
        ],
      ]);
    }
    //例外処理
    catch (ApiException $e) {
      return $rank_data;
    } finally {
      if (empty($response)) {
        return $rank_data;
      }

      foreach ($response->getRows() as $row) {
        $dimension_values = $row->getDimensionValues();
        if (empty($dimension_values)) {
          continue;
        }

        $metric_values = $row->getMetricValues();
        if (empty($metric_values)) {
          continue;
        }

        //パスから投稿IDを取得
        $page_path   = $dimension_values[0]->getValue();
        $post_id     = url_to_postid($page_path);
        if (empty($post_id)) {
          continue;
        }

        //投稿がない・もしくはステータスが公開ではない場合にはスキップ
        $post = get_post($post_id);
        if (empty($post) ||  $post->post_status !== 'publish') {
          continue;
        }

        $pv          = $metric_values[0]->getValue();
        $url         = get_permalink($post_id);

        $rank_data[] = [
          'page_path' => $page_path,
          'pv'        => $pv,
          'post_id'   => $post_id,
          'url'       => $url
        ];

        --$limit;
        if ($limit < 1) {
          break;
        }
      }

      set_transient($transient_name, $rank_data, 12 * HOUR_IN_SECONDS);

      return $rank_data;
    }
  }
}

関数を使うと人気記事一覧が配列として返ってくるようになっています。

ポイントは引数に指定する記事の取得件数の使い方です。

普通に考えると、記事の取得件数=APIの取得件数だと思って下記のようにするかと思います。

// API取得処理
$response = $client->runReport([
        'property'   => 'properties/' . $property_id,
        'limit'      => $limit, //ここに引数をそのまま使う
        // 以下省略...
]);

一見これでいけそうですが、こうすると「記事を削除した時」や「記事URLを変更した時」に不都合が出てきます。

例えば記事20件取得したい場合、そのうちの1記事を削除したとします。その場合、APIはただただGA上の表示回数順にデータを引っ張ってくるだけなので、削除された記事も含めて20件取得してしまいます。なのでテンプレート側で処理すると19件しか表示できなくなります。

そのため、取得件数はデフォルトで多めに1000件に設定しておいて、ループで回して公開されている記事を20件配列に含めることができたらループから出て配列を返す、という処理にすることで、削除された記事やURL変更された記事はスキップするようにしています。

こちらの関数を具体的には下記のように使います。

<?php

use Sharesl\Original\Ga4\Ranking\Ga4RankingPlugin;

//ランキングから記事を10件取得
$articles = Ga4RankingPlugin::get_ranking_data(10);
if (!empty($articles)) :
  foreach ((array)$articles as $article) :
    if (empty($article['post_id'])) {
      continue;
    }

    //記事タイトルを取得
    $title = get_the_title($article['post_id']);
    //記事URLを取得
    $url = get_permalink($article['post_id']);
?>
    <a href="<?php echo esc_url($url) ?>"><?php echo esc_html($title) ?></a>
<?php
  endforeach;
endif;

ここでのポイントは、get_ranking_data()関数の引数に表示したい記事数を入力することです。

一覧を表示させる場所によって、表示数を変えたい場合などもあるかと思います。そういった場合に引数で10件とか20件とか指定すると、指定した数に応じて自動的にキャッシュを作成するように実装しています。なので複数の場所で表示したい場合に効率良く呼び出すことが可能です。

おわりに

APIを使って人気記事ランキングプラグインを自作してみました。

自作プラグインにすることで、WordPressの管理画面上でデータを管理してDBに保存しておけるので少し便利にできました。想定していた他のサイトへの導入や停止もかなり簡単にできるようになりましたし、コードの内容を把握していることで、万が一の時の不具合に対する対応やカスタマイズもこれまでよりやりやすくなりました。

また、管理画面の項目は自分で好きなように追加できるので、今のところ必要ありませんがキャッシュの有効期限やランキングの対象期間といった項目も設定できるようにしてもいいかもしれませんし、細かいところで言うとsave_postフックとかで記事の更新時にキャッシュが消えるようにしておくのも必要かなと思いました。

ひとまず今日はここまで。