いいねカウントAPIをブログパーツとして使う
ページの「いいねカウント」を Cloudflare 上で動かす小さな API と、それを自分のサイトに ブログパーツ として埋め込む <script> 1行を作る。「同じブラウザからは1回だけカウント」の仕組みを自分で組み立てるのが今回のテーマ。題材はいいねだが、応用範囲は広い。
APIは別経路(ブックマークレット・Chrome拡張機能)からも使えるように、共通の window.likeApi というインターフェースを公開しておく。それらの呼び出しは続編の LEX(いいねカウントAPIをブックマークレットとChrome拡張機能から使う) で扱う。
本記事は応用編。ロードマップの Wranglerハンズオン を完了している前提で進める。Wrangler を使って手元から直接デプロイする方式なので、GitHub も Git連携も不要。
本記事の位置付け
Section titled “本記事の位置付け”ページにいいねボタンを置きたいなら、Claude Code にこんなざっくりとした依頼でも進められる:
ブログに付けるいいねボタンを Cloudflare で作りたいClaude Code は使う技術や構成を聞きながら案内してくれる。ただし、その過程で取る道は人によって違う。
本記事では、その中で Cloudflare Workers + Workers Static Assets + D1 を Wrangler で公開する 道筋を、つまずきにくい順序で追っていく。
1. 何を作るか
Section titled “1. 何を作るか”ページごとの「いいね数」をブラウザから取得・+1できる小さなAPIサービス。
仕様:
- ページ単位でいいね数をカウントする
- URL単位でカウント
- このAPIを使う全利用者のいいね数
- 同じブラウザから何度押しても1票しかカウントされないようにする
- ブラウザ側で十分な長さを持つランダムなIDを発行してそれをいいねAPIのIDとして使う
- IDは localStorage で保存することで1ブラウザにつき 1 IDとなる
- いいね数を取得するAPIといいねを +1 するAPIを用意
- APIはあらゆるWebページからアクセスできる
使い方の一例:
- ブログパーツ(APIとは別に作成)として、ブログの各記事のテンプレートに以下を貼り付ける
<button id="like-button">♥</button><script src="https://my-like-api.〇〇.workers.dev/like.js"></script>
<button>がいいねボタン本体、<script>で配信されるlike.jsがボタンに「♥ 数字」の表示・クリック動作を結びつける- 記事ごとのURLが識別子になるので、テンプレートに1回貼るだけで全記事に効く
全体像。本記事はデータベース(4〜5章)→ バックエンド(6章)→ フロントエンド相当(8章)の順に作っていく。
sequenceDiagram
participant pg as 自分のブログの記事ページ<br>=フロントエンド相当(8章)
participant cf as Cloudflare(my-like-api)<br>=バックエンド(6章)+データベース(4〜5章)
pg->>cf: like.js を読み込み(script src)
cf-->>pg: like.js(window.likeApi)
pg->>cf: fetch GET /api/like/:id
cf-->>pg: { count }
Note over pg: ボタンに「♥ 数字」を表示<br>読者がクリック
pg->>cf: fetch POST /api/like/:id(client_id 付き)
Note over cf: Worker が D1<br>(pages / likes)を読み書き
cf-->>pg: { count, liked }
Note over pg: ボタンの数字を更新
題材はいいねカウントだが、同じパターンで「閲覧回数表示」「不適切報告」「オンラインブックマーク」などにも流用できる。
このハンズオンで作るのは「個人ブログで動く程度のいいねボタン」。本格的な不正対策(連打防止・複アカ防止)は範囲外。詳しくは 9-1. 担保の範囲 で正直に説明する。
2. 事前準備
Section titled “2. 事前準備”2-1. 作業フォルダを作って Claude Code を起動
Section titled “2-1. 作業フォルダを作って Claude Code を起動”作業フォルダ ~/claude/my-like-api を作って、Claudeデスクトップアプリを起動。
Code(Claude Code)を選択 → New session をクリック → 作業フォルダを指定(~/claude/my-like-api)
名前は別のものでもよい。以降
my-like-apiと出てきたら自分のプロジェクト名に読み替える。
公開ファイルは public/ 配下、Worker のコードは src/index.js、設定ファイルはルート直下に置く構成にする。
2-2. Wrangler ログイン状態の確認
Section titled “2-2. Wrangler ログイン状態の確認”Wrangler ハンズオンを完了している場合、Node.jsとWranglerログインは済んでいるはず。下記コマンドでログイン状態を確認する。
npx wrangler whoamiアカウント名やメールアドレスが表示されればOK。表示されない場合はWranglerハンズオンの2章を参照してインストール・ログインする。
3. wrangler.jsonc の作成
Section titled “3. wrangler.jsonc の作成”Worker と D1、静的ファイル配信をつなぐための設定ファイル。Claude Code に作成を依頼する。
以下のテンプレートで wrangler.jsonc を作成してください。プロジェクト名は「my-like-api」、データベース名は「my-like-api-db」、YYYY-MM-DDは昨日、database_id はあとで記入するので xxxxxx のままにしておいてください。
---{ "name": "プロジェクト名", "main": "src/index.js", "compatibility_date": "YYYY-MM-DD", "assets": { "directory": "./public", "binding": "ASSETS" }, "d1_databases": [ { "binding": "DB", "database_name": "データベース名", "database_id": "xxxxxx", "migrations_dir": "migrations" } ]}---wrangler.jsonc:Wrangler の設定ファイル。プロジェクトのルートに置く。Worker の名前、対応する Cloudflare ランタイムのバージョン、静的ファイルの置き場、データベースへの接続情報などをまとめて宣言する。
.jsoncは コメントが書ける JSON。Cloudflare は新規プロジェクトにwrangler.jsoncを推奨している。
各項目の意味:
| 項目 | 内容 |
|---|---|
main | APIリクエストを処理する Worker スクリプトのパス |
compatibility_date | 使用する Workers ランタイムのバージョン基準日。UTC基準のため、JSTの今日の日付は未来日になる場合があるので前日以前を指定する |
assets.directory | 静的ファイルの置き場(フロントエンド) |
assets.binding | Worker から ASSETS にアクセスするための名前 |
d1_databases | D1 データベースの接続設定 |
migrations_dir | マイグレーションファイルの置き場 |
4. 【データベース】D1 データベースを作成(初回のみ)
Section titled “4. 【データベース】D1 データベースを作成(初回のみ)”ターミナルで実行するか、Claude Code にプロンプトとして渡す。
npx wrangler d1 create my-like-api-db「既に存在しています」エラーが出たら、別名(例:
my-like-api-db2)で作り直す。wrangler.jsoncのdatabase_nameも合わせて書き換える。
実行すると database_id が表示される。Claude Code に伝えて wrangler.jsonc を書き換えてもらう。
wrangler.jsonc の database_id を「xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx」に書き換えてください。Cloudflare D1:Cloudflare が提供するサーバーレスの SQL データベース。中身は SQLite で、Worker から
bindingを通してアクセスする。サーバーを自前で立てる必要がなく、Cloudflare の世界の中で完結する。無料プランで1日あたり500万行の読み込み、10万行の書き込みまで使えるので、個人サイト規模のいいねカウントなら十分。
Database ID(
database_id):データベースの識別子。仮に外部に漏れても、API Tokenがなければ操作できないため問題なし。
5. 【データベース】D1 スキーマとマイグレーション
Section titled “5. 【データベース】D1 スキーマとマイグレーション”Claude Code にアプリの仕様を伝えてテーブル設計を相談する。
いいねカウントAPIを作ります。仕様は以下のとおりです。
- ページ単位でいいね数をカウントする- 同じブラウザから何度押しても1票しかカウントされないようにする(ブラウザごとの識別子で判定)- いいね数の取得と +1 のAPIを用意する
このアプリに必要なデータベースのテーブル設計を提案してください。Claude Code が以下のようなスキーマを提案してくれる。
CREATE TABLE pages ( id TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0);
CREATE TABLE likes ( client_id TEXT NOT NULL, page_id TEXT NOT NULL, PRIMARY KEY (client_id, page_id));pages: ページごとの「いいね数」を保持する集計テーブルlikes: 「誰が(client_id)どのページに(page_id)いいねしたか」の事実テーブル。複合主キー(client_id, page_id)が1ブラウザ1いいねの担保
この複合主キーを INSERT OR IGNORE と組み合わせると、SQLレベルで「初回だけ追加、2回目以降は無視」が一文で書ける。これが重複判定の核心パターン。
INSERT OR IGNORE:SQLite/D1 で「制約違反になる INSERT は静かに無視する」構文。
INSERT INTO ...でPK重複なら通常はエラーになるが、INSERT OR IGNORE INTO ...なら何も起きずに 0 行影響で終わる。今回は「既にいいね済み」を判定する用途で使う。
マイグレーション:DB のスキーマ変更(テーブル作成や列追加など)を
.sqlファイルとして管理する仕組み。migrations/0001_init.sql、migrations/0002_...sqlのように番号付きで並べておくと、未適用のものだけが順に実行される。
「pages.count を別に持つ理由」は、毎回 SELECT COUNT(*) FROM likes WHERE page_id = ? だと数が増えたとき重くなるため。POST 成功時だけ集計値を +1 して、GET を軽くする。
スキーマに納得したら、マイグレーションファイルを作る。
このスキーマでマイグレーションファイルを作成してください。ファイルは migrations/0001_init.sql に保存してください。6. 【バックエンド】Worker の実装
Section titled “6. 【バックエンド】Worker の実装”6-1. Worker の実装
Section titled “6-1. Worker の実装”Claude Code に依頼してファイルを作ってもらう。
いいねカウントAPIを作って!仕様とスキーマは先ほど設計したものを使う。エンドポイントは GET /api/like/:id(いいね数の取得)と POST /api/like/:id(+1)。パスの :id は encodeURIComponent されたページ識別子(: や / を含む)なので、decodeURIComponent してから使う。
Cloudflare Workers + Workers Static Assets の構成で、src/index.js に実装。/api/* 以外のリクエストは Static Assets にフォールバックして public/ 配下を配信する構成。D1 の binding 名は DB、Assets の binding 名は ASSETS。CORS の Access-Control-Allow-Origin: * を付け、OPTIONS への対応も入れてください。生成されたコードの動きを Claude Code に箇条書きで説明してもらってから、自分の手で一度はざっと読む。「いまどの SQL が走って、何を返しているか」が頭に入っていると、後でデバッグするときに楽。
6-2. 実装例
Section titled “6-2. 実装例”Claude Code が生成した API は、実行のたびに細部が変わるが、おおよそ次のような外部仕様になっているはず。本記事の以降の章はこの仕様を前提に進める。
エンドポイント
| メソッド | パス | 用途 |
|---|---|---|
GET | /api/like/:id | ページ :id のいいね数を取得 |
POST | /api/like/:id | ページ :id のいいね数を +1 |
:id はパスパラメータで、ページを識別する文字列(後述の正規化URL)。
レスポンス
GET /api/like/:id:
{ "count": 42 }該当ページがまだ登録されていない場合は { "count": 0 }。
POST /api/like/:id:リクエストボディは { "client_id": "..." }。レスポンスは
{ "count": 43, "liked": true }liked: true… 今回新規にいいねが追加されたliked: false… この client_id は既にこのページにいいね済みで、カウントは変わらない
CORS は別オリジンから呼ぶ前提なので Access-Control-Allow-Origin: * を付ける。
生成された実装が上記と違っていて、本記事の以降の流れに合わせたい場合は、上のプロンプトの末尾に以下を追加して投げ直す:
- GET のレスポンスは
{"count": 42}の形 - POST のリクエストボディは
{"client_id": "..."}、レスポンスは{"count": 43, "liked": true}の形
6-3. Worker と Workers Static Assets の役割分担
Section titled “6-3. Worker と Workers Static Assets の役割分担”今回の実装で使う Workers Static Assets とは、Worker と一緒に静的ファイル(HTML/JS/CSS/画像など)を配信できる仕組み。wrangler.jsonc の assets.directory で指定したフォルダ(例:./public)の中身が、そのまま公開URLでアクセスできる。
静的ファイルへのリクエストは Worker のコードを通さずに配信される(高速、課金対象外)。一方、Worker 側のコードを通したいリクエスト(API など)は、fetch ハンドラ内で処理する。
本記事では:
/api/*のリクエスト → Worker が処理(D1 にアクセスしていいねカウント等を返す)- それ以外(
/like.js、/index.htmlなど) → Worker のコードを通さず Static Assets として直接配信
という構成にする。
7. 初回デプロイ
Section titled “7. 初回デプロイ”.gitignore の設定。プロジェクトに作られる .wrangler/ フォルダ(ローカル状態のキャッシュ)はコミット不要。
.gitignore に .wrangler/ と node_modules/ を追加して本番DBにマイグレーションを適用する。
npx wrangler d1 migrations apply my-like-api-db --remoteWorker をデプロイ。
npx wrangler deploy実行すると公開URLが表示される。だいたい https://my-like-api.<アカウント名>.workers.dev のような形式。
表示された URL のサブドメイン部分(
<アカウント名>の箇所)は、Cloudflare アカウントごとに固有の文字列が割り当てられる。例:https://my-like-api.tatsuwo.workers.devのようになる。以降https://my-like-api.workers.devと出てきたら自分のアカウントのフルURLに読み替える。
ブラウザで https://〇〇.workers.dev/api/like/page1 を直接開いてみる。{"count":0} が返ってくれば API は動いている。
POST の動作確認は curl で:
curl -X POST https://〇〇.workers.dev/api/like/page1 \ -H "Content-Type: application/json" \ -d '{"client_id":"client-A"}'{"count":1,"liked":true} が返ってきて、もう一度同じコマンドを叩くと {"count":1,"liked":false} になれば、client_id 重複判定が機能している。
8. 【フロントエンド相当】ブログパーツとして使う
Section titled “8. 【フロントエンド相当】ブログパーツとして使う”API が動いたら、次は自分のサイトに <script> で埋め込んで動かす。1章の全体像でいう左側(自分のブログ側)を作る工程。共通ロジックは window.likeApi として公開し、続編 LEX のブックマークレット・Chrome拡張機能でも再利用できるようにしておく。
8-1. like.js を作る
Section titled “8-1. like.js を作る”like.js はブログパーツの本体スクリプト。貼り付けたWebページ上で動き、APIを呼んでいいね数を取得・送信し、結果をボタン(「♥ 数字」)として表示する。public/like.js として配置する(Workers Static Assets から配信される)。
初心者版(ざっくり):
さっき作った API を使って、いいねボタンを動かす JavaScript を public/like.js に作って!ページに <button id="like-button"> があったら、いいね数を取得してボタンに「♥ 数字」の形で表示。ボタンをクリックすると +1。同じブラウザから2回目以降は数字が増えないようにして。API は https://my-like-api.〇〇.workers.dev/api/like/<ページのURL> の形(自分のWorkerのURLに置き換え。最後にいまのページのURLを付ける)。ページのURLは / などを含むので、encodeURIComponent で包んでからパスに付ける。これで「個人ブログにいいねボタンを置く」用途には十分な実装ができる。
詳細版: 続編 LEX(ブックマークレットや Chrome 拡張機能) でも同じロジックを使い回したい、共通ロジックを window.likeApi として公開しておきたい、といった事情があるなら、より具体的に指定する:
public/like.js を作って。以下の動作をする。
【共通ロジック(IIFE 内に閉じ込める)】- localStorage から like-uuid を取得。なければ crypto.randomUUID() で発行して保存- 現在ページのURLを正規化(hash と search を削除、末尾スラッシュを削除)して pageId にする- API のパスに pageId を入れるときは encodeURIComponent で包む(pageId は : や / を含むため。Worker 側は decodeURIComponent で戻す)- getCount(): pageId の API(https://my-like-api.〇〇.workers.dev/api/like/:pageId)に GET して { count } を返す- like(): 同じ API に client_id 付きで POST して { count, liked } を返す- API のオリジンは自分のWorkerのURLに置き換える
【公開API】- window.likeApi = { getCount, like } として外から呼べるようにする
【ボタン自動バインド(ブログパーツ用途)】- DOMContentLoaded 後(または既に load 済みなら即時)、ページ内に <button id="like-button"> があれば - getCount() で取得した数字を「♥ 数字」の形でボタンに表示 - クリックで like() を呼び、レスポンスの count でボタンを更新- ボタンが無ければ何もしない(ブックマークレットから呼ばれる用途を想定)like.js を更新したら、再度デプロイする:
npx wrangler deployブログパーツを使う側のHTMLには、以下を貼る:
<button id="like-button">♥</button><script src="https://my-like-api.〇〇.workers.dev/like.js"></script>これだけで、別ドメインのサイトからもこの API を使えるようになる。
ブックマークレットや Chrome 拡張機能から同じ API を呼ぶ方法は、続編 LEX(いいねカウントAPIをブックマークレットとChrome拡張機能から使う) を参照。ブックマークレットは
like.jsのwindow.likeApiをそのまま使い回し、拡張機能は同じ API を直接呼ぶ。
8-2. UUID 発行と localStorage 保存
Section titled “8-2. UUID 発行と localStorage 保存”詳細版のプロンプトで明示している、ブラウザを識別する UUID を localStorage で保存するパターンを解説する。
crypto.randomUUID() でランダムな UUID(v4)を作る。これがこのブラウザの「ローカル識別子」になる。
let uuid = localStorage.getItem('like-uuid');if (!uuid) { uuid = crypto.randomUUID(); localStorage.setItem('like-uuid', uuid);}初回アクセス時にだけ生成、以降は同じものを使い回す。
localStorage は オリジン単位(スキーム + ホスト + ポート)で保存される。同じ人でも blog-a.com と blog-b.com では別の UUID になる点に注意。
localStorage:ブラウザに文字列で値を保存できる仕組み。Cookie と違ってサーバーに自動送信されない。基本はクリアするまで残るが、Safari・iOS のブラウザ(WebKit)はしばらく開かないと自動で消すことがある(永続ではない)。容量は5MB前後。
8-3. pageId の正規化
Section titled “8-3. pageId の正規化”詳細版のプロンプトで明示している、ページのURLを正規化して識別子(pageId)として使うルールを解説する。
「いいねの集計単位」をどう決めるか。本記事では URL ベースで正規化したものを pageId として使う。
function getPageId() { const u = new URL(window.location.href); u.hash = ''; // # 以降を削除(SPA等の内部リンク) u.search = ''; // ? 以降を削除(utm 等のトラッキングパラメータ) let id = u.toString(); id = id.replace(/\/$/, ''); // 末尾スラッシュを削除 return id;}正規化ルールの理由:
| ルール | 例 | 理由 |
|---|---|---|
# 以降を削除 | /post1#section → /post1 | ページ内アンカーは同一ページ |
? 以降を削除 | /post1?utm=twitter → /post1 | SNSのトラッキング流入を別カウントしない |
末尾 / を削除 | /post1/ → /post1 | /post1 と /post1/ を同一視 |
ホスト名はそのまま残る(URL オブジェクトが自動で小文字化する)。
本格的な用途だと
?p=2のようなページネーション用パラメータも別カウントしたい場合があり、許可リスト方式の正規化が必要。本記事のサンプル実装では割り切って一律削除する。
このまま URL を pageId として使うので、pageId は https://example.com/post1 のような 読める文字列 になる。D1 で SELECT * FROM pages したときにデバッグしやすい。
pageIdは:や/を含むので、API のパス(/api/like/:id)に入れるときはencodeURIComponentで包む。受け取る Worker 側はdecodeURIComponentで元に戻してから DB のキーにする。これを忘れると/がパスの区切りと解釈され、ルーティングが崩れる。
9. 危険性と限界
Section titled “9. 危険性と限界”9-1. 担保の範囲
Section titled “9-1. 担保の範囲”想定されるケースごとの振る舞いを並べる。正常な利用には効くが、悪意ある攻撃者にはやられ放題なケースもある。隠さず書いておく。
| ケース | 結果 |
|---|---|
| 普通のユーザー(同じブラウザ) | 1票のみ。サーバー側 INSERT OR IGNORE で担保 |
| localStorage を消去した人 | 新しい UUID が発行されるので再カウント可能 |
| Safari・iOS で7日ほど放置 | localStorage が自動削除され、新しい UUID が発行される(再カウントされうる) |
| プライベートブラウジング | localStorage がセッション限り。閉じると毎回新規 UUID |
| 別ブラウザ・別デバイス | それぞれ別人扱い(仕様通り) |
| 別ドメインに埋め込まれた同一人物 | オリジンごとに別 localStorage なので別人扱い |
| 開発者ツールで UUID を毎回変える攻撃者 | 防げない |
| 連打する攻撃者 | UUID が同じなら2回目以降は無視されるが、毎回違う UUID を送られたら防げない |
つまり、これは 「素直なユーザーが意図せず多重投票することを防ぐ」 装置であって、「攻撃者の不正投票を防ぐ」 ものではない。
個人ブログのいいねカウントとしては十分なレベル。本格的な投票システムや有料コンテンツ評価などには向かない。
9-2. プライバシー上の注意
Section titled “9-2. プライバシー上の注意”UUID は ユーザーごとに一意なランダム識別子 であり、扱い方によってはトラッキング目的にも使える。以下を守る:
- UUID をサーバー側のログに出さない。Cloudflare のリクエストログにも極力残さない
- UUID と他の情報(IPアドレス、User-Agent、参照元など)を組み合わせて保存しない
- 読み取り API(GET)では UUID を要求しない。POST だけで使う
- GDPR・改正個人情報保護法などを意識する文脈(EU圏ユーザーが多い、企業サイト等)ではプライバシーポリシーに明記する
「いいね数を集計する」だけが目的なら、UUID をDBに残す以上の処理は不要。
9-3. 本格運用するときの強化策
Section titled “9-3. 本格運用するときの強化策”個人ブログ規模を超えて本格運用したい場合、以下を組み合わせると安全性が大きく上がる:
- 認証付き化: GitHub OAuth や Cloudflare Access を組み合わせて、ログインユーザーごとに1票にする
- 連打防止: 同じ IP からの POST に Cloudflare のレート制限を入れる
- 動的アクセス制御: 特定オリジンからしか POST を受け付けないように
Originヘッダーをチェック
10. 発展課題
Section titled “10. 発展課題”機能を広げたい場合の方向性:
- いいね取り消し:
DELETE /api/like/:idを追加して、likesから行を消し、pages.countを -1 - ハッシュベース pageId: URL をそのまま使うのが嫌なら SHA-256 でハッシュ化(短く・特殊文字なし・URLを隠せる)
- 集計の高度化: 日別・週別の集計、人気記事ランキングなど。
likes.created_atを追加して時系列クエリ - 独自ドメインの設定:
〇〇.workers.devではなく独自ドメインで配信したい場合は、Cloudflare ダッシュボードの Workers & Pages → 該当 Worker → Settings → Domains & Routes で設定できる
これらを少しずつ足していくと、本格的なリアクションシステムに育っていく。