zudo-paper

Slack Bot + Claude API + Cloudflare Workers で CMS なしでウェブサイト更新する仕組みを作った

Author: Takazudo | 作成: 2026/02/24

概要

妹がpinsel-germany.comというドイツの美大受験準備をやっていて、そのWebサイトはTakazudoの方で作ったものが使われている。元々Gatsby + Prismic Headless CMSで作っていたのだが、Next.jsのstatic exportで作り直すことにした。(まだ途中だけど)

問題は、開発者ではない妹がサイトのコンテンツをどうやって更新するかということ。従来のアプローチならヘッドレスCMSをセットアップして使い方を教える、という流れになり、今回もそのあたりを自分で作っちゃうかなどと考えていた。Claude Codeもあるしとか。

でも考えていたらめんどくさくなってきて、今回はSlack botを作って自然言語でサイト更新出来るんじゃないかという気がしたので試した。妹がSlackで日本語で「FAQに質問を追加して」みたいに書くと、botがClaude APIで意味を解釈して、GitHub APIでリポジトリのファイルを編集して、自動デプロイされる、という仕組み。そのまとめ。

背景

従来のアプローチ

非開発者がウェブサイトのコンテンツを更新する場合、だいたい以下のような流れになる。

  • Prismic、Contentful等のヘッドレスCMSを導入する
  • CMSをちゃんとセットアップする
  • サイトオーナーにCMSの使い方を教える

これはこれで定番のアプローチなのだが、CMSのセットアップも運用もそれなりにコストがかかる。

今回のアプローチ

CMSの代わりに、Slack botを作った。

  • 自然言語を理解するSlack botを作る
  • botはClaude APIでリクエストを解釈する
  • botはGitHub APIでリポジトリのファイルを直接編集する
  • Cloudflare Workersでbotをホストする
  • GitHub pushをトリガーにCloudflare Pagesが自動デプロイする

妹はSlackでチャットするだけ。CMSのUIを覚える必要がない。

結果

実際に動いている様子がこれ。

編集前のウェブサイト

これが編集前のウェブサイト。ここからSlack botに変更を依頼する。

Slack botとのチャット画面

Slackのスレッドで普通にチャットする感覚でサイトの更新ができる。botが変更案を提示して、確認後にコミットしてくれる。コミットURLも返してくれる。

GitHub上のコミット差分

GitHubで見ると、botが作ったコミットの差分がちゃんと確認できる。コミットされたらCloudflare Pagesが自動でビルド・デプロイするので、数分後にはサイトに反映される。

各APIの役割

Botを構成する4つのAPIの役割分担。

flowchart TD subgraph "Slack Bot" S1["Events APIでメッセージ受信"] S2["署名検証(HMAC-SHA256)"] S3["ファイルダウンロード"] S4["スレッドに返信"] end subgraph "Claude API" C1["自然言語リクエストを解釈"] C2["ツール呼び出しを判断"] C3["ツールユースループ"] end subgraph "GitHub REST API" G1["Contents API: ファイル読み書き"] G2["Git Data API: 一括操作"] G3["画像コミット(base64)"] end subgraph "Cloudflare Workers" W1["サーバーレスホスティング"] W2["ctx.waitUntil()"] W3["KVで会話履歴保存"] end S1 --> W1 W1 --> C1 C2 --> G1 C2 --> G2 C2 --> G3 W2 --> S4

Slack Bot

  • Slack Events APIでメッセージを受信する
  • リクエスト署名の検証(HMAC-SHA256)
  • URLベリフィケーションチャレンジのハンドリング(セットアップ時)
  • Slack file APIでアップロードされた画像をダウンロードする
  • chat.postMessageでスレッドに返信する

Claude API

  • 会話履歴とツール定義を受け取る
  • 日本語の自然言語リクエストを解釈する
  • どのGitHub操作を実行するか判断する
  • ツールユースループ: Claudeがツールを呼ぶ → Workerが実行 → 結果をClaudeに返す → Claudeが続行
  • システムプロンプトに「コンテンツマップ」を含めて、編集可能なファイルの一覧をClaudeに事前に伝えている

GitHub REST API

  • Contents APIで単一ファイルの読み取り・更新
  • Git Data APIで複数ファイルの一括操作
  • 画像のコミットもContents APIのPUTでbase64エンコードしたデータを送る

Cloudflare Workers

  • サーバーレス関数としてbotをホストする
  • ctx.waitUntil()でSlackに200を即座に返しつつバックグラウンドで処理を継続
  • KVで会話履歴を保存(Slackスレッドのthread_tsをキーに、24h TTL)
  • レートリミット(1ユーザーあたり30リクエスト/日)
  • wrangler secret putでシークレット管理

アーキテクチャ

システム構成

各コンポーネントの関係はこんな感じ。

flowchart LR subgraph Slack A[妹がメッセージ送信] G[Botがスレッドに返信] end subgraph Cloudflare B[Worker] C[KV Store] end subgraph External APIs D[Claude API] E[GitHub API] end subgraph Deployment F[Cloudflare Pages] end A --> B B <--> C B <--> D B <--> E B --> G E --> F

全体のフロー

処理の流れはこんな感じ。

sequenceDiagram participant Yuko as 妹(Slack) participant Worker as Cloudflare Worker participant KV as Cloudflare KV participant Claude as Claude API participant GitHub as GitHub API participant CF as Cloudflare Pages Yuko->>Worker: Slackでメッセージ送信 Worker-->>Yuko: 200 OK(即座に返す) Worker->>KV: 会話履歴をロード KV-->>Worker: 過去のメッセージ Worker->>Claude: messages[]とツール定義を送信 Claude-->>Worker: tool call: read_file Worker->>GitHub: GET /contents/path GitHub-->>Worker: ファイル内容 Worker->>Claude: ツール実行結果 Claude-->>Worker: tool call: update_file Worker->>GitHub: PUT /contents/path GitHub-->>Worker: コミット作成 Worker->>Claude: ツール実行結果 Claude-->>Worker: 最終テキスト Worker->>KV: 会話履歴を保存 Worker->>Yuko: スレッドに返信 GitHub->>CF: Webhookでデプロイ開始 CF->>CF: ビルド&公開

Claudeとの連携方法の選択

Claude連携のアプローチは2つ検討した。

flowchart TD A{Claude連携方法} B["Claude API + Tool Use"] C["Claude Code Agent SDK"] B1["✅ サーバーレスで動く"] B2["✅ HTTP APIコールのみ"] B3["✅ シンプル"] C1["❌ 長時間動くサーバーが必要"] C2["❌ ファイルシステムが必要"] C3["❌ コンテンツ更新にはオーバーキル"] A --> B A --> C B --> B1 B --> B2 B --> B3 C --> C1 C --> C2 C --> C3
  • Claude API + Tool Use: ツール(read_fileupdate_file等)を定義して、Claudeに判断させる。シンプルでサーバーレスで動く
  • Claude Code Agent SDK: フルのClaude Codeサブプロセスを起動して、ファイルシステムにアクセスさせる。高機能だが長時間動くサーバーが必要

今回は前者を採用した。理由は、GitHubの操作がすべて純粋なHTTP APIコールで完結するから。ファイルシステムが不要なので、サーバーレスのWorkersで問題なく動く。コンテンツ更新程度の用途にClaude Code Agent SDKはオーバーキル。

画像のアップロード

Slackに画像がアップロードされた場合の処理も、ファイルシステム不要で動く。

sequenceDiagram participant Yuko as 妹(Slack) participant Worker as Cloudflare Worker participant Slack as Slack API participant Claude as Claude API participant GitHub as GitHub API Yuko->>Worker: 画像をアップロード Worker->>Slack: url_privateで画像をダウンロード Slack-->>Worker: 画像バイナリ Worker->>Worker: メモリ上でbase64エンコード Worker->>Claude: 画像とテキストを送信 Claude-->>Worker: tool call: upload_image Worker->>GitHub: PUT /contents(base64) GitHub-->>Worker: コミット作成 Worker->>Yuko: 完了+コミットURL
  1. Workerがurl_private + botトークンでSlackから画像をダウンロード
  2. メモリ上でbase64エンコード
  3. GitHub Contents APIのPUTでbase64コンテンツとしてコミット

全部メモリ上で完結する。

マルチターン会話の仕組み

Claude APIはステートレスなので、毎回の呼び出しで会話履歴の全体(messages[]配列)を送信する必要がある。Workerはこの会話履歴をCloudflare KVに保存していて、Slackスレッドのthread_tsをキーにしている。スレッドごとに1つの会話。24h TTLで自動クリーンアップ。

stateDiagram-v2 [*] --> 待機中 待機中 --> メッセージ受信: Slackメッセージ到着 メッセージ受信 --> 履歴ロード: KVから会話履歴をロード 履歴ロード --> Claude呼び出し: messages[]を送信 Claude呼び出し --> ツール実行: tool_useレスポンス ツール実行 --> Claude呼び出し: ツール結果を返す Claude呼び出し --> 返信: テキストレスポンス 返信 --> 履歴保存: KVに会話履歴を保存 履歴保存 --> Slack返信: スレッドに投稿 Slack返信 --> 待機中

これはハックでもなんでもなくて、ChatGPTやその他のチャットLLM UIも裏ではまったく同じことをやっている。

Cloudflareを選んだ理由

元々サイトはNetlifyにホストしていたが、botのホスト先としてCloudflare Workersを選んだ。

flowchart LR subgraph "Cloudflare($5/月)" W[Workers] KV[KV Store] P[Pages] end W --- KV W --- P subgraph 機能 A["コールドスタート ≈ 0ms"] B["ctx.waitUntil()"] C["帯域無制限"] end W --> A W --> B P --> C
  • NetlifyのBackground FunctionsはProプラン($19/月)が必要
  • Cloudflare Workers Paidは$5/月
  • Cloudflare Workersはコールドスタートがほぼ0ms
  • ctx.waitUntil()パターンがSlack botに最適(即座に200を返してバックグラウンド処理)
  • KVストレージが有料プランに含まれている
  • サイトホスティング + bot + KVが全部1つのプラットフォームにまとまる

ということで、サイトのホスティングもNetlifyからCloudflare Pagesに移行した。

コンテンツマップの最適化

重要な最適化として、システムプロンプトに静的な「コンテンツマップ」を含めている。Claudeにリポジトリを毎回探索させると、list_filesの呼び出しでトークンを消費してしまう。コンテンツマップがあれば、Claudeは最初からどのファイルが何を含んでいるか知っているので、直接目的のファイルにアクセスできる。

flowchart LR subgraph "コンテンツマップなし" A1["Claudeがlist_files呼び出し"] --> A2["ディレクトリ探索"] A2 --> A3["目的のファイルを発見"] A3 --> A4["read_file"] A4 --> A5["update_file"] end subgraph "コンテンツマップあり" B1["Claudeがマップを参照"] --> B2["read_file"] B2 --> B3["update_file"] end
## Data files (TypeScript)
- data/faq-data.ts — FAQ Q&A (4 categories)
- data/voices-data.ts — Student testimonials
- data/results-data.ts — Admission results by year

## Notes (MDX)
- src/notes/what-is-mappe.mdx — マッペとは
- src/notes/german-language.mdx — ドイツ語について
...

こういうマップをシステムプロンプトに入れておくことで、list_filesの呼び出しを省略できる。

エディタモードの導入

実際に使ってみて気づいたのが、Claudeは小さな変更を頼まれたときに、周囲のテキストまで「改善」しようとする傾向があること。「この1行だけ直して」と頼んでも、前後の文章を書き直したりする。

なので、システムプロンプトに「エディタモード」のルールを追加した。

  • 依頼された変更だけを行う
  • 周囲のテキストを勝手に修正・改善・書き換えない
  • 何かおかしい点を見つけたら、編集せずに指摘だけする
  • 変更案を提示して、確認を得てからコミットする

料金

Claude API — 初回$40 + 従量課金

Claude APIを使うにはAnthropicのAPIクレジットを購入する必要がある。最低$5からチャージできるが、実用上はTier 2以上が必要。Tier 1だと1分あたり30Kの入力トークン制限があり、会話履歴を含めるとすぐに429 rate limitに引っかかる。

Tier 2には累計$40以上のクレジット購入が条件。これは月額ではなく一度きりの購入で、到達すると即座にTier 2に昇格する。購入したクレジットはそのまま残高としてAPI利用に使える。

Tier累計購入額月間利用上限Sonnet ITPM
Tier 1$5$10030,000
Tier 2$40$500450,000
Tier 3$200$1,000800,000
Tier 4$400$5,0002,000,000

モデルはclaude-sonnet-4-6を使っている。コンテンツ更新程度ならSonnetで十分。トークン単価は入力$3/1Mトークン、出力$15/1Mトークン。週数回の更新程度の軽い使用量なら実際の消費は月$1-5程度で済む。

Cloudflare Workers — $5/月

Cloudflare Workersの無料プランだとCPU時間が10msまでで、Claude APIの応答待ちを含むと全然足りない。Workers Standard($5/月)にアップグレードする必要がある。有料プランにはKVストレージも含まれている。Cloudflare Pagesは無料枠で帯域無制限。

その他 — 無料

サービスコスト備考
Slack$0無料プランで十分
GitHub API$0認証リクエストは無料

合計

初回にClaude APIクレジット$40 + Cloudflare Workers $5/月が必要。Claude APIの$40は一度きりの購入で、残高として消費されていく。ランニングコストとしてはCloudflare $5/月 + APIトークン消費$1-5/月程度。

まとめ

テキトーに指示すればやってくれるつもりなので、CMS無しで色々やってもらうには良い方法なのかもしんない。ただ自分で使うかと言われるとAPI料金になるのでやらないかも。うまいこと整備できればサービス的に提供するのも可能かなーという印象。

今後やること

トークン使用料が気になる。無駄にリポジトリを探索されると財布が痛いので、どこのページはどれいじるとかそういうCLAUDE.mdやスキル等を整備する必要があるかも。他しっかりやるならいじるサイト用コンテンツをまとめたリポジトリとして改めてそこだけで作るとか(不要なドキュメントやこのボット関連のものは除外する等)。

うまくいくのかは微妙に謎。ちょっとしか試してないけど関係無い所も併せて直されてしまったりしていた。単純に日本語の間違いだったので良いっちゃよいがAIっぽい。非開発者向けを意識した調整が必要かも。例えばテキストはなるべくコンポーネントの奥に隠さずにMDXファイル内にまとめるようにするとかそういう。