静的サイトを運用していると、必ずぶつかる壁があります。「動的なコンテンツをどう扱うか?」という問題です。特に「人気記事ランキング」のような、閲覧数に基づいたデータは、放っておくとすぐに古くなってしまいます。
「クライアントサイドでJSを動かしてAPIを叩けばいいじゃないか」という声も聞こえてきそうですが、表示速度やプライバシー、APIのクォータ(制限)を考えると、できればビルド時に解決したいところ。今回は、Astroで構築されたこの静的サイトが、GA4のデータをどうやってビルドプロセスへ取り込んだのか、を共有します。
アーキテクチャ:なぜ「ビルド時」なのか?
結論から言うと、このブログでは ビルド時に GA4 API を叩いて、結果を JSON として保存する という方式を採用しています。
クライアントサイドでの取得を避けた理由はシンプルです。
- パフォーマンス: ユーザーのブラウザで余計なJSを動かしたくない。
- セキュリティ: クレデンシャル周りを考えると不可能。
- 堅牢性: GA4 APIには割と厳しいクォータ制限があります。アクセスが急増したときにAPI制限でランキングが消えるのは避けたい。
- プライバシー: 閲覧者のブラウザから直接GoogleのAPIを叩かせるのは、計測以上のフットプリントを残す可能性がある。
ビルド時にデータを確定させてしまえば、ユーザーにはただの静的なHTMLとして届きます。爆速です。
実装の肝:GA4 API からデータを引っこ抜く
データ取得には @google-analytics/data を使用しています。人気記事をフェッチしてくるスクリプトを作成し、ビルドの直前に実行します。
以下は単純にPV順で持ってくる例です。滞在時間なども考慮して指標を作成するとさらに質の良いランキングが作れそうです。
// scripts/fetch-popular-posts.ts (抜粋)const [response] = await client.runReport({ property: `properties/${propertyId}`, dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }], dimensions: [{ name: 'pagePath' }, { name: 'pageTitle' }], metrics: [{ name: 'screenPageViews' }], orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }], limit: 50,});直近30日間のデータを取得し、上位50件ほどを引っ張ってきます。
データの「整合性」をどう保つか
ここが面倒なところです。GA4から返ってくるデータは、あくまで「過去の記録」です。
- 記事を削除したけれど、GA4の記録には残っている。
- 記事のスラッグ(URL)を変更した。
- タイトルを修正した。
そのままGA4のデータを表示すると、リンク切れ(404)が発生したり、古いタイトルが表示されたりします。これを防ぐために、取得したデータを 現在のコンテンツ(Markdownファイル)と照合 しています。
export async function getValidatedPopularPosts(lang: Lang, limit = 5) { const rawData = // ... GA4から取得したJSON const livePosts = await getPostsByLang(lang); // 現在のMarkdown全件
const titleBySlug = new Map( livePosts.map((p) => [getSlugFromId(p.id), p.data.title]) );
// GA4のデータにあるスラッグが、現在のMarkdownに存在するかチェック return rawData.posts .filter(p => titleBySlug.has(p.slug)) .map(p => ({ ...p, title: titleBySlug.get(p.slug) // タイトルを最新のMarkdownから上書き })) .slice(0, limit);}とりあえずの無理やりな突き合わせ処理ですが、この処理のおかげで記事を消したり直したりしても、存在する記事を持ってくることができます。
CI:ビルドを落とさない工夫
GitHub ActionsなどのCI環境でビルドする場合、GA4のAPIキー(Secrets)が必要です。しかし、プルリクエスト(PR)のビルドなど、Secretsが渡されない場面やAPIキーが使えなくなった、GA4が落ちているといったリスクもあります。
ここでAPIが叩けないからといってビルドをエラーにしてしまうと、開発体験が最悪になります。
そこで、以下のようなフォールバック処理を入れています。
- APIキーがない、またはAPI呼び出しが失敗した。
- 既存の
popular-posts.jsonがあれば、それをそのまま使う。 - 既存のファイルもなければ、空のデータでファイルを作る(ビルドを通すため)。
function fallback(reason: string): void { if (existsSync(OUTPUT_PATH)) { console.warn(`[fetch-popular-posts] ${reason} — 既存のスナップショットを維持します。`); return; } // ... 空のJSONを作成}これで、APIが叩けない場合のビルドでは以前のデータを使い回すという運用が可能になりました。
まとめ:動的な体験を静的に閉じ込める
「静的サイトだから動的な機能は諦める」あるいは「無理やりクライアントサイドで解決する」の二択ではありません。ビルドプロセスという「中間の時間」をうまく使うことで、静的サイトのメリットを活かしたまま、リッチな機能を実装できます。
実装には少し手間がかかりますが、一度組んでしまえばこれほど楽なものはありません。何より、自分のサイトが裏側でGA4と通信して、勝手にランキングを整理してくれる様子を眺めるのは、エンジニアとして純粋に楽しいものです。
みなさんの静的サイトにも、ぜひ「ビルド時ランキング」を導入してみてはいかがでしょうか。






