Lchiki-Lab
arrow_backBack to Archive

DBを使わずに「累計◯◯枚つくられました」をサイトに表示する(Next.js + Cloudflare Pages + GA4)

calendar_today2026.08.10folderTechnology
Cookie Cat

「累計〇〇枚作成!」みたいな数字があると、なんだかお店の看板っぽくてカッコいいよね!今回はDBを使わずに完全無料で動かすちょっと面白い構成を紹介するよ!

1. やりたかった​こと

自作のLINEスタンプ作成ツール「いじスタ」のトップページに、自作のアクセスカウンター(累計作成枚数カウンター)を導入したいと考えました。 いわゆるソーシャルプルーフ(社会的証明)として、「これまでにみんなが自作した実績」や安心感を伝えるのが目的です。

LINEスタンプ画像一括作成ツール いじスタ

自作のLINEスタンプ作成ツール「いじスタ」

前提条件

今回の構築にあたって設定した条件は以下の3点です。

今回の前提条件
  • Cloudflare Pages の無料枠で運用したい(運用コストゼロ)
  • 運用の手間はできるだけ増やしたくない(更新の自動化)
  • 実行時に Node.js サーバーがいないoutput: "export" を選択)

な​ぜ静的​書き出しなのか​(Node.js サーバーとの​違い)

Cloudflare Pages の無料枠を活用するために静的書き出し(output: "export")を選択していますが、通常の Next.js(Node.js サーバーあり)と比較すると以下のような機能制限があります。

機能通常の Next.js (Node.js サーバーあり)静的書き出し (output: 'export')
getServerSideProps◯ アクセス毎に動的データ取得✕ 使用不可
Server Actions◯ フォーム送信等をサーバー処理✕ 使用不可
headers / redirectsnext.config.js で動的設定✕ ホスティング側の設定で対応
画像最適化 (<Image/>)◯ サーバーで自動リサイズ△ 外部ローダーが必要
API Routes (app/api/...)◯ Next.js 内で API を作成✕ 使用不可

実行時にサーバーで API Routes や getServerSideProps を使って動的にデータを取得・処理する一般的な手が使えないため、今回の構成を検討する必要がありました。


2. 検討した​アプローチと​その​問題点

案A: クライアントから​ GA4 Data API を​叩く

無理。 GA4 Data API はサービスアカウント認証が必要であり、ブラウザから直接呼ぶことはできません。呼べたとしても認証情報がユーザーに丸見えになってしまいます。

案B: 自前で​アクセスカウンターを​持つ​(Cloudflare KV / D1)

ダウンロード時にエンドポイントを叩いてインクリメントする、Cloudflare D1やKVを使う王道のアプローチです。 正確な実数が取れるのが最大の利点ですが、今回の用途に対しては開発・運用のコストが重いのが問題でした。

自前カウンター(案B)で直面する開発・運用課題
  • DBやKVを用意して運用する必要がある
  • KVの競合リスク: read → 加算 → write の間に競合して取りこぼす(原子的な加算ができない)
  • D1(SQL)のアトミック性: UPDATE ... SET n = n + 1 で原子的に加算できるが、結局DB管理は必要
  • 防犯・スパム対策のコスト: 加算エンドポイントが公開されるため、誰でも好きなだけ数字を盛れてしまう。レート制限やスパム対策の実装が不可欠

案C: 静的な​数字を​手で​更新

一番簡単。ただし更新を忘れるためボツ。

Cloudflare KVとGA4定期ビルドの比較

Cloudflare KV(+エッジ関数)と GA4(+定期ビルド)の比較概要


Cloudflare KV と​ GA4​(定期ビルド)の​比較

王道である「Cloudflare KV(+エッジ関数)」と「GA4(+定期ビルド)」を比較すると、「リアルタイム性」を重視するか、「運用の楽さ・データの正確性」を重視するかで適性が大きく分かれます。

比較軸Cloudflare KV(+エッジ関数)GA4(+定期ビルド)
リアルタイム性◎ 即時反映(自アクセスが即カウント)△ タイムラグあり(ビルド周期依存)
データの正確性△ ガードが必要(Botアクセスも拾う)◎ 高い(GoogleのBot/スパム自動除外)
開発・保守の手間△ API/KV構築・防犯対策が必要◎ 完全にお任せ(既存タグ+ビルドフェッチ)
表示速度◯ 高速(クライアントから fetch 描画)◎ 爆速(静的ファイル埋め込み配信)
機能の拡張性◯ 自由度が高い(特定アクション集計)◎ 分析機能が強力(流入経路や分析蓄積)

今回の目的は「これまでに◯◯枚作られた」という社会的な証明(ソーシャルプルーフ)の表示であり、リロード直後に数字が増えるようなリアルタイム性は不要です。

構築・運用のコードを書かずに済み、クローラー等のスパム除外もGoogle任せにできるため、結果として GA4(+定期ビルド)の方が圧倒的に構築・運用稼働が少なく済むと判断しました。


3. 設計​思想:​書き込みを​週1回に​制限する

案Bが重くなる原因を分解すると、全部同時書き込みから来ている。 競合対策も、耐久性も、不正加算対策も、「複数のユーザーが任意のタイミングで書き込む」前提があるから必要になります。

じゃあ、書き込むのを週に1回・1プロセスだけにしたらどうなるか。

同時書き込みを排除した効果
  • 競合が起きない → 排他制御が要らない
  • 書き込み回数が週1 → 性能もクォータも考えなくていい
  • 外部から書き込ませない → 不正加算が構造的に不可能

ここまで削ると、残るのは「数字を1つ保存する場所」だけになります。それはもうファイルでいい。

さらに、表示を「◯◯枚以上」「先週時点」にしておけば、更新の遅れも仕様として自然に飲み込めます。


4. 全体​構成と​実装手順

アーキテクチャ構成

構成アーキテクチャフロー

GitHub Actions × Cloudflare Pages × GA4 の構成フロー図

GitHub Actions (週1回 cron)
  → Cloudflare の Deploy Hook を叩くだけ(curl 1発)
       ↓
Cloudflare Pages がビルド開始
  → ビルド時に GA4 Data API を叩く
  → public/stats.json を生成(gitignore対象)
  → next build
       ↓
デプロイ完了

ポイントは GitHub Actionsは数字を取ってこないこと。「ビルドしろ」と叩くだけの役に徹します。 数字の取得はCloudflareのビルド中に行うので、リポジトリに数値が一切コミットされません


実装手順

Step 1: サービスアカウントを​発行する

  1. Google Cloud でサービスアカウントを作成し、JSONキー(.json)を発行・保存
  2. GA4の 管理 → プロパティのアクセス管理 で、サービスアカウントのメールアドレスを**「閲覧者」**権限で追加
  3. GCPプロジェクトで Google Analytics Data API を有効化
セキュリティ上の注意点

JSONキーは Cloudflare Pages の環境変数(Secretとして値を非表示にできる機能)に保存します。リポジトリには絶対にコミットしないでください。

※ 画面付きの詳しいサービスアカウント作成手順や、GA4の権限追加でエラーが出た場合のトラブルシューティングについては、以下の記事で詳しく解説しています。

Step 2: 集計スクリプトを​作成する​ (scripts/fetch-stats.mjs)

認証とデータ取得について、公式クライアント @google-analytics/data(gRPC依存で重い)は使わず、認証部分のみ google-auth-library を使い、取得は軽量な REST fetch で叩く構成を採用しました。

  1. google-auth-libraryJWT クラスに認証情報を渡し、アクセス・トークンを取得
  2. GA4 Data API の runReport エンドポイントへ REST リクエストを送信し集計値を取得
  3. 取得した数値を JSON 形式に整形し、public/stats.json へ出力保存
import { JWT } from "google-auth-library";

// 認証トークンの取得だけ google-auth-library に任せる
const auth = new JWT({
  email: sa.client_email,
  key: sa.private_key,
  scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
});
const { token: accessToken } = await auth.getAccessToken();

アクセストークンを得たら、runReport REST API を fetch して public/stats.json に書き出します。

{
  "totalStamps": 52431,
  "totalUsers": 8210,
  "updatedAt": "2026-07-31",
  "source": "ga4"
}

Step 3: ビルドプロセスへ​組み込む

  1. package.jsonscripts.build 前段に、集計スクリプト(node scripts/fetch-stats.mjs)を実行するよう追記
  2. 生成された /public/stats.json.gitignore に指定し、Git管理から除外
{
  "scripts": {
    "build": "node scripts/fetch-stats.mjs && next build"
  }
}

Step 4: フロントエンドで​表示する​(静的​埋め込み)

クライアント側での useEffect + fetch によるチラつきを避けるため、get-site-stats.ts 等でサーバーコンポーネントがビルド時に直接ファイルを読み込んで静的に埋め込みます。

  1. ビルド時に public/stats.jsonfs.readFileSync で同期読み込み
  2. 設定した最小表示条件(例: 1,000枚以上)を満たしている場合のみ自作アクセスカウンターの表示用コンポーネントへ渡して描画
// lib/get-site-stats.ts
import fs from "node:fs";
import path from "node:path";

export function getSiteStats() {
  try {
    const filePath = path.join(process.cwd(), "public/stats.json");
    const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
    return data.totalStamps >= MIN_DISPLAY ? data : null;
  } catch {
    return null;
  }
}

Step 5: GitHub Actions で​定期ビルドを​実行する

  1. Cloudflare Pages の管理画面でビルド再実行用の Deploy Hook URL を生成
  2. GitHub Actions から cron スケジュール設定(例: 毎週月曜)で Deploy Hook を POST リクエスト呼び出し
on:
  schedule:
    - cron: "0 20 * * 0" # 毎週月曜 JST 5:00
  workflow_dispatch:

jobs:
  trigger-rebuild:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger Cloudflare Pages deploy hook
        env:
          DEPLOY_HOOK_URL: ${{ secrets.CF_PAGES_DEPLOY_HOOK }}
        run: |
          curl --fail --silent --show-error -X POST "$DEPLOY_HOOK_URL"

5. ハマりどころと​運用の​ポイント

一番の落とし穴: GA4のカスタム指標は事前登録が必須(遡及しない)

イベントに image_count: 8 のような数値パラメータを送っていても、GA4はカスタム指標として登録するまで合計してくれない。しかも登録は遡及せず、登録した日以降のデータにしか適用されない

管理 → データの表示 → カスタム定義 → カスタム指標 で、あらかじめ設定しておく必要があります。

項目
指標名Image Count
範囲イベント
イベントパラメータimage_count
測定単位標準

その​他の​留意点・ノウハウ

スムーズな運用のためのTips
  • データ保持期間の設定は Data API には影響しない: デフォルト2ヶ月を14ヶ月に上げていても、Data API(runReport)の集計値は影響を受けず過去データも取得可能です。
  • 数字を合計するイベントを1本に絞る: 複数イベントでパラメータを送信していると重複カウントされるため、集計対象イベントは1つに厳選します。
  • エラーでビルドを落とさない (exit 0): GA4の一時的な通信エラー等でデプロイ全体が落ちないよう、スクリプトの catch ブロックでは process.exit(0) で正常終了させます。
  • 数字は切り捨てて「以上」と表記する: トラッキング拒否等で 2〜3 割は取りこぼすため、1,000 枚単位で切り捨てて「52,000枚以上」と表示するのが実用的です。
  • 累計減少の保険(ベースラインの環境変数化): 全期間を集計し直す仕様の保険として、集計開始日とベースライン枚数を環境変数化し、GitHub Actions で月1回自動チェックを行わせています。

リソースと​コスト

リソース使用量無料枠判定
Cloudflare Pages ビルド月4回(+ 通常デプロイ)500回/月◎ 完全無料
GitHub Actions月4回 × 数秒パブリック無制限◎ 完全無料
GA4 Data API月4リクエスト十分な枠あり◎ 完全無料
データベースなし◎ ¥0

向いている​ケース / 向いていない​ケース

向いているケース / 向いていないケース

本方式が向いているケース・向いていないケースの比較整理

向いているケース

  • 静的サイトに「累計◯◯」「登録者◯◯人」のような自作アクセスカウンター(単調増加の数字)を出したい
  • 数字が数日古くても困らない
  • すでに GA4 など何らかの計測が入っている
  • DBやKVを増やしたくない / スパム対策コードを書きたくない

向いていないケース

  • リアルタイムに増えるカウンターを見せたい(演出・キリ番重視)
  • 計測の取りこぼしが許されず、厳密な実数が要る
  • ユーザーごとに違う数字を出す必要がある

6. まとめ

「カウンターを作る = DBを用意する」と反射的に考えていたけれど、更新頻度を落とすと必要な道具がごっそり減る、というのが今回の学び。

同時書き込みがなくなれば、排他制御も耐久性も不正対策も要らなくなって、残るのはファイル1つ。 週1回の更新で足りる要件だったのに、リアルタイム前提の設計を持ち出すところでした。

なお、この自作アクセスカウンター構成は乗り換えも簡単で、stats.json を返すエンドポイントを差し替えるだけで D1 などの実カウンターに移行できます。表示側のコードは1行も変わりません。

Cookie Cat

数字が増えていくのを見るのってワクワクするよね!ちなみにクッキー猫の今日食べたおやつ枚数カウンターも、毎日順調に更新中だよ!

関連記事