32blogby StudioMitsu
nextjs13 min read

Next.jsでGA4 APIを使い人気記事ランキングを実装する

Google Analytics 4のData APIをNext.js App Routerから呼び出し、PVベースの人気記事ランキングをISRキャッシュ付きで実装する手順をゼロから解説する。

nextjsgoogle-analyticsga4apiisr
目次

自分のブログに「人気記事ランキング」を置きたい。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プロジェクトの準備

  1. Google Cloud Console にログインする
  2. 既存のプロジェクトを使うか、新しいプロジェクトを作成する
  3. 左メニューの「APIとサービス」→「ライブラリ」を開く
  4. 「Google Analytics Data API」 を検索して「有効にする」をクリックする

サービスアカウントの作成

  1. 左メニューの「APIとサービス」→「認証情報」を開く
  2. 「+認証情報を作成」→「サービスアカウント」を選択する
  3. 名前を入力する(例: ga4-reporting
  4. 「作成して続行」をクリックする。ロールの付与はスキップしてよい
  5. 作成したサービスアカウントをクリックし、「キー」タブを開く
  6. 「鍵を追加」→「新しい鍵を作成」→「JSON」を選択して作成する

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_emailprivate_key の2つだ。

GA4プロパティにアクセス権を付与する

  1. Google Analytics を開く
  2. 左下の歯車アイコン(管理)→ プロパティの「プロパティのアクセス管理」を開く
  3. 右上の「+」→「ユーザーを追加」をクリックする
  4. ダウンロードしたJSONの client_email の値を貼り付ける
  5. 権限は 「閲覧者」 で十分だ(データの読み取りしかしないため)
  6. 「追加」をクリックする

GA4プロパティIDを確認する

GA4の管理画面で「プロパティ設定」→「プロパティの詳細」を開くと、プロパティID(数字のみ、例: 123456789)が表示される。この値もあとで使う。

Next.jsからGA4データを取得する

GCPの設定が終わったら、Next.jsプロジェクトでAPIを呼び出す。

パッケージのインストール

bash
npm install @google-analytics/data

環境変数の設定

.env.local に以下を追加する:

bash
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 を作成する:

typescript
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ルートを別途作る必要がない。

typescript
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やファイルシステムから記事メタデータを取得してスラッグとマッチングする処理を追加する。

typescript
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 を設定する:

typescript
// 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.tscacheComponents: true を有効にしておく必要がある:

typescript
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に変換する:

bash
base64 -w 0 your-service-account-key.json

出力された文字列をコピーする。

環境変数の設定

Vercelダッシュボードで Settings → Environment Variables を開き、以下を追加する:

NameValue
GA_PROPERTY_IDGA4のプロパティID(数字)
GA_SERVICE_KEY_BASE64上でbase64エンコードした文字列

Vercelの環境変数にダブルクォートは不要だ。値をそのまま貼り付ける。

デプロイと確認

bash
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/dataBetaAnalyticsDataClientscreenPageViews を取得
  • Server Component: App RouterのServer Componentから直接APIを呼び出し、APIルート不要で実装
  • ISRキャッシュ: revalidate でAPI呼び出し回数を抑え、クォータを節約
  • Vercelデプロイ: JSONキーをbase64エンコードして環境変数に設定し、改行パースの問題を回避

GA4 Data APIは無料で使える。クォータは1日200,000トークンあるので、ブログ規模なら余裕だ。ISRと組み合わせれば、ほぼリアルタイムのランキングをゼロコストで実現できる。

公式リソース: