いいねカウント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. 何を作るか

ページごとの「いいね数」を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. 事前準備

2-1. GitHubでリポジトリを作成

GitHubアカウントの作成や gh のセットアップはGit と GitHub の基本を参照のこと。

GitHub → New repository → リポジトリ名を mk-like-api で作成。Private にしておく(意図しないファイルの公開を防ぐため)。

名前は別のものでもよい。以降 mk-like-api と出てきたら自分のリポジトリ名に読み替える。

2-2. Account ID 確認・APIトークン作成・Secrets 登録

Cloudflareアカウントがなければ作る(こちらの手順(1-1))。

GitHub ActionsでCloudflare自動デプロイを設定する2章「事前準備」 を参照して、Account IDの確認・APIトークンの作成・GitHubへの Secrets 登録を行う。完了したらここに戻る。

2-3. リポジトリを clone する

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

terminal
mkdir -p ~/Desktop/claude
cd ~/Desktop/claude
git clone [email protected]:ユーザー名/mk-like-api.git

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

2-4. Claude Code を起動

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

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

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

3. Wrangler ログイン状態の確認

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

terminal / prompt
npx wrangler whoami

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

4. wrangler.toml の作成

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

prompt
以下のテンプレートで 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 データベースを作成(初回のみ)

ターミナルで実行するか、Claude Code にプロンプトとして渡す。

terminal / prompt
npx wrangler d1 create mk-like-api-db

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

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

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

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

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

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

prompt
いいねカウント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)
);

この複合主キーを 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 を軽くする。

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

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

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

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 }

CORS は別オリジンから呼ぶ前提なので Access-Control-Allow-Origin: * を付ける。

7-2. Pages Functions の実装

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

prompt
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 ワークフローの作成

GitHubにpushすると、GitHub ActionsがD1マイグレーションとCloudflare Pagesデプロイを自動実行する。

ワークフローファイル .github/workflows/deploy.yml を作成。Claude Code に依頼する。

prompt
以下のテンプレートで .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/)を本番環境に反映する

9. 初回デプロイ

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

prompt
.gitignore に .wrangler/ を追加して

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

prompt
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 / prompt
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. ブログパーツとして使う

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

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.comblog-b.com では別の UUID になる点に注意。

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

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 として使うので、pageIdhttps://example.com/post1 のような 読める文字列 になる。D1 で SELECT * FROM pages したときにデバッグしやすい。

10-3. like.js を作る

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

prompt
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 をそのまま使い回す。

11. 担保の範囲と限界

正直に書く。

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

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

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

12. プライバシー上の注意

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

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

13. 発展課題

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

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


2026-05-21 (last updated: 2026-05-24) タツヲ (yto)