🏝️

Google Analytics Data APIを使用して、ブログの視聴回数ランキングを表示しよう!

2025/03/18に公開
1

背景と目的

みなさんこんにちは。クラウドエースの清野です。

弊社では、10月の組織改編により、事業領域ごとにチームが再編されました。
そのため、これまでバックエンドを担当していたメンバーも、フロントエンドやインフラ領域に携わる機会が増えています。

そこで、元々バックエンドエンジニアリング部に所属していた私が、フロント領域のNext.jsと新たに加入したメディア事業部で活かせそうな Google Analytics を使用した学習内容を共有したいと思います。

本記事のゴール

Google Analytics で集計したブログデータを Google Analytics Data API を用いて取得し、Next.js で構築した Web 画面に表示します。

画像で説明

使用技術の一覧

  • Next.js
  • Google Analytics
  • Google Analytics Data API
  • microCMS
  • Vercel

手順

Google Analyticsの初期設定

https://www.google.com/analytics にアクセスし、初期設定を行います。

Next.jsへのGoogle Analytics組み込み

以下の公式ドキュメントを参考に、Next.js に Google Analytics を組み込みます。

// app/layout.js
import { GoogleAnalytics } from '@next/third-parties/google'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
      <GoogleAnalytics gaId="G-XYZ" />
    </html>
  )
}

Next.js公式ドキュメント

今回は、microCMS からブログ情報を取得します。
Next.js と microCMS の構成は、公式のテンプレートがございますのでそちらを参考にしてみてください。
microCMSのテンプレート

Google Cloud プロジェクトを作成し、Google Analytics Data API を有効化

Google Cloud プロジェクトを作成し、Google Analytics Data API を有効化します。

画像で説明

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

  1. IAMと管理 > サービスアカウント > サービスアカウントを作成 を選択
    画像で説明
  2. 任意のサービスアカウント名を入力し、完了 を選択
    画像で説明
  3. 作成したサービスアカウントの詳細を確認し、メールアドレスを保管
    画像で説明

Google Analyticsの設定

  1. Google Analytics の ホーム画面 > 設定 > プロパティ > プロパティのアクセス管理 > ユーザーを追加 を選択

  2. プロパティのアクセス管理でユーザーを新規追加

    • ユーザーは、先ほど作成したサービスアカウントのメールアドレスとなります。
  3. 権限は、閲覧者以上の権限を付与してください。

    画像で説明

Next.jsの実装

Google Analytics のどの指標を取得したいかはこちらの公式ドキュメントに記載があります。

今回は、ブログの視聴回数のランキングを取得したいので、screenPageViews(視聴回数)を取得します。

また、dimensionFilterを使用してブログとは関係ない記事を弾くようにしています(トップページやaboutページなど)。

const propertyId = {'プロパティID'};
const serviceAccountKey = JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS || '{}');

async function getAccessToken() {
    const response = await fetch('https://oauth2.googleapis.com/token', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
            grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            assertion: await createJwt(),
        }),
    });
    const data = await response.json();
    return data.access_token;
}

async function createJwt() {
    const header = {
        alg: 'RS256',
        typ: 'JWT',
    };
    const payload = {
        iss: serviceAccountKey.client_email,
        scope: 'https://www.googleapis.com/auth/analytics.readonly',
        aud: 'https://oauth2.googleapis.com/token',
        exp: Math.floor(Date.now() / 1000) + 3600,
        iat: Math.floor(Date.now() / 1000),
    };

    const encodedHeader = btoa(JSON.stringify(header));
    const encodedPayload = btoa(JSON.stringify(payload));

    const keyData = str2ab(atob(serviceAccountKey.private_key.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\n/g, '')));
    const key = await crypto.subtle.importKey(
        'pkcs8',
        keyData,
        {
            name: 'RSASSA-PKCS1-v1_5',
            hash: 'SHA-256',
        },
        false,
        ['sign']
    );

    const signature = await crypto.subtle.sign(
        'RSASSA-PKCS1-v1_5',
        key,
        new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`)
    );

    return `${encodedHeader}.${encodedPayload}.${btoa(String.fromCharCode(...new Uint8Array(signature)))}`;
}

function str2ab(str: string): ArrayBuffer {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

export async function runReportByPageViews() {
    try {
        const accessToken = await getAccessToken();
        const response = await fetch(`https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${accessToken}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                dateRanges: [
                    {
                        startDate: '2024-11-11',
                        endDate: 'today',
                    },
                ],
                dimensions: [
                    {
                        name: 'pagePath',
                    },
                ],
                metrics: [
                    {
                        name: 'screenPageViews',
                    },
                ],
                orderBys: [
                    {
                        desc: true,
                        metric: { metricName: 'screenPageViews' },
                    },
                ],
                dimensionFilter: {
                    notExpression: {
                        filter: {
                            fieldName: 'pagePath',
                            inListFilter: {
                                values: ['/blog', '/', '/about', '/contact'],
                            },
                        },
                    },
                },
                limit: 5,
            }),
        });
        const reportData = await response.json();

        if (reportData.rows) {
            return reportData.rows.map((row: { dimensionValues: { value: string }[]; metricValues: { value: string }[] }) => ({
                path: row.dimensionValues?.[0]?.value || '',
                screenPageViews: row.metricValues?.[0]?.value || '',
            }));
        } else {
            return [];
        }
    } catch (error) {
        console.error('Error running report by page views:', error);
        throw new Error('Failed to fetch report data by page views');
    }
}

次に Next.js のAPIルートを定義するファイル/api/report/route.tsを作成します。
microCMS のすべての記事を取得してきています。

import { NextResponse } from 'next/server';
import { runReportByPageViews } from '@/app/libs/runReport';
import { getAllPosts } from '@/app/libs/client';

export async function GET() {
    try {
        const reportDataByPageViews = await runReportByPageViews();
        const blogPosts = await getAllPosts();
        return NextResponse.json({ reportDataByPageViews, blogPosts });
    } catch (error) {
        console.error('Error fetching report data:', error);
        return NextResponse.json({ error: 'Failed to fetch report data' }, { status: 500 });
    }
}

最後にトップページで出力します。
先ほど作成したapi/reportを呼び出して画面で表示して完成です。

"use client";

import { useEffect, useState } from 'react';
import styles from './page.module.css';

interface ReportDataByPageViews {
  path: string;
  screenPageViews: string;
  title?: string;
}

interface BlogPost {
  id: string;
  title: string;
}

export const runtime = 'edge';

export default function Home() {
  const [reportDataByPageViews, setReportDataByPageViews] = useState<ReportDataByPageViews[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const items = document.querySelectorAll(`.${styles.mvv_item}`);
    items.forEach((item, index) => {
      setTimeout(() => {
        item.classList.add(styles.visible);
      }, index * 700);
    });

    async function fetchData() {
      try {
        const response = await fetch('/api/report');
        const data = await response.json();
        if (data.reportDataByPageViews && Array.isArray(data.reportDataByPageViews)) {
          const enrichedData = data.reportDataByPageViews.map((row: ReportDataByPageViews) => {
            const matchingPost = data.blogPosts.find((post: BlogPost) => row.path.includes(post.id));
            return {
              ...row,
              title: matchingPost ? matchingPost.title : 'No title'
            };
          });

          setReportDataByPageViews(enrichedData);
        } else {
          setError('Invalid data format for page views report');
        }
      } catch {
        setError('Failed to fetch report data');
      }
    }
    fetchData();
  }, []);

  return (
    <>
      <div className={styles.hero}></div>

      <section className={styles.reportSection}>
        <h2 className={styles.reportTitle}>人気のBLOGページ</h2>
        {error ? (
          <p className={styles.error}>{error}</p>
        ) : (
          <ul className={styles.reportList}>
            {reportDataByPageViews
              .filter(row => row.title !== 'No title')
              .map((row, index) => (
                <li key={index} className={styles.reportItem}>
                  <a href={row.path}>{row.title}</a> - {row.screenPageViews} views
                </li>
              ))}
          </ul>
        )}
      </section>

          </>
  );
}

※今回のランクキングデータは、テスト用データを使用しています。
画像で説明

Vercelへのデプロイ

以下のURLから Vercel にデプロイし、完成となります。

画像で説明

最後に

今回の記事では、Google Analytics Data API を活用してブログの視聴回数ランキングを表示する方法をご紹介しました。
Google Analytics は他にも様々な機能がありますので、ぜひ試してみてください。

1

Discussion