Experiment record

Cloudflare Pages + D1によるアクセスカウンター実装とBinding障害の復旧

AI Experiment Logのフッターへ公開アクセスカウンターを追加し、D1 BindingがRuntimeへ渡らず503になった障害をFunctionsログで特定し、Binding再設定と再デプロイで復旧した記録。

結論

AI Experiment Logのフッターに、サイト全体の公開アクセス数を表示するカウンターを追加した。

最終構成は、ブラウザ側JavaScriptから /api/counter を呼び、Cloudflare Pages FunctionがD1の単一カウンターを読み書きする方式である。日本語ページでは アクセス数、英語ページでは Site visits と表示する。

実装直後は /api/counter がHTTP 503と counter_unavailable を返した。最終的にCloudflare Pages Functionsの実行ログから、実行中のFunctionに COUNTER_DB D1 Bindingが存在しないことを特定した。D1 Bindingを再設定し、再デプロイした後、カウンターが正常に加算されることを確認した。

重要なのは、今回の直接原因はD1のSQLエラーではなく、SQL実行前のRuntime Binding不足だったことである。

1. 目的

Cloudflare Web Analyticsはすでに有効化されていたが、それとは別にサイト訪問者が見られる小さな公開カウンターを置くことにした。

目的はアクセス解析を置き換えることではない。公開カウンターは概算の見える数字、Cloudflare Web Analyticsは管理・解析用という役割分担である。

2. 最終アーキテクチャ

ブラウザ
/js/access-counter.js
GET または POST /api/counter
Cloudflare Pages Function
context.env.COUNTER_DB
Cloudflare D1

公開コードは次のファイルにある。

D1 Bindingの変数名は COUNTER_DB とした。

3. カウント方式

ブラウザ側では sessionStorageael-site-visit-counted-v1 を使う。

同じタブセッションの初回表示では POST /api/counter を送信してグローバルカウンターを1増やす。その後、同じタブセッションでは GET /api/counter で現在値を読むだけにする。

したがって、この数字はユニークユーザー数ではない。新しいタブや新しいセッションでは同じ人でも再加算され得る。公開表示用の概算アクセスカウンターとして扱う。

D1側はサイト全体の単一カウンターだけを保持する。カウンター実装自身は、IPアドレス、User-Agent、ページパス、deviceId、永続的なvisitor IDをD1へ保存しない。

4. 実装

Pages Functionでは、D1テーブルがなければ初回アクセス時に作成する。

site_counter
  counter_key   TEXT PRIMARY KEY
  counter_value INTEGER
  updated_at    TEXT

GET /api/counter は現在値を返す。POST /api/counter はUPSERTで counter_value + 1 を行い、その後の値を返す。

FunctionがD1 Bindingを取得できない場合やD1操作が失敗した場合、公開レスポンスは詳細を露出せず、HTTP 503と次のJSONを返す設計にした。

{"ok":false,"error":"counter_unavailable"}

一方、Functionログには実際の例外を console.error で残す。

5. 障害発生

アクセスカウンター実装は、SwitchBot記事の改訂と同じ作業でGitHubへ追加した。該当コミットは次である。

84744e803aa03f4345f14426320fb7799e8044ac
Update SwitchBot article and add public site counter

Cloudflare Pagesへのデプロイ自体は成功したが、実際に

/api/counter

へアクセスするとHTTP 503となった。

{"ok":false,"error":"counter_unavailable"}

サイトフッターも数字を取得できず、アクセス数: — のままだった。

ここで、ビルド成功とRuntime依存関係の正常性は別であることが明確になった。

6. 最初の切り分け

当初は次の可能性を検討した。

  1. D1 Binding設定後の再デプロイ順序が悪かった。
  2. POST /api/counter のUPSERTだけが失敗している。
  3. D1 SQLまたはテーブル作成が失敗している。
  4. Cloudflare Dashboard設定とリポジトリ内の wrangler.toml が競合している。

しかし、ブラウザで直接開く GET /api/counter も503だったため、「POSTだけの問題」は除外された。

決定的だったのはPages Functionsの実行ログである。

counter GET failed
Error: COUNTER_DB D1 binding is not configured

現在のFunctionは最初に context.env.COUNTER_DB を確認するため、このログによりD1 SQLを実行する前に止まっていることが分かった。

したがって、SQL構文やUPSERTは今回の503の直接原因ではなかった。

7. wrangler.toml の整理

リポジトリ直下には、もともと直接デプロイ用の参考設定として wrangler.toml が残っていた。

設定の責任範囲を明確にするため、Git接続のCloudflare PagesではDashboard設定を使う方針に統一し、次のコミットで wrangler.toml を削除した。

b3d3322a9496434b715a0f90c72819805b3822ae
Remove Wrangler config and use Cloudflare Pages dashboard settings

削除後のビルドログでは次を確認した。

No Wrangler configuration file found. Continuing.
Found Functions directory at /functions. Uploading.
Compiled Worker successfully

これによりWrangler設定ファイルの存在という曖昧さは解消した。

ただし、wrangler.toml を削除したことがD1 Binding消失の原因だったとは確認されていない。ここは因果関係を断定しない。

8. 直接原因の確定と復旧

その後Cloudflare Pagesの設定を確認すると、D1 Bindingが存在しない状態になっていた。

再度、D1 Bindingを次のように設定した。

Variable name: COUNTER_DB
D1 database: 公開カウンター用D1データベース

設定後にCloudflare Pagesを再デプロイした。

再デプロイ後、カウンターが正常にカウントされることを人間が確認した。

したがって、この障害について確認できた直接原因は次である。

実行中のPages Functionに context.env.COUNTER_DB が存在しなかった。

D1 Bindingがなぜ設定から消えていたかは、この記録では確定していない。

9. 有効だった診断手順

今回、特に有効だった順序は次だった。

  1. Webページの表示だけで判断せず、/api/counter を直接開く。
  2. HTTPステータスと公開JSONを確認する。
  3. Pages FunctionsのRuntimeログを確認する。
  4. context.env.COUNTER_DB の有無とSQL実行を分離して考える。
  5. Binding設定を確認する。
  6. 設定変更後に再デプロイする。
  7. APIが正常化してからフッター表示を確認する。

この順序にすると、UI、JavaScript、Function routing、Runtime Binding、D1 SQLを別レイヤーとして切り分けられる。

10. 再利用できる知見

10.1 「デプロイ成功」は外部Bindingの成功を意味しない

Pages Functionがコンパイルされ、静的ファイルが公開されても、Runtime Bindingが欠けていればFunctionは実行時に失敗する。

10.2 公開エラーと内部診断ログを分ける

利用者へは counter_unavailable のような単純なエラーを返し、内部ログには具体的な例外を残すと安全に診断できる。

10.3 Binding名はコードと完全一致させる

今回のコードが要求する名前は COUNTER_DB である。Dashboard側のVariable nameと完全一致している必要がある。

10.4 設定の正本を曖昧にしない

Dashboard管理とWrangler設定の両方を残す場合は、どちらを正本にするか明確にする。今回のリポジトリでは wrangler.toml を削除し、Pagesの設定をDashboard管理へ寄せた。

10.5 公開カウンターとアクセス解析を混同しない

今回のカウンターはタブセッション単位の概算値であり、ユニーク訪問者数ではない。詳細な分析は既存のCloudflare Web Analyticsを使う。

11. プライバシー境界

トラブルシュート時のCloudflare Runtimeログには、接続元IP、概略地域、User-Agent、TLS・ブラウザ関連メタデータ等が含まれていた。

そのため、本公開記録には生ログ全体を掲載せず、診断に必要なエラー文字列だけを抽出した。

D1カウンター自体には個別訪問者を識別する情報を保存していない。

12. 未解決事項

今回の記録では、D1 BindingがCloudflare Pages設定から消えた理由までは確定していない。

また、デプロイログで意図していたHugoバージョンと異なる v0.147.7 が使われた場面も観察した。この差異はカウンター復旧には不要だったため、本実験では未調査とした。

13. 最終状態

2026-08-14時点で、以下を確認済みとする。

構造化記録は experiments/CLOUDFLARE-COUNTER-001/ に保存した。