Hugo のような静的サイトは、HTML を配信するだけならとても軽いのですが、
POST /api/... のような 小さい受付口 を足したくなることがあります。

例えば次のような用途です。

  • 記事末尾の軽いリアクション
  • お問い合わせの簡易受付
  • Slack への通知トリガー
  • ちょっとした webhook の受け口

このとき、サイト全体を動的構成へ寄せるほどではないが、POST /api/... だけは欲しい、という場面があります。
そういう用途にちょうどよいのが Cloudflare Worker です。

今回は、Hugo サイトに軽いリアクション導線を追加した実例をもとに、
Cloudflare Worker + D1 + Slack でサーバーレスな受付口を作る方法 を、実際のファイルと設定をそのまま出しながらまとめます。

Cloudflare Worker と D1 は何をするものか

まず役割を分けておきます。

Cloudflare Worker

Cloudflare Worker は、Cloudflare のエッジ上で動くサーバーレス実行環境です。
ざっくり言うと、Cloudflare 側に小さな API を置ける仕組み です。

今回の用途では、POST /api/like を Worker で受けます。

Cloudflare D1

Cloudflare D1 は、Cloudflare が提供している SQL データベースです。
SQLite に近い感覚で使え、Worker から直接読み書きできます。

今回の用途では、次のようなイベント記録を保存します。

  • どの記事に対するリアクションか
  • 同じ visitor がすでに送っていないか
  • いつ送られたか

Slack Incoming Webhook

通知先です。
新しいイベントがあったときだけ Slack に飛ばします。

つまり今回の分担はこうです。

  • Worker: リクエスト受付、入力検証、通知判定
  • D1: イベント保存、重複抑止
  • Slack: 新規イベントの通知先

もともとの配信構成

このサイトは Cloudflare Pages ではなく、かなり素朴な構成です。

  • Hugo で public/ を生成する
  • rsync で Ubuntu サーバーへ反映する
  • 本番は nginx で配信する
  • Cloudflare は前段の CDN / Proxy として使う

つまり、ページ本体は静的です。
そのうえで、/api/like だけを Cloudflare Worker へルーティングします。

今回の完成形

今回の構成はこうです。

  • フロント: Hugo 側でボタンを描画
  • ブラウザ: localStorage と Cookie に押下済み状態を保存
  • API: Cloudflare Worker
  • 保存先: Cloudflare D1
  • 通知先: Slack Incoming Webhook

データの流れは次の通りです。

  1. 記事ページでハートを押す
  2. ブラウザが POST /api/like を送る
  3. Worker が payload を検証する
  4. visitorId をハッシュ化して D1 に保存する
  5. 新規イベントなら Slack に通知する

Slack 受信イメージ

実際に使ったファイル

このリポジトリでは、次のファイルを使っています。

  • layouts/partials/article_like.html
  • layouts/partials/extend_footer.html
  • assets/css/extended/article-like.css
  • cloudflare/like-handler.js
  • cloudflare/likes-schema.sql
  • workers/like-api/src/index.js
  • workers/like-api/wrangler.jsonc

ここから実際の導入手順

ここからは、そのまま真似できる形で順番に書きます。

Tip
`wrangler` は Cloudflare Workers / D1 をローカルから操作するための公式 CLI です。この記事では、D1 の作成、secret の登録、Worker の deploy に使います。

1. D1 を作る

まず workers/like-api/ へ移動して wrangler を使える状態にします。

cd workers/like-api
npm install
npx wrangler d1 create araisun-like-db

実行すると、database_id が返ってきます。
この値を workers/like-api/wrangler.jsonc に入れます。

今回の設定はこうです。

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "araisun-like-api",
  "main": "src/index.js",
  "compatibility_date": "2026-04-21",
  "workers_dev": true,
  "d1_databases": [
    {
      "binding": "LIKES_DB",
      "database_name": "araisun-like-db",
      "database_id": "YOUR_DATABASE_ID"
    }
  ],
  "observability": {
    "enabled": true
  }
}

ポイントは binding 名です。
Worker 側コードでは env.LIKES_DB を参照しているので、ここは必ず LIKES_DB に合わせます。

Cloudflare Worker に D1 binding をつないだ画面

2. D1 にテーブルを作る

次に schema を流します。

cd workers/like-api
npx wrangler d1 execute araisun-like-db --remote --file=../../cloudflare/likes-schema.sql

使っている schema はこれです。

CREATE TABLE IF NOT EXISTS article_likes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  article_slug TEXT NOT NULL,
  article_title TEXT NOT NULL,
  article_url TEXT NOT NULL,
  visitor_hash TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(article_slug, visitor_hash)
);

UNIQUE(article_slug, visitor_hash) を置いているので、
同じ visitor が同じ記事へ何度も送っても重複しにくくなります。

3. Slack Incoming Webhook を secret に登録する

通知先の Webhook URL を Cloudflare の secret に入れます。

cd workers/like-api
npx wrangler secret put SLACK_LIKE_WEBHOOK_URL

入力欄には、コマンドではなく webhook URL 本体だけ を貼ります。

https://hooks.slack.com/services/XXXX/XXXX/XXXX

4. Worker を deploy する

cd workers/like-api
npx wrangler deploy

このとき workers.dev サブドメイン登録を聞かれても、
本番で araisun.com/api/like の Route だけ使うなら n で大丈夫です。

5. Cloudflare ダッシュボードで Route を設定する

ここが記事で省略されがちな部分ですが、実際にはここをやらないと動きません。

Cloudflare ダッシュボードで次の順に進みます。

  1. Workers & Pages を開く
  2. araisun-like-api を開く
  3. Settings または Triggers を開く
  4. RoutesAdd route を押す
  5. Route に araisun.com/api/like を入れる
  6. 対象 zone として araisun.com を選ぶ
  7. 保存する

これで、

  • HTML 本体は nginx
  • /api/like だけ Worker

という構成になります。

注意点として、対象ドメインは Cloudflare Proxy 配下である必要があります。

Cloudflare Worker の Settings で Route と Secret を確認した画面

6. Worker 側コード

今回の Worker 本体はかなり薄いです。

workers/like-api/src/index.js

import { handleLikeRequest, handlePreflight } from "../../../cloudflare/like-handler.js";

export default {
  async fetch(request, env) {
    if (request.method === "OPTIONS") {
      return handlePreflight();
    }

    if (request.method !== "POST") {
      return new Response("Method Not Allowed", {
        status: 405,
        headers: { Allow: "POST, OPTIONS" },
      });
    }

    return handleLikeRequest(request, env);
  },
};

本体ロジックは cloudflare/like-handler.js に寄せています。

7. Worker が実際にやっていること

cloudflare/like-handler.js では、次の処理をしています。

  • LIKES_DBSLACK_LIKE_WEBHOOK_URL の存在確認
  • articleSlug articleTitle articleUrl visitorId の検証
  • visitorId の SHA-256 ハッシュ化
  • D1 への INSERT OR IGNORE
  • 新規 insert のときだけ Slack 通知

実際の保存部分はこの形です。

const insertResult = await env.LIKES_DB.prepare(
  `INSERT OR IGNORE INTO article_likes (article_slug, article_title, article_url, visitor_hash)
   VALUES (?1, ?2, ?3, ?4)`
)
  .bind(articleSlug, articleTitle, articleUrl, visitorHash)
  .run();

8. Hugo 側のフロント実装

Hugo 側では、記事末尾にボタンを置いて POST /api/like を呼びます。

やっていることは次の程度です。

  • 記事末尾にハートボタンを差し込む
  • 押下済みならピンク表示にする
  • localStorage と Cookie に visitor ID を保存する
  • 押したら /api/like へ POST する

つまり、フロントは見た目と押下状態の保持、バックエンドは受付と保存です。

9. 動作確認

確認は次の順でやると分かりやすいです。

  1. 記事ページを開く
  2. ハートを押す
  3. ブラウザの NetworkPOST /api/like を確認する
  4. 200 が返ることを確認する
  5. Slack に通知が届くことを確認する

Response が次なら、新規保存まで成功しています。

{"ok":true,"liked":true,"inserted":true}

10. 導入中にハマりやすいところ

今回、実際に詰まった点もそのまま書いておきます。

compatibility_date を未来日にしない

未来日にすると deploy 時に

Can't set compatibility date in the future

で落ちます。
wrangler.jsonc には実行日以前の日付を入れます。

schema を流す前に叩かない

D1 にテーブルが無い状態だと

no such table: article_likes

になります。

Slack secret には URL 本体だけを入れる

コマンド文字列を貼ると

TypeError: Invalid URL

になります。

workers.dev は今回必須ではない

araisun.com/api/like の Route で使うだけなら、workers.dev の登録は不要です。

11. シークレットモードでは別人扱いになる

今回の visitor 判定はブラウザ側の保存に依存しているため、

  • シークレットモード
  • 別ブラウザ
  • 別端末

では別 visitor 扱いになります。

匿名のまま完全な「一人一回」にはなりませんが、
軽いリアクション導線としてはまず十分です。

必要なら今後、

  • IP ハッシュを併用する
  • User-Agent を組み合わせる
  • Turnstile を挟む

といった方向へ強化できます。

12. 無料枠でもかなり試しやすい

今回のような軽いイベント受付なら、無料枠でもかなり試しやすい のも大きいです。
2026年4月時点の Cloudflare 公式情報では、Workers Free は 1日 100,000 リクエスト、D1 Free は 1日 500万 rows read / 10万 rows written / 5GB storage が含まれています。
根拠:

個人ブログのリアクション受付や簡易 webhook 用途なら、まずは無料の範囲で十分回しやすいと思います。

まとめ

Cloudflare Worker を使うと、静的サイトでも POST /api/... のようなサーバーレス受付口を後付けできます。

今回のように

  • 受付は Worker
  • 保存は D1
  • 通知は Slack

と分けると、既存の Hugo + nginx 構成を崩さずに導入できます。

静的サイトに小さな動的機能を足したいなら、かなり扱いやすい選択肢です。

関連記事

参考