Cloudflare Pages, Workersを使って、サイトを立ち上げた備忘録:
なるべくシンプルにしたかったので、githubを使用せず、コードはすべてローカルに置き、devモードで動きを確認してからdeployモードでアップロードした。
作りたいのは、有名な英国郵便局の会計システムHorizonの被害者へ同情と連帯の意を示す人にカウントボタンをクリックしてもらい、日本の何人が同意するかを調べる、サイトである。
それだけの機能しかない。彼らにもurlを教えてどれだけの日本人が同情しているかを見て、少しでも癒されて欲しいと考えたわけ。
これが上手くいったら次は英語のサイトを作り世界中で何人が同情・連帯の意を示すかを調べたい。
文言がまだ決まっていないので、完成は少々先だが、機能することを確認したので、共有したい。
実際に時間がかかったのは、chatgptに作業している新しいUIを教えるのに手間がかかった。
構成:
ホームページ:Cloudflare Pages
投票処理:Cloudflare Workers
データベース:Cloudflare D1
ドメイン:最初は無料の *.pages.dev を利用し、活動が軌道に乗ったら独自ドメイン(例:js4hv.org)を取得
この構成なら、初期費用はほぼ0円で始められるはず。
手順:
Cloudflareにサインアップして、サインイン。
左側のサイドメニューのBuild>compute>Workers & Pages を選ぶ:
HTMLだけのホームページを作る:
まず準備:
ローカルPCに適当なフォルダを作り、index.htmlを作成する:
js4hv/
└── index.html
index.htmlに以下のコードをコピペする:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>英国郵便局 Horizon 被害者への連帯</title>
</head>
<body>
<h1>英国郵便局 Horizon 被害者への連帯</h1>
<p>
英国郵便局のHorizonシステムによって、
多くの郵便局長が長年にわたり深刻な被害を受けました。
</p>
<p>
私は、日本から被害者の皆さまに
同情と連帯の意を表します。
</p>
<button id="supportButton">
私も連帯の意を表します
</button>
<p>
現在の賛同者数:
<strong id="count">0</strong> 人
</p>
<script>
const button = document.getElementById("supportButton");
const count = document.getElementById("count");
button.addEventListener("click", () => {
if (localStorage.getItem("supported")) {
alert("すでに賛同されています。");
return;
}
localStorage.setItem("supported", "true");
alert("ご賛同ありがとうございます。");
});
</script>
</body>
</html>
このページの右上の Create applicationボタンをクリック:
今回はGitHubを使わず、**Direct Upload(Drag and drop)**で進める。
Start with Hello World!
Select a template
Upload your static filesがあるが、これを選ぶ:
uploadするのをfileかfolderを選ぶので、folderを選ぶ:
先ほど用意したjs4hvフォルダを選ぶ。
するとDeployボタンと適当な名前のWorker nameが出現する:
Worker nameを適切な名称に変更する:js4hv-support
Deployをクリックする。Visitをクリックする:
ブラウザに「英国郵便局 Horizon 被害者への連帯」のページが表示される。
現在のHTMLの localStorage は、同じブラウザでの連続クリックを防いでいるだけで、別のブラウザや別の端末なら何度でも投票できる。
そこで
ブラウザ → Cloudflare Worker → Cloudflare D1データベース
という構成にする。
上部のボタン群からBindingsをクリック:
**「一人が連続してクリックしても増えない」**という部分は、単なるCookieだけに頼らず、サーバー側でも制御する設計にする。
① D1データベースを作る
↓
② そのD1をWorkerにBindingする
↓
③ 賛同ボタンからWorkerを呼ぶ
↓
④ D1に1票を記録する
の順にやっていく。
① D1データベースを作る:
ここがちょっと探しづらかった
Buildカテゴリ>AI Storage & databases>D1 SQLite Databaseを選ぶ:
Create (...)をクリックして、データベース名を求められたら、
js4hv-votes に設定する
② D1をWorkerにBindingする:
次の操作
今のD1画面はそのままにしておく
js4hv-support の画面に戻る
上部の Settings → Bindings
Add binding
D1 Database の Add Binding
Variable name:DB
D1 database:今作った js4hv-votesを選んでAdd bindingをクリックしたらBindings
Add and connect external resources to your Worker without needing to manage permissions or API keys.と表示され、Connected Bindingsにjs4hv-support が+Bindingに接続されている画が出る。
構成が:
ブラウザ
│
▼
js4hv-support
│
│ Binding: DB
▼
js4hv-votes
D1の中に**「賛同票を記録する仕組み」**を作る:
D1 Database>js4hv-votes ページで上部にあるConsoleをクリック:
すると使えるSlash commandsの一覧が出る
その下の入力欄に以下をコピペ:
CREATE TABLE votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vote_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Executeをクリックする。
SQLの実行結果:
CREATE TABLE votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, vote_key TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP );
This query successfully executed.
この votes テーブルには、
項目 意味
id 投票番号
vote_key 同じ人の重複投票を防ぐための識別子
created_at 投票日時
が生成されたので保存する。
特に重要なのが、
UNIQUE
これによって、同じ vote_key を持つ投票をデータベース側で二重登録できなくする。
今の状態:
Webサイト
↓
js4hv-support Worker
↓ Binding: DB
js4hv-votes
└── votes テーブル
これからWorkerコードを作成して行くがトラブった。
cd /e/my/cloudflare-pages/js4hv
npm init -y
npm install --save-dev wrangler@latest
ここでエラーが出たので、ここから少しづつCMDで確認していくことにした(理由はgit-bashのwindows パスが見えずらいのとgit-bashが怪しいと直感。chatgptが反対したがCMDに切り替えた。これが当たり!!)
以下はchatgptによる冗長な確認は省略して、記す。
dirの出力:
E:\my\cloudflare-pages\js4hv のディレクトリ
2026/08/09 08:45
node_modules が残っている。先ほどGit Bashの rm -rf では一度消えたように見えたが、npm install が途中まで作成して、その後失敗したため再び存在している。
rmdir /s /q node_modules
で確実に削除した。
以下Wranglerを少しづつインストールしていく:
npm install --save-dev wrangler@4.120.0 --ignore-scripts
added 34 packages, and audited 35 packages in 6s
6 packages are looking for funding
run npm fund for details
found 0 vulnerabilitie
npx wrangler --version
E:\my\cloudflare-pages\js4hv>npx wrangler --version
⛅️ wrangler 4.120.0
npx wrangler login
ブラウザが開いて、Cloudflareへの認証を求められる。
────────────────────
Attempting to login via OAuth...
Opening a link in your default browser: [https://dash.cloudflare.com/oauth2/auth?response\_type=code&client\_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect\_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers\_kv%3Awrite%20workers\_routes%3Awrite%20workers\_scripts%3Awrite%20workers\_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl\_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20websearch.run%20agent-memory%3Awrite%20queues%3Awrite%20pipelines%3Awrite%20secrets\_store%3Awrite%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email\_routing%3Awrite%20email\_sending%3Awrite%20browser%3Awrite%20challenge-widgets.write%20offline\_access&state=Nv\_drzn74wxxj\_lnZkU\_.wgBAXc9\_15P&code\_challenge=4P2IcRK85jwQIfV2cza6knh169aX3VPu3--elRNwgy8&code\_challenge\_method=S256](https://dash.cloudflare.com/oauth2/auth?response_type=code\&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7\&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback\&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20websearch.run%20agent-memory%3Awrite%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20artifacts%3Awrite%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20browser%3Awrite%20challenge-widgets.write%20offline_access\&state=Nv_drzn74wxxj_lnZkU_.wgBAXc9_15P\&code_challenge=4P2IcRK85jwQIfV2cza6knh169aX3VPu3--elRNwgy8\&code_challenge_method=S256)
Successfully logged in.
? Before you go, Wrangler detected AI coding agents that may not be best configured to work with Cloudflare: Cursor, Codex, Continue, GitHub Copilot, Gemini CLI, Antigravity, Junie. Would you like Wrangler to automatically install Cloudflare skills for the best experience? » (Y/n)
AI使わないので、n と入力する。
mkdir public
mkdir src
copy index.html public\index.html
notepad wrangler.jsonc #jsoncを作成する
以下をコピペ
{
"name": "js4hv-support",
"main": "src/index.js",
"compatibility_date": "2026-08-09",
"assets": {
"directory": "./public",
"binding": "ASSETS"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "js4hv-votes",
"database_id": "53e490f5-fcc0-472c-ba5c-b35944d1b6b9"
}
]
}
保存して、閉じる。
次にWorker本体を作る
notepad src\index.js
次をコピペする:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 賛同者数を取得
if (url.pathname === "/api/count" && request.method === "GET") {
const result = await env.DB
.prepare("SELECT COUNT(*) AS count FROM votes")
.first();
return Response.json({
count: result.count
});
}
// 賛同する
if (url.pathname === "/api/vote" && request.method === "POST") {
const body = await request.json();
const voteKey = body.voteKey;
if (!voteKey) {
return Response.json(
{ error: "voteKey is required" },
{ status: 400 }
);
}
try {
await env.DB
.prepare(
"INSERT INTO votes (vote_key) VALUES (?)"
)
.bind(voteKey)
.run();
const result = await env.DB
.prepare("SELECT COUNT(*) AS count FROM votes")
.first();
return Response.json({
success: true,
count: result.count
});
} catch (error) {
// UNIQUE制約による重複投票
if (String(error).includes("UNIQUE")) {
const result = await env.DB
.prepare("SELECT COUNT(*) AS count FROM votes")
.first();
return Response.json({
success: false,
alreadyVoted: true,
count: result.count
});
}
throw error;
}
}
// それ以外は静的ファイルを返す
return env.ASSETS.fetch(request);
}
};
次はローカルでdry-run
npx wrangler deploy --dry-run
以下を含んだ成功表示:
env.DB (js4hv-votes) D1 Database
env.ASSETS Assets
つまり以下の構成ができている:
ブラウザ
│
▼
js4hv-support Worker
│
├── env.ASSETS → index.html
│
└── env.DB → js4hv-votes
│
└── votes
npx wrangler dev
以下のurlを含んだメッセージが表示される
http://localhost:8787
別のCMDで
curl http://127.0.0.1:8787/api/count
がエラーを出すのは、ローカルにはテーブルが無いから。
npx wrangler d1 execute js4hv-votes --local --command "CREATE TABLE votes (id INTEGER PRIMARY KEY AUTOINCREMENT, vote_key TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);"
でvotesテーブルを作成する。
再度実行すると
{"count":0}
と表示される。
次は「投票登録」をテストする:
curl -X POST http://127.0.0.1:8787/api/vote -H "Content-Type: application/json" -d "{\"voteKey\":\"test-user-001\"}"
{"success":true,"count":1}
同じキーでもう一度
curl -X POST http://127.0.0.1:8787/api/vote -H "Content-Type: application/json" -d "{\"voteKey\":\"test-user-001\"}"
{"success":false,"alreadyVoted":true,"count":1}
ここからが本番用の設計
今回の目的なら、Cookie + ランダムなUUID方式を採用
初めてサイトを訪問
↓
ブラウザにランダムなUUIDを発行
↓
Cookieに保存
↓
「賛同する」をクリック
↓
WorkerがUUIDを受け取る
↓
D1に保存
↓
同じブラウザから再クリック
↓
同じUUID
↓
UNIQUE制約 → 2票目を拒否
この方式のメリット
IPアドレスを保存しない
メールアドレスなど個人情報を要求しない
同じブラウザからの連続クリックを防げる
D1の UNIQUE と相性が良い
実装が比較的簡単
無料で運用しやすい
ただし、Cookieを消せば再投票できるという限界があるが、サイトの目的が選挙ではないので割り切る。
今回は localStorage の "supported" を使う方式から、ランダムなUUIDをCookieに保存する方式に変更する。Cookie名は support_id とする。
現在の public\index.html を丸ごと以下の内容で置き換える:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Horizon被害者への連帯</title>
</head>
<body>
<p>
英国郵便局のHorizonシステムによって、
多くの郵便局長が長年にわたり深刻な被害を受けました。
</p>
<p>
私は、日本から被害者の皆さまに
同情と連帯の意を表します。
</p>
<button id="supportButton">
私も連帯の意を表します
</button>
<p>
現在の賛同者数:
<strong id="count">0</strong> 人
</p>
<script>
const button = document.getElementById("supportButton");
const count = document.getElementById("count");
// ブラウザ固有の識別IDをCookieから取得。
// なければランダムなUUIDを作成して保存する。
function getSupportId() {
const name = "support_id=";
const cookies = document.cookie.split(";");
for (const cookie of cookies) {
const value = cookie.trim();
if (value.startsWith(name)) {
return value.substring(name.length);
}
}
const id = crypto.randomUUID();
document.cookie =
"support_id=" + id +
"; max-age=31536000" +
"; path=/" +
"; SameSite=Lax";
return id;
}
// 現在の賛同者数を取得
async function loadCount() {
try {
const response = await fetch("/api/count");
const data = await response.json();
count.textContent = data.count;
} catch (error) {
console.error("賛同者数の取得に失敗しました:", error);
}
}
// 賛同ボタン
button.addEventListener("click", async () => {
button.disabled = true;
try {
const supportId = getSupportId();
const response = await fetch("/api/vote", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
voteKey: supportId
})
});
const data = await response.json();
if (data.success) {
count.textContent = data.count;
alert("ご賛同ありがとうございます。");
} else if (data.alreadyVoted) {
count.textContent = data.count;
alert("すでに賛同されています。");
} else {
alert("賛同の登録に失敗しました。");
button.disabled = false;
}
} catch (error) {
console.error("投票処理に失敗しました:", error);
alert("通信エラーが発生しました。もう一度お試しください。");
button.disabled = false;
}
});
// ページを開いたときに現在の人数を取得
loadCount();
</script>
</body>
</html>
この変更で以下のような動きになる:
ページを開く
↓
/api/count
↓
D1から実際の賛同者数を取得
「賛同」クリック
↓
UUIDをCookieから取得
↓
/api/vote
↓
Worker
↓
D1
↓
votesテーブル
したがって、画面に表示される人数が本当のD1の人数になる。
http://127.0.0.1:8787
にアクセスして以下のテストを実施する:
1.「現在の賛同者数」が 1 と表示されるか
2.「私も連帯の意を表します」をクリック
3. ご賛同ありがとうございます。 が出るか
4. 表示が 2 になるか
5. もう一度クリックすると「すでに賛同されています」となるか
を確認する。
ただし、先ほど curl でローカルD1に test-user-001 の1票を入れているので、最初の表示は 1人 が正しい。
本番テスト
ブラウザで次の公開URLを開く:
js4hv-support.nagata-tetsu.workers.dev
先ほどと同様なテストをする:
ただし、最初の「現在の賛同者数」が 0 と表示される。
最後は、この値が1と表示される。
ここまでくれば、サイトはプログラム的にはできた!!
いよいよ、本番サイト用に諸々の文言を調整することになる。
後始末:
テストで自分の1票が入っている状態なので、これを削除して0にする:
npx wrangler d1 execute js4hv-votes --remote --command "SELECT id, vote_key, created_at FROM votes;"
│ id │ vote_key │ created_at │
│ 1 │ xxxxxxxx-xxxx-xxxx-b4e0-41e3b3eb13a6 │ 2026-08-09 02:00:14 │
このid=1のみを消す
npx wrangler d1 execute js4hv-votes --remote --command "DELETE FROM votes WHERE id = 1;"
賛同者数が 0 になった
現在の状態
Cloudflare Worker:js4hv-support ✅
公開URL:https://js4hv-support.nagata-tetsu.workers.dev ✅
D1:js4hv-votes ✅
votes テーブル:存在 ✅
テスト票:削除済み ✅
現在の賛同者数:0人 ✅
重複投票防止:本番で確認済み ✅
今後のサイトの更新法:
public\index.html
src\index.js
wrangler.jsonc
を更新したら、
npx wrangler deploy
で更新する。
以上