いいねカウントAPIをブログパーツとして使う
⚠️ 現在製作中のため、内容は不正確なことがあります。
D1W で「同じブラウザから投稿した人には同じ会員番号」という仕様を Claude Code に作ってもらった。本記事では、その「同じブラウザ判定」を自分で組み立てるパターンを学ぶ。題材は いいねカウント だが、応用範囲は広い。
Cloudflare Pages Functions と D1 で いいねカウントAPI を作り、それを自分のサイトに ブログパーツ として埋め込む(<script> 1行で動く形)。
APIは別経路(ブックマークレット・Chrome拡張機能)からも使えるように、共通の window.likeApi というインターフェースを公開しておく。それらの呼び出しは続編の LEX(いいねカウントAPIをブックマークレットとChrome拡張機能から使う) で扱う。
本記事は応用編。ロードマップの Git と GitHub の基本・Wranglerハンズオン・CGA(GitHub Actions)・D1W(D1 Webアプリ) を一通り体験している前提で進める。
1. 何を作るか
Section titled “1. 何を作るか”ページごとの「いいね数」をD1に保存し、ブラウザから取得・+1できる小さなAPIサービス。同じブラウザからは1回しかカウントが増えないように、crypto.randomUUID() でクライアントごとのIDを発行し、サーバー側で重複判定する。
全体像:
[ブラウザ] [Cloudflare]ブログパーツ <script src=...like.js> <button id="like-button">♥</button> Pages Functions D1 │ /api/like/:id ──→ pages ↓ fetch (UUID付きPOST) ──→ likes ──────────────────────────→題材はいいねカウントだが、同じパターン(API + UUID + INSERT OR IGNORE)は「閲覧回数」「ブックマーク数」「お気に入り」などにそのまま使える。
このハンズオンで作るのは「個人ブログで動く程度のいいねボタン」。本格的な不正対策(連打防止・複アカ防止)は範囲外。詳しくは 第11章 担保の範囲と限界 で正直に説明する。
2. 事前準備
Section titled “2. 事前準備”2-1. GitHubでリポジトリを作成
Section titled “2-1. GitHubでリポジトリを作成”GitHubアカウントの作成や gh のセットアップはGit と GitHub の基本を参照のこと。
GitHub → New repository → リポジトリ名を mk-like-api で作成。Private にしておく(意図しないファイルの公開を防ぐため)。
名前は別のものでもよい。以降
mk-like-apiと出てきたら自分のリポジトリ名に読み替える。
2-2. Account ID 確認・APIトークン作成・Secrets 登録
Section titled “2-2. Account ID 確認・APIトークン作成・Secrets 登録”Cloudflareアカウントがなければ作る(こちらの手順(1-1))。
GitHub ActionsでCloudflare自動デプロイを設定するの 2章「事前準備」 を参照して、Account IDの確認・APIトークンの作成・GitHubへの Secrets 登録を行う。完了したらここに戻る。
2-3. リポジトリを clone する
Section titled “2-3. リポジトリを clone する”ターミナル(Claudeデスクトップアプリ右上の ビューメニュー → ターミナル でもよい)で以下を実行する。
mkdir -p ~/Desktop/claudecd ~/Desktop/claudegit clone git@github.com:ユーザー名/mk-like-api.gitGitHub への接続がまだの場合はGit と GitHub の基本を参照。
2-4. Claude Code を起動
Section titled “2-4. Claude Code を起動”Claudeデスクトップアプリを起動。
Code(Claude Code)を選択 → New session をクリック → 作業フォルダを指定(~/Desktop/claude/mk-like-api)
公開ファイルは public/ 配下、サーバー側のAPI実装は functions/api/ 配下、設定ファイルはルート直下に置く構成にする。
3. Wrangler ログイン状態の確認
Section titled “3. Wrangler ログイン状態の確認”Wrangler ハンズオンを完了している場合、Node.jsとWranglerログインは済んでいるはず。下記コマンドでログイン状態を確認する。
npx wrangler whoamiアカウント名やメールアドレスが表示されればOK。表示されない場合はWranglerハンズオンの2章を参照してインストール・ログインする。
4. wrangler.toml の作成
Section titled “4. wrangler.toml の作成”D1データベースとPages Functionsを接続するための設定ファイル。Claude Code に作成を依頼する。
以下のテンプレートで wrangler.toml を作成してください。プロジェクト名は「mk-like-api」、データベース名は「mk-like-api-db」、YYYY-MM-DDは昨日、database_id はあとで記入するので xxxxxx のままにしておいてください。
---name = "プロジェクト名"compatibility_date = "YYYY-MM-DD"pages_build_output_dir = "./public"
[[d1_databases]]binding = "DB"database_name = "データベース名"database_id = "xxxxxx"migrations_dir = "migrations"---pages_build_output_dir = "./public" で公開対象を public/ 配下のファイルに絞る。
5. 【データベース】D1 データベースを作成(初回のみ)
Section titled “5. 【データベース】D1 データベースを作成(初回のみ)”ターミナルで実行するか、Claude Code にプロンプトとして渡す。
npx wrangler d1 create mk-like-api-db「既に存在しています」エラーが出たら、別名(例:
mk-like-api-db2)で作り直す。wrangler.tomlのdatabase_nameも合わせて書き換える。
実行すると database_id が表示される。Claude Code に伝えて wrangler.toml を書き換えてもらう。
wrangler.toml の database_id を「xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx」に書き換えてください。Database ID(
database_id) — データベースの識別子。仮に外部に漏れても、API Tokenがなければ操作できないため問題なし。
6. 【データベース】D1 スキーマとマイグレーション
Section titled “6. 【データベース】D1 スキーマとマイグレーション”Claude Code にアプリの仕様を伝えてテーブル設計を相談する。
いいねカウントAPIを作ります。仕様は以下のとおりです。
- ページ単位でいいね数をカウントする- 同じブラウザから何度押しても1票しかカウントされないようにする (ブラウザ側で localStorage に UUID を持つ前提)- いいね数の取得と +1 のAPIを用意する
このアプリに必要なデータベースのテーブル設計を提案してください。Claude Code が以下のようなスキーマを提案してくれる。
CREATE TABLE pages ( id TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0);
CREATE TABLE likes ( uuid TEXT NOT NULL, page_id TEXT NOT NULL, PRIMARY KEY (uuid, page_id));pages: ページごとの「いいね数」を保持する集計テーブルlikes: 「誰が(uuid)どのページに(page_id)いいねしたか」の事実テーブル。複合主キー(uuid, page_id)が1ブラウザ1いいねの担保
この複合主キーを INSERT OR IGNORE と組み合わせると、SQLレベルで「初回だけ追加、2回目以降は無視」が一文で書ける。これが重複判定の核心パターン。
INSERT OR IGNORE — SQLite/D1 で「制約違反になる INSERT は静かに無視する」構文。
INSERT INTO ...でPK重複なら通常はエラーになるが、INSERT OR IGNORE INTO ...なら何も起きずに 0 行影響で終わる。今回は「既にいいね済み」を判定する用途で使う。
「pages.count を別に持つ理由」は、毎回 SELECT COUNT(*) FROM likes WHERE page_id = ? だと数が増えたとき重くなるため。POST 成功時だけ集計値を +1 して、GET を軽くする。
スキーマに納得したら、マイグレーションファイルを作る。
このスキーマでマイグレーションファイルを作成してください。ファイルは migrations/0001_init.sql に保存してください。7. 【バックエンド】API の設計と Pages Functions の実装
Section titled “7. 【バックエンド】API の設計と Pages Functions の実装”7-1. API 設計
Section titled “7-1. API 設計”REST 風にシンプルにまとめる。
エンドポイント
| メソッド | パス | 用途 |
|---|---|---|
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:リクエストボディは { "uuid": "..." }。レスポンスは
{ "count": 43, "liked": true }liked: true… 今回新規にいいねが追加されたliked: false… この UUID は既にこのページにいいね済みで、カウントは変わらない
CORS は別オリジンから呼ぶ前提なので Access-Control-Allow-Origin: * を付ける。
7-2. Pages Functions の実装
Section titled “7-2. Pages Functions の実装”Claude Code に依頼してファイルを作ってもらう。
Cloudflare Pages Functions で、以下の API を functions/api/like/[id].js に実装してください。
- GET: D1 の pages テーブルから id のカウントを取得して { count } を返す。レコードがなければ count: 0- POST: リクエストボディの uuid を受け取り、likes テーブルに (uuid, page_id) を INSERT OR IGNORE。 影響行数が 1 なら pages の count を +1(レコードがなければ INSERT)し、{ count: 最新値, liked: true } を返す。 影響行数が 0 なら { count: 既存値, liked: false } を返す。- CORS の Access-Control-Allow-Origin: * と OPTIONS への対応も入れる- D1 の binding 名は DB生成されたコードの動きを Claude Code に箇条書きで説明してもらってから、自分の手で一度はざっと読む。「いまどの SQL が走って、何を返しているか」が頭に入っていると、後でデバッグするときに楽。
8. GitHub Actions ワークフローの作成
Section titled “8. GitHub Actions ワークフローの作成”GitHubにpushすると、GitHub ActionsがD1マイグレーションとCloudflare Pagesデプロイを自動実行する。
ワークフローファイル .github/workflows/deploy.yml を作成。Claude Code に依頼する。
以下のテンプレートで .github/workflows/deploy.yml を作成してください。プロジェクト名は「mk-like-api」、データベース名は「mk-like-api-db」としてください。
---name: Deploy to Cloudflare Pages
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Apply D1 migrations uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} wranglerVersion: "4" command: d1 migrations apply データベース名 --remote
- name: Create Pages project (初回のみ) continue-on-error: true uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} wranglerVersion: "4" command: pages project create プロジェクト名 --production-branch=main
- name: Deploy to Cloudflare Pages uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} wranglerVersion: "4" command: pages deploy ./public --project-name=プロジェクト名---ワークフローの流れ:
pushをトリガーに、以下の順で実行される。
- コードを取得(
actions/checkout) — GitHubのファイルをCI環境に展開する - D1マイグレーションを適用(
d1 migrations apply --remote) — 未適用のSQLファイルを本番DBに適用する - Pagesプロジェクトを作成(
pages project create) — 初回のみ実行。2回目以降は失敗するがcontinue-on-error: trueで無視される - Pagesにデプロイ(
pages deploy) —public/のファイルとPages Functions(functions/)を本番環境に反映する
9. 初回デプロイ
Section titled “9. 初回デプロイ”push 前に .gitignore の設定。プロジェクトに作られる .wrangler/ フォルダ(ローカル状態のキャッシュ)はコミット不要なので追加しておく。
.gitignore に .wrangler/ を追加して続けて push する。Claude Code にプロンプトを渡す。
git add .git commit -m "initial"git push -u origin mainGitHub Actions がワークフローにそって、マイグレーション・プロジェクト作成・デプロイを自動実行する。
デプロイ完了後、ブラウザで https://mk-like-api.pages.dev/api/like/test を直接開いてみる。{"count":0} が返ってくれば API は動いている。
POST の動作確認は curl などで:
curl -X POST https://mk-like-api.pages.dev/api/like/test \ -H "Content-Type: application/json" \ -d '{"uuid":"test-uuid-1"}'{"count":1,"liked":true} が返ってきて、もう一度同じコマンドを叩くと {"count":1,"liked":false} になれば、UUID 重複判定が機能している。
10. ブログパーツとして使う
Section titled “10. ブログパーツとして使う”API が動いたら、次は自分のサイトに <script> で埋め込んで動かす。共通ロジックは window.likeApi として公開し、続編 LEX のブックマークレット・Chrome拡張機能でも再利用できるようにしておく。
10-1. UUID 発行と localStorage 保存
Section titled “10-1. 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 と違ってサーバーに自動送信されない。クリアしないと永続。容量は5MB前後。
10-2. pageId の正規化
Section titled “10-2. 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 したときにデバッグしやすい。
10-3. like.js を作る
Section titled “10-3. like.js を作る”public/like.js として配置する(Cloudflare Pages から配信される静的ファイル)。LEX のブックマークレットや Chrome 拡張機能でも同じファイルを再利用するので、共通ロジックは window.likeApi として公開しておく。
public/like.js を作って。以下の動作をする。
【共通ロジック(IIFE 内に閉じ込める)】- localStorage から like-uuid を取得。なければ crypto.randomUUID() で発行して保存- 現在ページのURLを正規化(hash と search を削除、末尾スラッシュを削除)して pageId にする- getCount(): pageId の API(https://mk-like-api.pages.dev/api/like/:pageId)に GET して { count } を返す- like(): 同じ API に uuid 付きで POST して { count, liked } を返す
【公開API】- window.likeApi = { getCount, like } として外から呼べるようにする
【ボタン自動バインド(ブログパーツ用途)】- DOMContentLoaded 後(または既に load 済みなら即時)、ページ内に <button id="like-button"> があれば - getCount() で取得した数字を「♥ 数字」の形でボタンに表示 - クリックで like() を呼び、レスポンスの count でボタンを更新- ボタンが無ければ何もしない(ブックマークレットから呼ばれる用途を想定)ブログパーツを使う側のHTMLには、以下を貼る:
<button id="like-button">♥</button><script src="https://mk-like-api.pages.dev/like.js"></script>これだけで、別ドメインのサイトからもこの API を使えるようになる。
ブックマークレットや Chrome 拡張機能から同じ API を呼ぶ方法は、続編 LEX(いいねカウントAPIをブックマークレットとChrome拡張機能から使う) を参照。
like.jsのwindow.likeApiをそのまま使い回す。
11. 担保の範囲と限界
Section titled “11. 担保の範囲と限界”正直に書く。
| ケース | 結果 |
|---|---|
| 普通のユーザー(同じブラウザ) | 1票のみ。サーバー側 INSERT OR IGNORE で担保 |
| localStorage を消去した人 | 新しい UUID が発行されるので再カウント可能 |
| プライベートブラウジング | localStorage がセッション限り。閉じると毎回新規 UUID |
| 別ブラウザ・別デバイス | それぞれ別人扱い(仕様通り) |
| 別ドメインに埋め込まれた同一人物 | オリジンごとに別 localStorage なので別人扱い |
| 開発者ツールで UUID を毎回変える攻撃者 | 防げない |
| 連打する攻撃者 | UUID が同じなら2回目以降は無視されるが、毎回違う UUID を送られたら防げない |
つまり、これは 「素直なユーザーが意図せず多重投票することを防ぐ」 装置であって、「攻撃者の不正投票を防ぐ」 ものではない。
個人ブログのいいねカウントとしては十分なレベル。本格的な投票システムや有料コンテンツ評価などには向かない。
12. プライバシー上の注意
Section titled “12. プライバシー上の注意”UUID は ユーザーごとに一意なランダム識別子 であり、扱い方によってはトラッキング目的にも使える。以下を守る:
- UUID をサーバー側のログに出さない。Cloudflare のリクエストログにも極力残さない
- UUID と他の情報(IPアドレス、User-Agent、参照元など)を組み合わせて保存しない
- 読み取り API(GET)では UUID を要求しない。POST だけで使う
- GDPR・改正個人情報保護法などを意識する文脈(EU圏ユーザーが多い、企業サイト等)ではプライバシーポリシーに明記する
「いいね数を集計する」だけが目的なら、UUID をDBに残す以上の処理は不要。
13. 発展課題
Section titled “13. 発展課題”サンプル実装としては十分だが、もう少し本格的にしたい場合の方向性:
- 認証付き化: GitHub OAuth や Cloudflare Access を組み合わせて、ログインユーザーごとに1票にする
- 連打防止: 同じ IP からの POST に Cloudflare のレート制限を入れる
- いいね取り消し:
DELETE /api/like/:idを追加して、likesから行を消し、pages.countを -1 - ハッシュベース pageId: URL をそのまま使うのが嫌なら SHA-256 でハッシュ化(短く・特殊文字なし・URLを隠せる)
- 動的アクセス制御: 特定オリジンからしか POST を受け付けないように
Originヘッダーをチェック - 集計の高度化: 日別・週別の集計、人気記事ランキングなど。
likes.created_atを追加して時系列クエリ
これらを少しずつ足していくと、本格的なリアクションシステムに育っていく。