コンテンツにスキップ

いいねカウント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アプリ) を一通り体験している前提で進める。

ページごとの「いいね数」を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章 担保の範囲と限界 で正直に説明する。

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 登録を行う。完了したらここに戻る。

ターミナル(Claudeデスクトップアプリ右上の ビューメニュー → ターミナル でもよい)で以下を実行する。

Terminal window
mkdir -p ~/Desktop/claude
cd ~/Desktop/claude
git clone git@github.com:ユーザー名/mk-like-api.git

GitHub への接続がまだの場合はGit と GitHub の基本を参照。

Claudeデスクトップアプリを起動。

Code(Claude Code)を選択 → New session をクリック → 作業フォルダを指定(~/Desktop/claude/mk-like-api

公開ファイルは public/ 配下、サーバー側のAPI実装は functions/api/ 配下、設定ファイルはルート直下に置く構成にする。

Wrangler ハンズオンを完了している場合、Node.jsとWranglerログインは済んでいるはず。下記コマンドでログイン状態を確認する。

Terminal window
npx wrangler whoami

アカウント名やメールアドレスが表示されればOK。表示されない場合はWranglerハンズオンの2章を参照してインストール・ログインする。

D1データベースとPages Functionsを接続するための設定ファイル。Claude Code に作成を依頼する。

Claudeへの指示
以下のテンプレートで 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 にプロンプトとして渡す。

Terminal window
npx wrangler d1 create mk-like-api-db

「既に存在しています」エラーが出たら、別名(例:mk-like-api-db2)で作り直す。wrangler.tomldatabase_name も合わせて書き換える。

実行すると database_id が表示される。Claude Code に伝えて wrangler.toml を書き換えてもらう。

Claudeへの指示
wrangler.toml の database_id を「xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx」に書き換えてください。

Database ID(database_id — データベースの識別子。仮に外部に漏れても、API Tokenがなければ操作できないため問題なし。

6. 【データベース】D1 スキーマとマイグレーション

Section titled “6. 【データベース】D1 スキーマとマイグレーション”

Claude Code にアプリの仕様を伝えてテーブル設計を相談する。

Claudeへの指示
いいねカウント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 を軽くする。

スキーマに納得したら、マイグレーションファイルを作る。

Claudeへの指示
このスキーマでマイグレーションファイルを作成してください。
ファイルは migrations/0001_init.sql に保存してください。

7. 【バックエンド】API の設計と Pages Functions の実装

Section titled “7. 【バックエンド】API の設計と Pages Functions の実装”

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: * を付ける。

Claude Code に依頼してファイルを作ってもらう。

Claudeへの指示
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をトリガーに、以下の順で実行される。

  1. コードを取得actions/checkout) — GitHubのファイルをCI環境に展開する
  2. D1マイグレーションを適用d1 migrations apply --remote) — 未適用のSQLファイルを本番DBに適用する
  3. Pagesプロジェクトを作成pages project create) — 初回のみ実行。2回目以降は失敗するが continue-on-error: true で無視される
  4. Pagesにデプロイpages deploy) — public/ のファイルとPages Functions(functions/)を本番環境に反映する

push 前に .gitignore の設定。プロジェクトに作られる .wrangler/ フォルダ(ローカル状態のキャッシュ)はコミット不要なので追加しておく。

Claudeへの指示
.gitignore に .wrangler/ を追加して

続けて push する。Claude Code にプロンプトを渡す。

Claudeへの指示
git add .
git commit -m "initial"
git push -u origin main

GitHub Actions がワークフローにそって、マイグレーション・プロジェクト作成・デプロイを自動実行する。

デプロイ完了後、ブラウザで https://mk-like-api.pages.dev/api/like/test を直接開いてみる。{"count":0} が返ってくれば API は動いている。

POST の動作確認は curl などで:

Terminal window
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 重複判定が機能している。

API が動いたら、次は自分のサイトに <script> で埋め込んで動かす。共通ロジックは window.likeApi として公開し、続編 LEX のブックマークレット・Chrome拡張機能でも再利用できるようにしておく。

crypto.randomUUID() でランダムな UUID(v4)を作る。これがこのブラウザの「ローカル識別子」になる。

let uuid = localStorage.getItem('like-uuid');
if (!uuid) {
uuid = crypto.randomUUID();
localStorage.setItem('like-uuid', uuid);
}

初回アクセス時にだけ生成、以降は同じものを使い回す。

localStorageオリジン単位(スキーム + ホスト + ポート)で永続化される。同じ人でも blog-a.comblog-b.com では別の UUID になる点に注意。

localStorage — ブラウザに文字列で値を保存できる仕組み。Cookie と違ってサーバーに自動送信されない。クリアしないと永続。容量は5MB前後。

「いいねの集計単位」をどう決めるか。本記事では 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/post1SNSのトラッキング流入を別カウントしない
末尾 / を削除/post1//post1/post1/post1/ を同一視

ホスト名はそのまま残る(URL オブジェクトが自動で小文字化する)。

本格的な用途だと ?p=2 のようなページネーション用パラメータも別カウントしたい場合があり、許可リスト方式の正規化が必要。本記事のサンプル実装では割り切って一律削除する。

このまま URL を pageId として使うので、pageIdhttps://example.com/post1 のような 読める文字列 になる。D1 で SELECT * FROM pages したときにデバッグしやすい。

public/like.js として配置する(Cloudflare Pages から配信される静的ファイル)。LEX のブックマークレットや Chrome 拡張機能でも同じファイルを再利用するので、共通ロジックは window.likeApi として公開しておく。

Claudeへの指示
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.jswindow.likeApi をそのまま使い回す。

正直に書く。

ケース結果
普通のユーザー(同じブラウザ)1票のみ。サーバー側 INSERT OR IGNORE で担保
localStorage を消去した人新しい UUID が発行されるので再カウント可能
プライベートブラウジングlocalStorage がセッション限り。閉じると毎回新規 UUID
別ブラウザ・別デバイスそれぞれ別人扱い(仕様通り)
別ドメインに埋め込まれた同一人物オリジンごとに別 localStorage なので別人扱い
開発者ツールで UUID を毎回変える攻撃者防げない
連打する攻撃者UUID が同じなら2回目以降は無視されるが、毎回違う UUID を送られたら防げない

つまり、これは 「素直なユーザーが意図せず多重投票することを防ぐ」 装置であって、「攻撃者の不正投票を防ぐ」 ものではない。

個人ブログのいいねカウントとしては十分なレベル。本格的な投票システムや有料コンテンツ評価などには向かない。

UUID は ユーザーごとに一意なランダム識別子 であり、扱い方によってはトラッキング目的にも使える。以下を守る:

  • UUID をサーバー側のログに出さない。Cloudflare のリクエストログにも極力残さない
  • UUID と他の情報(IPアドレス、User-Agent、参照元など)を組み合わせて保存しない
  • 読み取り API(GET)では UUID を要求しない。POST だけで使う
  • GDPR・改正個人情報保護法などを意識する文脈(EU圏ユーザーが多い、企業サイト等)ではプライバシーポリシーに明記する

「いいね数を集計する」だけが目的なら、UUID をDBに残す以上の処理は不要。

サンプル実装としては十分だが、もう少し本格的にしたい場合の方向性:

  • 認証付き化: 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 を追加して時系列クエリ

これらを少しずつ足していくと、本格的なリアクションシステムに育っていく。