Gatsby×GA4で人気記事ランキング実装!PV数API連携から表示まで徹底解説

この記事を作成するにあたり下記の記事を大いに参考にさせていただきました。
誠にありがとうございます。
前半部分は上記の記事とほぼ同じ内容を解説しているので読み飛ばしていただいても問題ありません。
また、Gatsby.jsの記述に関してはおよそ初診の域を出ないので無駄な記述があったり、もっとこうした方が良いんじゃない?という部分があるかとは思いますが、なにとぞご容赦いただければと思います。
PV数データの扱いが課題に
WordPressでWebサイトを運用している場合、「WordPress Popular Posts」などのプラグインを使えば誰でも簡単に『人気記事ランキング』のようなウィジェットを取り入れることができるかと思います。
しかし、これまでWordPressで運用していたサイトを、機能も見た目もそのままにJamStack製のサイトに移行したいとなった場合にこの『人気記事ランキング』のような機能は個人的にはかなりの鬼門でした。
例えば、これまで運用していたサイトのPV(ページビュー)数のデータは当然WordPressのデータベースに保存されています。
しかし、JamStackに移行した場合、そのPV数データをそのまま取得しても意味がありません。
なぜならWebサイトの表示を担うのはWordPress側ではなくあくまでもGatsby.jsやNext.jsなどのJamStackで作った側なのでWordPress側のPV数データが更新されることは無いからです。
つまり、JamStackサイト側でのPV数をどこかに保存できるしくみが必要です。
JamStack側はあくまでフロントの表示部分を担うので独自のデータベースを持たないのが通常の構成かと思います。
そこで登場していただくのが皆さんご存じの「GoogleAnalytics」です。
GoogleAnalyticsを使用すれば、サイトのPV数をカウントしてGoogleAnalytics(つまり外部に)にデータを保持しておくことができます。
また、「Google Analytics Data API」を使用することでGoogleAnalytics上のデータに外部プログラムからアクセスすることができるようになるので、JamStack側でAPIルートにフェッチしてデータを取得し加工するという流れで人気記事ランキングを作成していきます。
上記の流れをまとめるとざっとこんな感じになります。
- Google Analytics Data APIを使用できる状態にする
- Gatsby側でGoogle Analytics Data APIを使用してGA4のPV数データを取得する
- 取得したPV数データをGatsbyノードとして保存して各ページにPropsとして渡す。
今回の構成
ちなみに話が前後しますが今回のWebサイトの構成は下記のようになっています。
今回はGatsby.jsを使用する方法ですので、GoogleAnalyticsのWebサイトへの設置方法は通常のbodyタグの中に<script>タグを埋め込む方法とは少し異なっており、Gatsbyの「gatsby-plugin-google-tagmanager」というプラグインを使用します。
このGoogleAnalyticsの設置方法についての詳細は今回の趣旨とは少し外れるので割愛させていただきます。
個人的には下記の記事が分かりやすいかなと思いますので是非ご参考にしてください。
Google Analytics Data APIの設定
まずはGoogle Cloud Plattformにログインします。
私自身GoogleCloundの料金の仕組みがよく分かっていないのですが、今回利用するGoogle Analytics Data APIは無料で利用できます。
①プロジェクトの作成
「プロジェクトを作成または選択」をクリックして、「新しいプロジェクトを作成」をクリックします。
「プロジェクト名」の個所に分かりやすい名前を入力します。
場所に関しては、GoogleCloundの仕組みそのものの仕組みを解説する必要が出てくるのでここでは省きます。(私もよく分かっていない…)
今回はとりあえずテストなのでそのままでもOKです。
②Google Analytics Data APIを有効にする
プロジェクトを先ほど作成したものになっているか確認し、サイドバーの「APIとサービス」から「ライブラリ」を選択します。
検索バーに「analytics」と入力します。
「Google Analytics Data API」を選択します。
有効化します。
③サービスアカウントの作成
サービスアカウントとは現在ログインしているGoogleアカウントとは違い、「人間ではなく、システムやプログラムに対して与える特別なアカウント」になります。
例えば、
- 自動でデータを取得・更新したいとき
- 外部サービスと連携したいとき
など、「人が操作しなくても、自動で動く処理」 に使用するアカウントになります。
まず、サイドバーの「IAMと管理」から「サービスアカウント」を選択します。
プロジェクト名を確認して「サービスアカウントを作成」を選択します。
サービスアカウント名を入力します。
IDは自動で割り振られるので入力しなくてOKです。
アカウントの説明にかんしては任意で入力しても大丈夫ですが、今回はテストなので省略します。
入力し終えたら「完了」を選択します。
上記のようにサービスアカウントが1つ追加されました。
ここで作成されたメールアドレスは後程連携の際に使用するので、コピーしておくことをお勧めします。
④キーを取得する
次に連携で必要となる「キー」を取得します。
まずは、先ほど作ったアカウント名をクリックします。
「鍵」のタブに移動し、「キーを追加」を選択します。
※2025年3月現在Google側はキーを使用した連携ではなく「 Workload Identity 連携」というものを推奨しているようです。
「新しい鍵を作成」を選択します。
JSON形式を選択して、作成を選択します。
これで、連携に必要なものが揃いました。
GoogleAnalyticsで連携のための設定を行う
Google Analyticsへ移動します。
サイドバーから①歯車マークをクリックし、②「管理」を選択、③「アカウントのアクセス管理」の順に進んでいきます。
「+」マークをクリックし、ユーザーを追加を選択します。
先ほど「サービスアカウント」で作成したユーザーのメールアドレスを入力し、
役割は「閲覧者」あたりを選択しておきます。
そして「追加」をクリックしておけばOKです。
これでようやく事前準備が完了しました…
GatsbyでGA4のデータを取得する
下準備の方が長くなりましたが、ここから実際の構築作業の方に入っていきます。
まずは、gatsbyでGoogle Analyticsのデータを使用できるようにするためのライブラリをインストールします。
npm install @google-analytics/data
次に、「.env」ファイルに先ほどGoogle Plattformで自動的に設定されたメールアドレスと、キーを環境変数として登録します。
※このファイルは絶対にGithubのリポジトリ上にプッシュしないでください。
GATSBY_CLIENT_EMAIL="my-jamstack-account@my-jamstack-project.iam.gserviceaccount.com"
GATSBY_PRIVATE_KEY="××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××"
gatsby-node.jsにGA4のデータを取得するためのコードを記述していきます。
/************************************************************
* GoogleAnalyticsから記事の閲覧数を取得
***********************************************************/
// パッケージをインポート
const { BetaAnalyticsDataClient } = require("@google-analytics/data");
// プロパティIDを設定
const propertyId = "×××××××××";
const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: {
client_email: process.env.GATSBY_CLIENT_EMAIL,
private_key: process.env.GATSBY_PRIVATE_KEY.split(String.raw`\n`).join("\n"),
},
});
まずは、インストールしたパッケージをrequireでインポートします。
プロパティIDに関してはGoogle Analyticsの管理画面から取得してきてください。
そして、このコードの部分では.envファイルに設定したメールアドレスとキーを使用してインスタンスを生成しています。
const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: {
client_email: process.env.GATSBY_CLIENT_EMAIL,
private_key: process.env.GATSBY_PRIVATE_KEY.split(String.raw`\n`).join("\n"),
},
});
また、GATSBY_PRIVATE_KEYに含まれる\nという文字列は本来は改行の意味を持ちますが、.envファイルで使用するとそのまま「\n」という文字列になります。
そこで、「\n」という文字列で改行できるようにしているのが下記の部分になります。
private_key: process.env.GATSBY_PRIVATE_KEY.split(String.raw`\n`).join("\n")
String.raw
`\n` → これは「\n
」という文字列のこと。.split()
→ その「\n
という文字列」で分割する。.join("\n")
→ 分割したものを、本物の改行 \n に置き換えてつなげる。
例として示すと下記のようなイメージです。
const key = "-----BEGIN KEY-----\\nAAA\\nBBB\\n-----END KEY-----\\n";
console.log(key);
// -----BEGIN KEY-----\nAAA\nBBB\n-----END KEY-----\n ← 改行されてない
const formattedKey = key.split("\\n").join("\n");
console.log(formattedKey);
// -----BEGIN KEY-----
// AAA
// BBB
// -----END KEY----- ← 改行されてる
そして、次に実際にGA4からデータを取得していく作業です。
const [response] = await analyticsDataClient.runReport({
property: `properties/${propertyId}`,
dateRanges: [
{
startDate: "2022-08-01",
endDate: "today",
},
],
dimensions: [
{
name: "pagePath",
},
],
dimensionFilter: {
filter: {
fieldName: "pagePath",
stringFilter: {
matchType: "BEGINS_WITH",
value: "/web-tips/" /* ブログページに共通するパス */,
},
},
},
metrics: [
{
name: "screenPageViews",
},
],
orderBys: [
{
desc: true,
metric: {
metricName: "screenPageViews",
},
},
],
});
runReport()とはGA4のデータを取得するためのメインメソッドになります。
その中に各プロパティを設定してあげることで、任意のデータを取得できるようになる、というわけです。
各プロパティの詳細については下記のページに詳しく記載されています。
Method: properties.runReport | Google Analytics | Google for Developers
ここでは少しだけ解説していきたいと思います。
dateRanges: [
{
startDate: "2022-08-01",
endDate: "today",
},
],
startDate
: データ取得開始日endDate
: データ取得終了日
"today"
は特殊なキーワードで、「今日」を意味します。
→ "yesterday"
や "7daysAgo"
も使えます。
dimensions: [
{
name: "pagePath",
},
],
- 「軸」になるデータ。
- ここでは「ページパス(
pagePath
)」でデータを集計。
dimensionFilter: {
filter: {
fieldName: "pagePath",
stringFilter: {
matchType: "BEGINS_WITH",
value: "/web-tips/",
},
},
},
- フィルターを使って、「特定のページだけ」に絞る。
fieldName
: 対象となるディメンション → ここでは"pagePath"
stringFilter
: 文字列によるフィルターmatchType: "BEGINS_WITH"
→ 「指定文字列で始まる」ページだけ。value: "/web-tips/"
→"/web-tips/"
で始まるページを抽出。(今回は特定のカスタム投稿タイプのみランキング表示するため)
metrics: [
{
name: "screenPageViews",
},
],
- 「数値データ」として集計する項目。
- ここでは「ページビュー数(screenPageViews)」。
orderBys: [
{
desc: true,
metric: {
metricName: "screenPageViews",
},
},
],
desc: true
→ 降順(大きい順)。metric.metricName: "screenPageViews"
→ ページビュー数でソート。
これらをまとめると下記のようになります。
プロパティID | ${propertyId} |
期間 | 2022年8月1日~今日 |
ディメンション | ページパス(pagePath ) |
フィルター | "/web-tips/" で始まるページのみ |
メトリクス | ページビュー数(screenPageViews ) |
並び順 | ページビュー数 降順 |
これにてGoogle Analyticsからデータを取得する準備はOKです。
次は取得したデータを各ページにコンテキストとして送れるようにデータを加工していきます。
取得したGA4のデータを加工してコンテキストとして渡す
一度ここで取得したデータをターミナル上のログに出力してみます。
(gatsby-node.jsはサーバー側の処理なのでブラウザのログには出力されないことに注意!)
console.log(response);
すると下記のように出力されているはずです。
{
dimensionHeaders: [ { name: 'pagePath' } ],
metricHeaders: [ { name: 'screenPageViews', type: 'TYPE_INTEGER' } ],
rows: [
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] },
{ dimensionValues: [Array], metricValues: [Array] }
],
totals: [],
maximums: [],
minimums: [],
rowCount: 22,
metadata: {
dataLossFromOtherRow: false,
currencyCode: 'USD',
_currencyCode: 'currencyCode',
timeZone: 'Asia/Tokyo',
_timeZone: 'timeZone'
},
propertyQuota: null,
kind: 'analyticsData#runReport'
}
ですが、このままだと何が何だか分からないので、さらに中の方まで見ていきます。
response.rows.forEach((row) => {
console.log(row.dimensionValues[0].value, row.metricValues[0].value);
});
/web-tips/873/ 912
/web-tips/1216/ 454
/web-tips/1024/ 375
/web-tips/ 216
/web-tips/1129/ 198
/web-tips/828/ 181
/web-tips/1237/ 127
/web-tips/829/ 68
/web-tips/650/ 43
/web-tips/638/ 35
/web-tips/1105/ 26
/web-tips/781/ 23
/web-tips/642/ 21
/web-tips/128/ 10
/web-tips/1092/ 9
/web-tips/1075/ 8
/web-tips/865/ 8
/web-tips/140/ 7
/web-tips/page-2/ 3
/web-tips/754/ 1
/web-tips/853/ 1
/web-tips/873/null/ 1
これは左側に「ページパス」、右側に「ページビュー数」が出力されている状態です。
次は、このデータを加工していきます。
const formattedGaData = response.rows.map((row) => {
return {
value: row.dimensionValues[0].value,
oneValue: row.metricValues[0].value,
};
});
const rankingdata = formattedGaData
//「/」で区切った配列から空欄文字列を削除した配列
.map((item) => item.value.split("/").filter((str) => str !== ""))
//その配列に対して、長さが1以上のもののみ抽出(つまり'web-tips'は除外)
.filter((parts) => parts.length > 1)
//配列の最後の部分を取り出し、IDを数値化
.map((parts) => parseInt(parts.pop()));
const rankingPostIds = rankingdata.slice(0, 5);
まず下記のコードの部分について解説します。
formattedGaData.map((item) => item.value.split("/").filter((str) => str !== ""))
item.value
→"/web-tips/123"
みたいな文字列を取り出してる。.split("/")
→/
で分割 →["", "web-tips", "123"]
になる。.filter((str) => str !== "")
→ 空文字列""
を除去。["web-tips", "123"]
になる。
次に下記の部分です。
.filter((parts) => parts.length > 1)
- 配列の長さが 2以上 のものだけ残す。
- 記事ID付きパス(
/web-tips/123
)は →["web-tips", "123"]
→ 長さ2 → 残る "/web-tips"
だけのパスは →["web-tips"]
→ 長さ1 → 除外
最期にこのの部分です。
.map((parts) => parseInt(parts.pop()));
parts.pop()
→ 配列の最後の要素を取り出して削除する。- 例えば
["web-tips", "123"]
→"123"
が取り出され、parts
は["web-tips"]
になる。 parseInt()
→ 文字列を数値に変換。"123"
→123
結果として最終的な配列は下記のようになります。
[873, 1216, 1024, 1129, 828]
これが記事のIDをページビュー数順に並べた配列です。
これを下記のような形でページ生成する際のコンテキストとして渡してあげます。
blogResult.data.allWpWebTips.nodes.forEach((node) => {
createPage({
path: `/web-tips/${node.databaseId}`,
component: path.resolve(`./src/templates/blogpost-template.js`),
context: {
id: node.databaseId,
reportData: rankingPostIds,
},
});
});
※blogResultという変数についてはこの記事では初出ですが、これはgraphqlクエリなのでご自身で任意に設定してください。
ページコンポーネント側で出力する
それでは実際にページコンテキストとして渡されたデータを利用して、ランキング表示していきます。
import { graphql } from "gatsby";
export default function BlogPost({data, pageContext}){
//コンテキストから人気記事順の配列を取得
const rankingPostIds = pageContext.reportData;
const sortedData = data.allWpWebTips.nodes
.sort((a, b) => rankingPostIds.indexOf(a.databaseId) - rankingPostIds.indexOf(b.databaseId))
.filter((item) => rankingPostIds.includes(item.databaseId));
return (
<div>
<h1>記事ページ - {id}</h1>
//投稿記事本文は省略
<h2>人気記事ランキング</h2>
<ul>
{sortedData.map((item) => {
return (
<li key={item.databaseId}>
<Link to={`/web-tips/${item.databaseId}`}>
<GatsbyImage image={item.featuredImage.node.localFile.childImageSharp.gatsbyImageData} alt={item.featuredImage.node.altText} />
<h3>{item.title}</h3>
</Link>
</li>
);
})}
</ul>
</div>
);
};
export const query = graphql`
query($id: Int!) {
wpWebTips(databaseId: { eq: $id}){
//投稿記事本文にどのようなデータを持ってくるかは任意で設定してください
}
allWpWebTips(sort: { date: DESC }) {
nodes {
databaseId
title
}
}
}
`;
ここではまず、graphqlにて本文記事表示用にpageContextの$idで取得できる「wpWebTips」とランキング表示用にいったんすべての投稿を取得する「allWpWebTips」をクエリとして登録しています。
それが下記の部分です。
export const query = graphql`
query($id: Int!) {
wpWebTips(databaseId: { eq: $id}){
//投稿記事本文にどのようなデータを持ってくるかは任意で設定してください
}
allWpWebTips(sort: { date: DESC }) {
nodes {
databaseId
title
}
}
}
`;
そして、下記の部分でコンテキストから取得したreportDataと照らし合わせてsortとfilterを行っています。
const sortedData = data.allWpWebTips.nodes
.sort((a, b) => rankingPostIds.indexOf(a.databaseId) - rankingPostIds.indexOf(b.databaseId))
.filter((item) => rankingPostIds.includes(item.databaseId));
あとはsortedDataを使用してJSX記法にて実際のフロントの表示を構築していきますが、この部分はご自身で任意にデザインを考えて構築してください。
<h2>人気記事ランキング</h2>
<ul>
{sortedData.map((item) => {
return (
<li key={item.databaseId}>
<Link to={`/web-tips/${item.databaseId}`}>
<GatsbyImage image={item.featuredImage.node.localFile.childImageSharp.gatsbyImageData} alt={item.featuredImage.node.altText} />
<h3>{item.title}</h3>
</Link>
</li>
);
})}
</ul>
以上で完成です。
Vercelにデプロイする
最後にこの実装したコードをVercel等でデプロイしたい場合ですが、.envファイルをそのままGithub上にアップロードするのはセキュリティ的にアウトなので、「settings」の「Environment Variables」から環境変数として登録しておくようにしましょう。