前提条件
- Next.js v14.0.2
- App Router
- Route Handlers
- Google Analytics 4
- microCMS
- microcms-js-sdk v2.7.0
- Vercel(Cron Jobsで定期実行する)
記事のコードは、上のバージョンで動かしたときのものです。新しいバージョンでは書き方が変わっている場合があるので、使っているバージョンのドキュメントもあわせて確認してください。
目的
Next.jsで構築し、ヘッドレスCMSにmicroCMSを採用しているサイトに人気記事を表示します。
その際に、GA4のデータを使用してmicroCMSの記事データを定期的に更新し、そのデータを元に実装します。
Google Cloudで鍵を取得する
サービスアカウントを作成する
- Google Cloudコンソール(新しいタブで開く)にログインし、「API とサービス」を開きます。
- 検索フォームにAnalyticsと入力するとGoogle Analytics Data APIがあるのでクリックします。

一番下にある「Google Analytics Data API」を選択する Google Analytics Data API | Google for Developers(新しいタブで開く)
- 以下の画面で「有効にする」を押します。
「有効にする」を押す - メニューから[IAMと管理] > [サービスアカウント]を開き、「サービスアカウントを作成」を押します。

左のメニューの[IAMと管理]から[サービスアカウント]を開く サービスアカウントを作成する - 任意のサービスアカウント名を入力してから「完了」を押します。

サービスアカウント名を入力する
鍵を取得する
- サービスアカウントの[操作]から[鍵を管理]を押します。
操作メニューから「鍵を管理」を開く - 鍵のページで[鍵を追加] > [新しい鍵を作成]を押します。

[鍵を追加]>[新しい鍵を作成]を選ぶ - キーのタイプは[JSON]を選択し、[作成]を押すとJSONファイルがダウンロードされます。

キーのタイプにJSONを選んで「作成」を押す ダウンロードした鍵のファイルは、あとから再ダウンロードできません。なくした場合は、新しい鍵を作り直します。
また、この鍵があれば誰でもサービスアカウントとしてGA4のデータを読めてしまうので、流出しないように厳重に管理する必要があります。
組織(Google Workspaceなど)に属するGoogle Cloudでは、組織のポリシー(「サービス アカウント キー作成を無効にする」)で鍵の作成が禁止されていることがあります。2024年5月3日以降に作られた組織では、このポリシーが最初から有効です。鍵を作れないときは、組織の管理者に相談してください。
サービスアカウントにGA4の権限を付与する
- Google Analyticsを開き、左下の[管理]を押します。
- [アカウントのアクセス管理]を開きます。

[アカウントのアクセス管理]を開く - 青丸の+ボタンを押したあと、[ユーザーを追加]を押します。
[ユーザーを追加]を選ぶ - 先ほど作成したサービスアカウントのメールアドレスを入力し、「新規ユーザーにメールで通知する」のチェックを外します。「標準の役割」は「閲覧者」にチェックを入れて「追加」を押します。
サービスアカウントのメールアドレスを入力し、役割を「閲覧者」にする
GA4から取得したデータをmicroCMSに送信するAPIを作成する
- microCMSの記事のAPI(ここでは
posts)のスキーマに、表示回数を入れる数字フィールドを追加します。フィールドIDはscreenPageViewsにします。 - Google Analytics Data APIを使用してGoogle Analyticsにアクセスするために、Google Analytics Data: Node.js Client(新しいタブで開く)をインストールします。
npm install @google-analytics/data - Google Cloudから取得したJSONファイルの中身から
private_keyとclient_emailをメモしておきます。{ "type": "service_account", "project_id": "xxxxxxxxxx", "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "private_key": "-----BEGIN PRIVATE KEY-----\nxxxxxxxxxx\n-----END PRIVATE KEY-----\n", "client_email": "xxxxxxxxxx@xxxxxxxxxx.iam.gserviceaccount.com", "client_id": "xxxxxxxxxx", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxxxxxxxxx%40xxxxxxxxxx.iam.gserviceaccount.com", "universe_domain": "googleapis.com"} - GA4の[管理] > [プロパティの詳細]の右上にあるプロパティIDをメモしておきます。
プロパティIDを控える - 環境変数に以下のように設定します。Vercelで動かす場合は、Vercelのプロジェクトの環境変数にも同じ値を設定します。
GOOGLE_ANALYTICS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nxxxxxxxxxx\n-----END PRIVATE KEY-----\n"GOOGLE_ANALYTICS_CLIENT_EMAIL=xxxxxxxxxx@xxxxxxxxxx.iam.gserviceaccount.comGOOGLE_ANALYTICS_PROPERTY_ID=xxxxxxxxx - src/libs/ga/getPageViews.ts を作成し、以下のように記述します。
import { BetaAnalyticsDataClient } from '@google-analytics/data'const analyticsDataClient = new BetaAnalyticsDataClient({ credentials: { client_email: process.env.GOOGLE_ANALYTICS_CLIENT_EMAIL, private_key: process.env.GOOGLE_ANALYTICS_PRIVATE_KEY?.replace(/\\n/g, '\n'), },})export const getPageViews = async ( startDate: '7daysAgo' | '14daysAgo' | '30daysAgo', limit: number = 10000,) => { const [response] = await analyticsDataClient.runReport({ property: `properties/${process.env.GOOGLE_ANALYTICS_PROPERTY_ID}`, dateRanges: [ { startDate, endDate: 'today', }, ], dimensions: [ { name: 'pagePath', }, ], metrics: [ { name: 'screenPageViews', }, ], dimensionFilter: { filter: { fieldName: 'pagePath', stringFilter: { matchType: 'BEGINS_WITH', value: '/posts/', }, }, }, limit, }) return response.rows?.map((row) => { const path = row.dimensionValues !== null && row.dimensionValues !== undefined ? row.dimensionValues[0].value : null const postId = path ? path.split('/').pop() : null // pathからpostIdを抽出 return { path, postId, readCount: row.metricValues !== null && row.metricValues !== undefined ? Number(row.metricValues[0]?.value) : 0, } })} - src/libs/microcms.ts に以下のように記述し、記事のデータを更新できるようにupdatePostDataを作成します。
import { createClient } from "microcms-js-sdk";if (!process.env.MICROCMS_SERVICE_DOMAIN) { throw new Error("MICROCMS_SERVICE_DOMAIN is required");}if (!process.env.MICROCMS_API_KEY) { throw new Error("MICROCMS_API_KEY is required");}/** * MicroCMSのクライアントを初期化して生成します。 * サービスドメインとAPIキーは環境変数から取得します。 */export const client = createClient({ serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN || "", apiKey: process.env.MICROCMS_API_KEY || "",});// 失敗したときは呼び出し側(route.ts)で記録するので、ここではエラーをそのまま投げるexport const updatePostData = async (contentId: string, editData: Record<string, unknown>) => { await client.update({ endpoint: "posts", contentId, content: { ...editData, }, });}; - src/app/api/ga/fetchPageViews/route.ts を作成し、microCMSにデータを送信するためのAPIを作成します。
import { getPageViews } from '@/libs/ga/getPageViews'import { updatePostData } from '@/libs/microcms'import { NextRequest } from 'next/server'export async function GET(request: NextRequest) { // Vercel Cron Jobsからの呼び出しだけを受け付ける const cronSecret = process.env.CRON_SECRET if (!cronSecret || request.headers.get('authorization') !== `Bearer ${cronSecret}`) { return new Response('Unauthorized', { status: 401 }) } try { const pageViewsPosts = await getPageViews('30daysAgo') if (!pageViewsPosts || pageViewsPosts.length === 0) { return new Response('Posts Not Found', { status: 404 }) } const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) for (const post of pageViewsPosts) { if (post.postId) { try { await updatePostData(post.postId, { screenPageViews: post.readCount || 0, }) console.log( `${post.postId} の screenPageViews を ${post.readCount || 0} に更新しました。`, ) } catch (error) { console.error(`${post.postId} の更新に失敗しました。`, error) } await delay(500) } } return new Response('Success', { status: 200 }) } catch (error: any) { return new Response(error?.message, { status: 400 }) }}microCMSのWRITE API(POST・PUT・PATCH・DELETE)は、1サービスあたり1秒間に5回までしか送信できないため、delay関数を作成してリクエストの間隔を調整しています。
制限事項/注意事項|microCMSドキュメント(新しいタブで開く)
VercelのCron JobsはGETリクエストしか送信できないため、GETリクエストにしています。誰でもこのURLを開けば更新が走ってしまうので、Cron Jobsが送ってくる
Authorizationヘッダーを確かめています(設定は次の章で行います)。 - PATCHリクエストを送信できるように、microCMSのAPIキーの権限で、
postsのPATCHにチェックを入れてください。デフォルト権限でPATCHにもチェックを入れる Next.jsで作成したAPIはGETリクエストですが、そこからmicroCMSのAPIにはPATCHリクエストを送信しています。
APIでコンテンツを更新すると、microCMSの更新日時(updatedAt・revisedAt)も変わり、Webhookを設定していれば記事ごとに送られます。サイトに記事の更新日を表示している場合、この仕組みを動かすと、表示回数が変わった記事がすべて「更新された」ように見えてしまいます。更新日は、別に用意した日付のフィールドから表示するのがおすすめです。
Vercel Cron Jobsで定期的に実行する
- Vercelのプロジェクトの環境変数に
CRON_SECRETを追加します。値は16文字以上のランダムな文字列にします。Vercelは、Cron Jobsを実行するときに、この値をAuthorization: Bearer <CRON_SECRET>として送ってきます。 - プロジェクトのルートに vercel.json を作成し、実行するパスとスケジュールを書きます。スケジュールはUTCで指定するので、以下は毎日午前3時(日本時間)に実行する設定です。
{ "crons": [ { "path": "/api/ga/fetchPageViews", "schedule": "0 18 * * *" } ]} - デプロイすると、Vercelのプロジェクトの[Settings] > [Cron Jobs]に登録されます。実行の結果は、Vercelのログで確かめられます。
Hobbyプランでは、Cron Jobsは1日1回までです。また、実行される時刻は、指定した時刻から最大59分遅れることがあります。
Managing Cron Jobs | Vercel(新しいタブで開く)
ページに出力する
今回はコンポーネントとして使いまわせるようにしたいので、非同期のServer Componentとしてデータを取得し、Suspenseで読み込み中の表示を出します。
- src/libs/microcms.ts に以下の記述を追加します。
MicroCMSQueriesは、ファイルの先頭のimportに追加してください。import { createClient, type MicroCMSQueries } from "microcms-js-sdk";/** * MicroCMSからすべての投稿を取得します。 * * @param queries - MicroCMSのクエリオプション * @returns - 投稿のリスト */export const getAllPosts = async (queries?: MicroCMSQueries) => { const listData = await client.getList<any>({ customRequestInit: { next: { revalidate: 60, }, }, endpoint: 'posts', queries, }) return listData} - src/components/PopularPosts.tsx を作成し、以下のように記述します。
import { getAllPosts } from "@/libs/microcms";import Link from "next/link";export const PopularPosts = async () => { const { contents } = await getAllPosts({ orders: "-screenPageViews", limit: 3, }); return ( <ol> {contents.map((post) => ( <li key={post.id}> <Link href={`/posts/${post.id}`}>{post.title}</Link> </li> ))} </ol> );}; - src/app/page.tsx でPopularPostsコンポーネントを呼び出します。このとき、Suspenseでラップしてください。
import { PopularPosts } from '@/components/PopularPosts'import { Suspense } from 'react'export default function Home() { return ( <div> <Suspense fallback={<p>Loading...</p>}> <PopularPosts /> </Suspense> </div> )}これでページに人気記事のランキングを出せるようになりました。
参考・引用
- Google Analytics 4 APIで人気記事を表示する【Next.js + TypeScript】 | フナイログ(新しいタブで開く)
- Google Cloud セキュリティ ベースラインの制約 | Resource Manager | Google Cloud Documentation(新しいタブで開く)
この記事は役に立ちましたか?