自分のブログに「人気記事ランキング」を置きたい。Google Analytics 4(GA4)にはページビューのデータが溜まっている。あとはそのデータをNext.jsから取得して表示するだけだ。
この記事では、GA4のData APIをNext.js App Routerから呼び出し、PVベースの人気記事ランキングを実装する手順をゼロから解説する。ISRキャッシュを使うのでAPIの呼び出し回数を抑えつつ、常に最新に近いランキングを表示できる。
GA4 Data APIとは何か
GA4 Data APIは、Googleアナリティクスに蓄積されたデータをプログラムから取得するためのAPIだ。ブラウザでGA4のダッシュボードを開いて見ているデータと同じものを、コードから取得できる。
僕たちが使うのは runReport というメソッドで、これは「どのページが何回見られたか」のようなレポートを生成する。GA4のダッシュボードで「ページとスクリーン」レポートを見るのと同じことをAPIで行う。
料金は無料だ。 GA4 Standard(無料版)のプロパティであれば、Data APIに追加料金はかからない。1日あたり200,000トークンのクォータ制限があるが、ブログのランキング取得程度では使い切ることはまずない。
GCPサービスアカウントを作成する
GA4 Data APIを使うには、Google Cloud Platform(GCP)でサービスアカウントを作成し、GA4プロパティへのアクセス権を付与する必要がある。手順は多いが、一度だけやれば済む作業だ。
GCPプロジェクトの準備
- Google Cloud Console にログインする
- 既存のプロジェクトを使うか、新しいプロジェクトを作成する
- 左メニューの「APIとサービス」→「ライブラリ」を開く
- 「Google Analytics Data API」 を検索して「有効にする」をクリックする
サービスアカウントの作成
- 左メニューの「APIとサービス」→「認証情報」を開く
- 「+認証情報を作成」→「サービスアカウント」を選択する
- 名前を入力する(例:
ga4-reporting) - 「作成して続行」をクリックする。ロールの付与はスキップしてよい
- 作成したサービスアカウントをクリックし、「キー」タブを開く
- 「鍵を追加」→「新しい鍵を作成」→「JSON」を選択して作成する
JSONファイルがダウンロードされる。中身はこんな構造だ:
{
"type": "service_account",
"project_id": "your-project-id",
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "ga4-reporting@your-project-id.iam.gserviceaccount.com",
"client_id": "...",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
この中で重要なのは client_email と private_key の2つだ。
GA4プロパティにアクセス権を付与する
- Google Analytics を開く
- 左下の歯車アイコン(管理)→ プロパティの「プロパティのアクセス管理」を開く
- 右上の「+」→「ユーザーを追加」をクリックする
- ダウンロードしたJSONの
client_emailの値を貼り付ける - 権限は 「閲覧者」 で十分だ(データの読み取りしかしないため)
- 「追加」をクリックする
GA4プロパティIDを確認する
GA4の管理画面で「プロパティ設定」→「プロパティの詳細」を開くと、プロパティID(数字のみ、例: 123456789)が表示される。この値もあとで使う。
Next.jsからGA4データを取得する
GCPの設定が終わったら、Next.jsプロジェクトでAPIを呼び出す。
パッケージのインストール
npm install @google-analytics/data
環境変数の設定
.env.local に以下を追加する:
GA_PROPERTY_ID=123456789
GA_CLIENT_EMAIL=ga4-reporting@your-project-id.iam.gserviceaccount.com
GA_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...(省略)...\n-----END PRIVATE KEY-----\n"
GA_PRIVATE_KEY はJSONファイルの private_key の値をそのまま貼り付ける。ダブルクォートで囲むこと。値に含まれる \n は改行文字のエスケープ表現だ。そのまま設定すればよい。コード側で .replace(/\\n/g, '\n') を使って実際の改行に変換する。
GA4クライアントの作成
lib/ga4.ts を作成する:
import { BetaAnalyticsDataClient } from "@google-analytics/data";
function getCredentials() {
// base64エンコードされたJSONキー(Vercel推奨)
if (process.env.GA_SERVICE_KEY_BASE64) {
return JSON.parse(
Buffer.from(process.env.GA_SERVICE_KEY_BASE64, "base64").toString()
);
}
// ローカル開発用フォールバック
return {
client_email: process.env.GA_CLIENT_EMAIL,
private_key: process.env.GA_PRIVATE_KEY?.replace(/\\n/g, "\n"),
};
}
const client = new BetaAnalyticsDataClient({
credentials: getCredentials(),
});
const propertyId = process.env.GA_PROPERTY_ID;
export type PageViewEntry = {
path: string;
views: number;
};
export async function getTopPages(
limit: number = 10,
days: number = 30
): Promise<PageViewEntry[]> {
const [response] = await client.runReport({
property: `properties/${propertyId}`,
dimensions: [{ name: "pagePath" }],
metrics: [{ name: "screenPageViews" }],
dateRanges: [{ startDate: `${days}daysAgo`, endDate: "today" }],
orderBys: [
{
metric: { metricName: "screenPageViews" },
desc: true,
},
],
limit,
});
if (!response.rows) return [];
return response.rows.map((row) => ({
path: row.dimensionValues?.[0]?.value ?? "",
views: parseInt(row.metricValues?.[0]?.value ?? "0", 10),
}));
}
getCredentials() は2つの認証方法をサポートしている。GA_SERVICE_KEY_BASE64 があればJSONキーをbase64デコードして使い、なければ個別の環境変数にフォールバックする。Vercelでは前者を推奨する(理由は後述)。
runReport に渡しているパラメータを説明する:
dimensions: [{ name: "pagePath" }]— URLのパスごとにグループ化するmetrics: [{ name: "screenPageViews" }]— ページビュー数を取得するdateRanges— 過去30日間のデータを対象にするorderBys— ページビュー数の降順(多い順)で並べるlimit— 上位10件に絞る
人気記事ランキングコンポーネントを作る
取得したデータを表示するServer Componentを作る。App RouterのServer Componentなら、コンポーネントの中で直接 async 関数を呼べる。APIルートを別途作る必要がない。
import Link from "next/link";
import { getTopPages } from "@/lib/ga4";
export async function PopularPosts() {
const pages = await getTopPages(10, 30);
// トップページやカテゴリページを除外し、記事ページだけに絞る
const articles = pages.filter(
(p) => p.path.match(/^\/[a-z]{2}\/[^/]+\/[^/]+$/) && p.views > 0
);
if (articles.length === 0) return null;
return (
<section>
<h2 className="text-xl font-bold mb-4">Popular Posts</h2>
<ol className="space-y-3">
{articles.slice(0, 5).map((entry, i) => (
<li key={entry.path} className="flex items-baseline gap-3">
<span className="text-sm font-mono text-muted">{i + 1}</span>
<Link href={entry.path} className="text-sm hover:underline">
{entry.path}
</Link>
<span className="text-xs text-muted ml-auto">
{entry.views.toLocaleString()} PV
</span>
</li>
))}
</ol>
</section>
);
}
このコンポーネントは pagePath(URLパス)をそのまま表示している。記事タイトルを表示したい場合は、CMSやファイルシステムから記事メタデータを取得してスラッグとマッチングする処理を追加する。
import { getAllArticles } from "@/lib/content";
import { getTopPages } from "@/lib/ga4";
export async function getPopularArticles(locale: string, limit: number = 5) {
const [pages, articles] = await Promise.all([
getTopPages(50, 30),
Promise.resolve(getAllArticles(locale)),
]);
const viewMap = new Map(pages.map((p) => [p.path, p.views]));
return articles
.map((article) => ({
...article,
views: viewMap.get(`/${locale}/${article.category}/${article.slug}`) ?? 0,
}))
.filter((a) => a.views > 0)
.sort((a, b) => b.views - a.views)
.slice(0, limit);
}
ISRでAPIコールをキャッシュする
Server Componentで直接GA4 APIを呼ぶと、ページがリクエストされるたびにGoogleのAPIを叩いてしまう。これではクォータを消費するし、レスポンスも遅くなる。
ISR(Incremental Static Regeneration)を使えば、一定時間キャッシュしたHTMLを返し、バックグラウンドでデータを再取得できる。
ランキングを表示するページに revalidate を設定する:
// app/[locale]/popular/page.tsx
import { getPopularArticles } from "@/lib/ga4";
export const revalidate = 3600; // 1時間ごとに再生成
export default async function PopularPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const articles = await getPopularArticles(locale);
return (
<main>
<h1>Popular Articles</h1>
<ul>
{articles.map((article) => (
<li key={article.slug}>
<a href={`/${locale}/${article.category}/${article.slug}`}>
{article.title}
</a>
<span>{article.views.toLocaleString()} views</span>
</li>
))}
</ul>
</main>
);
}
revalidate = 3600 は「このページを最大1時間キャッシュする」という意味だ。1時間経過後の最初のリクエストでバックグラウンドでページを再生成し、次のリクエストからは新しいHTMLが返される。
関数単位でキャッシュする方法
ページ全体ではなく、GA4 APIの呼び出しだけをキャッシュしたい場合は use cache ディレクティブを使う。next.config.ts で cacheComponents: true を有効にしておく必要がある:
import { cacheLife } from "next/cache";
import { getTopPages } from "@/lib/ga4";
export async function getCachedTopPages(limit: number, days: number) {
"use cache";
cacheLife("hours");
return getTopPages(limit, days);
}
これなら複数のページで同じランキングデータを使い回しつつ、APIコールは数時間に1回で済む。
Vercelにデプロイする
ローカルでは .env.local に環境変数を直接書いたが、Vercelでは private_key の改行文字(\n)がパースエラーを起こすことがある。DECODER routines::unsupported というエラーが出たら、この問題だ。
回避策として、JSONキーファイルをまるごとbase64エンコードして1つの環境変数に入れる 方法を推奨する。改行の問題を完全に回避できる。
base64エンコード
ダウンロードしたJSONキーファイルをbase64に変換する:
base64 -w 0 your-service-account-key.json
出力された文字列をコピーする。
環境変数の設定
Vercelダッシュボードで Settings → Environment Variables を開き、以下を追加する:
| Name | Value |
|---|---|
GA_PROPERTY_ID | GA4のプロパティID(数字) |
GA_SERVICE_KEY_BASE64 | 上でbase64エンコードした文字列 |
Vercelの環境変数にダブルクォートは不要だ。値をそのまま貼り付ける。
デプロイと確認
git add .
git commit -m "feat: add GA4 popular posts ranking"
git push
Vercelが自動でビルド・デプロイする。デプロイ後、ランキングページにアクセスしてデータが表示されれば成功だ。
トラブルシューティング
うまく動かない場合、以下のエラーに対応する:
DECODER routines::unsupported:private_keyの改行パースに失敗している。base64エンコード方式に切り替えるPERMISSION_DENIED: Google Analytics Data API has not been used in project...: GCPコンソールで「Google Analytics Data API」を有効にしていない。「APIとサービス」→「ライブラリ」から有効化するPERMISSION_DENIED: User does not have sufficient permissions...: サービスアカウントのメールアドレスがGA4プロパティの「閲覧者」に追加されていない- 環境変数が反映されない: Vercelで環境変数を変更したら再デプロイが必要だ。Deployments から最新のデプロイを「Redeploy」する
まとめ
この記事で実装したこと:
- GCPサービスアカウント: Google Cloud Consoleでサービスアカウントを作成し、GA4プロパティに閲覧権限を付与
- GA4 Data API:
@google-analytics/dataのBetaAnalyticsDataClientでscreenPageViewsを取得 - Server Component: App RouterのServer Componentから直接APIを呼び出し、APIルート不要で実装
- ISRキャッシュ:
revalidateでAPI呼び出し回数を抑え、クォータを節約 - Vercelデプロイ: JSONキーをbase64エンコードして環境変数に設定し、改行パースの問題を回避
GA4 Data APIは無料で使える。クォータは1日200,000トークンあるので、ブログ規模なら余裕だ。ISRと組み合わせれば、ほぼリアルタイムのランキングをゼロコストで実現できる。
公式リソース:
- GA4 Data API Overview — APIの全体像
- Dimensions & Metrics — 使えるディメンションとメトリックの一覧
- Data API Quotas — クォータ制限の詳細
- @google-analytics/data — Node.jsクライアントライブラリ