SiteOps 導入ガイド

SiteOpsは、既存の会社サイトに記事管理、問い合わせフォーム、問い合わせ対応を追加するツールです。本体は自社のCloudflareアカウント上のWorker・D1・R2で動作し、既存サイトの表示、URL、ホスティングは変更せずに使えます。

SiteOps専用環境Worker・D1・R2・認証情報を管理
既存サイト表示・URL・ホスティングは変更なし
接続方式公開HTTPS APIで接続。既存サイトへのコード生成なし

導入後にできること

管理画面から記事と画像を公開・更新できる
公開した記事が、設定した反映方式で既存サイトへ反映される
フォームの送信内容が問い合わせ一覧へ保存される
既存サイトの表示、URL、ホスティングは従来のまま
導入後の管理画面。記事、フォーム、画像、問い合わせの状態を確認できます。

1. 事前準備

導入を始める前に、次のものを用意します。

CloudflareアカウントSiteOps本体(Worker、D1、R2)の設置先です。デプロイ時にブラウザでログイン承認を行います。
Node.js 20以上node -v でバージョンを確認します。
SiteOps配布ファイルリリースと一緒に受け取ります(例: siteops-0.8.0.tgz)。vendor/siteops.tgz の名前で配置し、リリースに記載されたSHA-512値と照合して、改ざんされていないことを確かめてから使います。
対応する既存サイトHTTPSで公開されているサイト。フレームワークを問わず、公開APIを呼び出せるサーバー側処理またはビルド処理があれば接続できます。

手元に揃えておく情報

会社名と管理者メールアドレス
既存サイトの公開URL
記事の反映方式(live / webhook / manual)
既存サイトのデプロイ手順

ディレクトリ構成

SiteOps専用フォルダは任意の場所に作成できます。既存サイトのフォルダとは完全に独立しており、位置関係の制約は「既存サイトのプロジェクト内には作らないこと」の1つです。配布ファイルは次の配置を前提にコマンドを記載します。

work/                    ← 任意の作業フォルダ(場所は自由)
├─ vendor/
│   └─ siteops.tgz       ← 受け取った配布ファイルをこの名前で配置
└─ company-siteops/      ← これから作成するSiteOps専用フォルダ

2. セットアップ

SiteOps専用プロジェクトを作成します。実行場所は配布ファイル(vendor/)を置いた作業フォルダで、既存サイトのフォルダとは無関係です。

配布ファイルの検証

リリースに記載されたSHA-512値と、次のコマンドの出力を照合します。

shasum -a 512 vendor/siteops.tgz
# Windows (PowerShell) — リリースノートの値を貼り付けると True/False で判定できます:
(Get-FileHash vendor\siteops.tgz -Algorithm SHA512).Hash -eq "<リリースノートのSHA-512>"

セットアップの実行

実行場所: vendor/ を置いた作業フォルダ
mkdir company-siteops
cd company-siteops
npm init -y
npm install ../vendor/siteops.tgz
npx siteops setup .
1

会社名管理画面に表示する名称

2

管理者メール初期管理者の連絡先

# 対話できない環境では2項目を引数で渡します
npx siteops setup . --yes --name "株式会社サンプル" --admin-email admin@example.com
管理者トークンの保存

セットアップ完了時に so_admin_ で始まる管理者トークンが1回だけ表示されます。管理画面へのログインに使うため、パスワードマネージャーへ保存してください。控えは .siteops/credentials.json(Git対象外)にあり、紛失時は npx siteops auth reset で再発行できます。

SiteOps専用プロジェクトに作成されるもの

ファイル役割Gitでの扱い
siteops.config.json会社名、SiteOps ID、各アダプター設定。公開サイトURLは接続時に追加コミット
package.jsonとlockfilesiteops依存と、siteops:deploy など siteops:* 運用コマンド(npm scripts)を追加コミット
.gitignore.siteops/をGit対象から除外コミット
.siteops/管理者トークン、Worker Secret、デプロイ状態、ローカル接続先Git対象外
補足

npm run siteops:* はセットアップがpackage.jsonへ追加する運用コマンドで、npx siteops * と同じ処理を実行します。以後のコマンドはSiteOps専用フォルダで実行します。現在の進捗と次のコマンドは npm run siteops:status で確認できます。

3. デプロイ

SiteOps本体(Worker、D1、R2、管理画面)を会社のCloudflareアカウントへ配置します。実行場所はSiteOps専用フォルダ(company-siteops/)です。

Cloudflareログイン

npm run siteops:deploy は未ログインの場合にブラウザのログイン画面(wrangler login)を自動で開きます。SiteOpsを置くアカウントを選んで承認してください。CI・自動化などブラウザを開けない環境では、Cloudflareダッシュボードで作成したAPIトークンを環境変数 CLOUDFLARE_API_TOKEN に設定します。認証エラー時にwranglerが提案する --temporary は一時アカウント向けのため、会社アカウントへの配置では選ばず、上記いずれかの認証を用意してください。デプロイ前には確認プロンプトが表示されるため、対話できない環境での実行では npm run siteops:deploy -- --yes のように --yes を付けます。

# 実行場所: company-siteops/
npm run siteops:deploy

成功すると SiteOps deployed、管理画面URL(Administration URL)、サイトデータの初期化確認が表示されます。初期化まで完了すると公開APIをすぐに利用できます。表示されたURL(SiteOps URLの /login)を開き、保存した管理者トークンでログインできることを確認します。

途中で止まった場合は npm run siteops:status が現在の進捗と次に実行するコマンドを表示します。

4. 既存サイトへの接続

SiteOpsと既存サイトの間に共有コードはなく、公開APIで接続します。接続情報の取得、公開URLの登録、既存サイト側の実装、の順に進めます。

  1. 接続情報を取得npx siteops website manifest --json がSiteOps URL、SiteOps ID(APIパスの {siteKey})、記事・画像・フォームAPIのURLを返します。ブラウザへ渡るのは公開情報に限られます。
  2. 公開URLと反映方式を登録npx siteops connect live --origin で登録し、もう一度 npm run siteops:deploy で反映します。方式は3つ: リクエスト時に表示するSSR・動的サイトは live、ビルド時にHTMLを生成する静的サイトは webhook、承認工程を挟む運用は manual(反映方式の詳細)。ビルドフックを用意できない検証環境や移行中は manual で開始し、後から connect webhook の再実行と再デプロイで切り替えられます。検証環境では http://127.0.0.1:8788 のようなURLも登録できます。成功すると反映方式と許可オリジン(Allowed origins)が表示されます。
  3. 既存サイトへ実装下記の公開APIを既存サイトのデータ取得処理とフォームへ組み込みます。実装を終えたら既存サイトを普段の手順でデプロイし、「5. 動作確認」へ進みます。
# 実行場所: company-siteops/
npx siteops website manifest --json                             # 手順1
npx siteops connect live --origin "https://www.example.com"     # 手順2
npx siteops doctor                                              # 設定の点検(任意)
npm run siteops:deploy                                          # 手順2の反映
# 手順3: 下記の実装 → 既存サイトを普段の手順でデプロイ
npm run siteops:verify

https://www.example.com は実際の公開URLへ置き換えます。静的サイトの反映方式は記事の反映方式、メールを使う場合はメール接続を参照してください。

公開APIエンドポイント

website manifest --json が実際のURLへ展開した一覧を返します。管理トークンを使わず、公開済みデータとフォーム受付に接続できます。

記事一覧GET /api/public/sites/{siteKey}/content/{contentType}/entries — 公開済みの記事を {entries} で返します。
記事詳細GET /api/public/sites/{siteKey}/content/{contentType}/entries/{slug} — 1件を {entry} で返します。下書きは公開APIへ出ません。
画像GET /api/public/sites/{siteKey}/assets/{key} — 公開画像を返します。imgのsrcへそのまま使えます。
フォーム保護設定GET /api/public/sites/{siteKey}/forms/security — Turnstileの公開キーとactionを返します。
フォーム送信POST /api/public/sites/{siteKey}/forms/{formKey}/submissions — 検証と保存を行い {accepted: true} を返します。

セットアップ直後から使える既定リソース

コンテンツタイプとフォームは、次のものが最初から用意されています。管理画面で項目の追加・変更ができます。

種類キー内容
コンテンツタイプnewsお知らせ。項目: title(必須) / displayDate / category / excerpt / body / externalUrl / important / seoTitle / seoDescription
コンテンツタイプcolumns / interviews / pagesコラム / インタビュー / 固定ページ
フォームcontactお問い合わせ。受付項目: name(必須) / email(必須) / company / message(必須)
フォームpartner代理店・協力会社からの問い合わせ。受付項目: company(必須) / name(必須) / email(必須) / phone / businessType / message(必須)

記事エントリの構造

一覧は {entries: [...]} 、詳細は {entry: {...}} を返します。既定の編集画面では本文(body)はプレーンテキスト(段落は空行区切り)で保存されるため、HTMLへの変換はサイト側で行います。

{
  "entries": [
    {
      "slug": "sakura-no-ie-open-house",
      "publishedAt": "2026-07-17T03:25:00.000Z",
      "updatedAt": "2026-07-17T03:25:00.000Z",
      "data": {
        "title": "新モデルハウス完成見学会",
        "displayDate": "2026-07-20",
        "category": "イベント",
        "excerpt": "一覧や検索結果に見せる概要",
        "body": "本文はプレーンテキストで返ります。\n\n段落は空行で区切られます。",
        "seoTitle": "SEOタイトル(任意)",
        "seoDescription": "SEO説明(任意)"
      }
    }
  ]
}

記事一覧と記事詳細の実装

サーバー側(SSR、ビルド処理)で公開APIを取得し、title、meta description、canonical、構造化データを既存サイトのHTMLへ出力します。"news"はコンテンツタイプ(記事の種類を表すID)で、管理画面の記事設定と一致させます。

const SITEOPS_URL = "https://siteops-xxxxx.workers.dev"; // manifestのSITEOPS_URL
const SITE_KEY = "main";                                  // manifestのSITEOPS_SITE_KEY
const api = SITEOPS_URL + "/api/public/sites/" + SITE_KEY;

const list = await fetch(api + "/content/news/entries");
const { entries } = await list.json();

const detail = await fetch(api + "/content/news/entries/" + slug);
if (detail.status === 404) {
  // 既存サイトの404処理へ接続
}
const { entry } = await detail.json();

問い合わせフォームの実装

フォームはブラウザから公開APIへ直接送信します。許可オリジンの検証、レート制限、サイズ上限、Turnstileの検証はSiteOps側で実行されます。contactは管理画面で作成したフォームID、JSONの各キーは受付項目のキーと一致させます。Turnstileのウィジェット作成と秘密キーの登録は、フォーム保護の設定を参照してください。

const response = await fetch(api + "/forms/contact/submissions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name,
    email,
    message,
    turnstileToken // フォーム保護がTurnstileの場合。未設定(none)なら省略できます
  })
});

const result = await response.json();
if (!response.ok || result.accepted !== true) {
  throw new Error(result.error || "inquiry_submission_failed");
}
  1. フォーム保護設定を読むGET .../forms/security からTurnstileの公開キーとactionを取得し、ウィジェットへ渡します。
  2. 公開APIへ直接送信するSiteOpsが許可オリジン、サイズ、Turnstileを検証して問い合わせ一覧へ保存します。
  3. 保存結果で完了表示を出すaccepted: trueを受付完了として扱います。メール通知の結果はレスポンスのdeliveryフィールドへ分かれて返ります。

許可オリジンの検証は、ブラウザからの送信(Originヘッダー付き)に適用されます。Originヘッダーを送らないサーバー間リクエストはこの検証の対象外で、ボット対策の実体はTurnstileです。

実装後

既存サイトを普段の手順でデプロイし、「5. 動作確認」へ進みます。

秘密情報の管理場所

Cloudflareの認証情報、D1、R2、メールの認証情報、管理者トークンはSiteOps専用環境が管理します。既存サイトへ渡る情報はSiteOps URLとSiteOps IDの2つです。

5. 動作確認

公開前に、SiteOps本体、既存サイト、フォーム、メール(設定した場合)を確認します。

公開記事が既存サイトへ表示される
下書きが公開APIへ出ない
title、canonical、構造化データがHTMLにある
ブラウザからのフォーム送信が許可オリジンに限定されている
無効なTurnstileトークンが拒否される
問い合わせがメール成否に関係なく保存される
通知先と受付メールの内容が正しい
スマホで横はみ出しや入力ズームがない
npx siteops doctor --remote
npm run siteops:verify
npx siteops backup create
npx siteops backup verify backups/<作成されたディレクトリ>

backup createが保存先(既定はbackups/日時)を表示し、その値をbackup verifyへ渡します。

チェックリストの確認コマンド例

# 下書きが公開APIへ出ないこと(公開前のslugは404)
curl -i {SITEOPS_URL}/api/public/sites/{siteKey}/content/news/entries/下書きのslug

# 許可していないオリジンからのブラウザ送信が拒否されること(403)
curl -i -H "Origin: https://evil.example.com" {SITEOPS_URL}/api/public/sites/{siteKey}/forms/security

siteops verify 成功時の表示

PASS  runtime          SiteOps本体へ接続できました (312 ms)。
PASS  website          公開APIが https://www.example.com からの接続を許可しています(フォーム保護: turnstile)。ページ表示とフォームは既存サイト側で確認します。

FAILが1件でもあると終了コード1で停止し、原因の場所と次の対処を表示します。

SiteOpsの更新

配布ファイル運用では、新しいリリースのtgzでvendor/siteops.tgzを置き換え、npm install ../vendor/siteops.tgzで入れ替えます。その後npx siteops doctor --remotenpx siteops deploy --dry-runを通してからデプロイします。npm run siteops:updateはnpmレジストリ配布を利用する場合の更新手段です。

復旧

管理者トークンを紛失した場合はnpx siteops auth resetで再発行します。D1とR2のバックアップはハッシュ検証と復元リハーサル(backup verify)を通過した状態で保管します。

管理画面はPCとスマホの両方で利用できます。公開前に実機でも確認します。

メール接続

基本セットアップでフォームの問い合わせを管理画面へ保存できます。社内通知、受付メール、管理画面からの返信を使う段階で送信サービスを接続します。

問い合わせ保存基本セットアップフォームから届いた内容を問い合わせ一覧へ記録
社内通知・受付メール送信接続を追加Cloudflare Email Sending、SendGrid、Amazon SES、Agentic Inboxから選択
会社メール取り込み受信接続を追加contact@ や support@ へのメールを問い合わせ一覧へ統合
管理画面では使いたい機能を選ぶと、必要な接続手順が順番に表示されます。
メール接続の方式を確認送信サービス、DNS、会社メール取り込みの構成別手順

複数行コマンドの「\」はbashの行継続記号です。PowerShellでは1行へ連結して実行します。

会社メール取り込み時のMX設定

--forms-only、SendGrid、SESでは現在の会社メール受信先を維持します。Cloudflareで会社メールも取り込む構成では、受信MXがCloudflareへ切り替わります。実行前にCLIが変更内容を明示します。

Cloudflare DNSならAPIキーの登録を省略できる会社ドメインがCloudflare DNSならCloudflare Email Sendingを利用できます。SiteOps Workerの送信bindingを使うため、SendGridやAWSのAPIキーを追加する作業が省けます。
既存の会社メール送信設定では送信ドメイン用の認証レコードを追加します。さくら、Xserver、Google Workspaceなど現在の受信MXは、会社メール取り込みを選ぶまで維持できます。
既存SMTP現在のSiteOps送信アダプターはCloudflare Email Sending、SendGrid、Amazon SES、Agentic Inboxの4方式です。レンタルサーバーのSMTP認証情報を直接登録する経路は、今後のアップデートで対応予定です。
問い合わせ保存から開始現在の会社メールを維持

問い合わせ一覧への保存は基本セットアップで有効です。このレシピは送信停止の状態を設定として明示します。送信接続を追加すると社内通知と受付メールも有効になります。

npx siteops mail setup disabled
npx siteops deploy
Cloudflare DNSで通知を送信会社メールのMXは現在のまま

ブラウザでCloudflare認証を行い、Email Sending、Workerデプロイ、検証を自動実行します。認証情報はCloudflare CLIが管理します。

npx siteops cloudflare connect \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --notification-emails team@example.com \
  --forms-only
Cloudflare DNSで送信+会社メール受信Email Routing有効化でMXが変わる

指定した会社アドレスをSiteOps Workerへ接続します。実行前にCLIがMX変更を明示し、既存ルールと競合する場合は停止します。

npx siteops cloudflare connect \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --notification-emails team@example.com \
  --addresses contact@example.com,support@example.com
Cloudflare以外のDNS+SendGrid現在のDNSへCNAMEを追加

送信専用APIキーをSiteOpsへ登録します。デプロイ後に管理画面で認証レコードを発行し、Xserver、さくら、Route 53など現在のDNSへ追加して「DNSの反映を確認」を押します。

npx siteops mail setup sendgrid \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com
npx siteops secret set SITEOPS_SENDGRID_API_KEY
npx siteops deploy
Cloudflare以外のDNS+Amazon SES現在のDNSへDKIMを追加

SiteOps専用のIAMの認証情報を登録し、権限をSESの送信APIに限定します。

npx siteops mail setup ses \
  --from-email no-reply@example.com \
  --from-name "Example Company" \
  --reply-to contact@example.com \
  --region ap-northeast-1
npx siteops secret set SITEOPS_AWS_ACCESS_KEY_ID
npx siteops secret set SITEOPS_AWS_SECRET_ACCESS_KEY
npx siteops deploy
Agentic Inbox外部メールボックスを送受信に使う

会社所有のメールボックスを送受信アドレスに指定し、Cloudflare AccessのService Authで保護します。

npx siteops mail setup agentic-inbox \
  --from-email support@example.com \
  --from-name "Example Company" \
  --base-url https://inbox.example.com \
  --mailbox support@example.com
npx siteops secret set SITEOPS_AGENTIC_INBOX_ACCESS_CLIENT_ID
npx siteops secret set SITEOPS_AGENTIC_INBOX_ACCESS_CLIENT_SECRET
npx siteops deploy

トラブルシューティング

導入はどの段階からでも再開できます。

現在地がわからないnpm run siteops:status が4段階の進捗と、次に実行する1件のコマンドを表示します。中断後の再開もここから始めます。
デプロイ直後に404が出るデプロイはサイトデータの初期化まで自動実行します。それでも site_not_found 等が出る場合は npx siteops doctor --remote が初期化と診断を実行します。その後に再試行してください。
Cloudflare上のリソースを消してしまったnpm run siteops:deploy がD1・R2の消失を検知して再作成します(データは空の状態へ戻ります)。データの復元は backup verify を通過したバックアップから行います。
CI・自動化環境で実行したい認証は CLOUDFLARE_API_TOKEN、確認プロンプトは --yes、秘密値は --secret-env <環境変数名> または npx siteops secret set <NAME> --from-env <環境変数名> で渡します。基本フロー(セットアップ〜動作確認)の対話プロンプトにはすべて非対話の代替があります。例外はCloudflareメール接続(siteops cloudflare connect)で、ブラウザでのOAuth承認が必要なため対話できる端末で一度実行します。
エラーが出たnpx siteops doctor --remote が設定、認証情報、Cloudflare側の状態をPASS/WARN/FAILで点検し、原因の場所を特定します。
公開URLを変更したいnpx siteops connect live --origin で新しい公開URLを登録し、npm run siteops:deploy で反映します。
verifyでwebsiteがFAIL公開URL(allowedOrigins)の登録、npm run siteops:deploy での反映、既存サイトが公開中であることを順に確認します。
管理者トークンを紛失npx siteops auth reset が新しいトークンを発行し、既存のブラウザセッションを無効化します。
データの復元npx siteops backup verify <バックアップディレクトリ> でハッシュ検証と復元リハーサルを通してから復元します。

構成別の詳細

構成の考え方、公開APIリファレンス、反映方式、フォーム保護の技術手順です。

既存サイト、SiteOps、メールを分けて接続

公開サイトがXserver、DNSがCloudflareという構成にも対応します。ホスティング、DNS、メールはそれぞれ現在の契約に合わせて選べます。

既存サイトの設置先どのホスティングでも公開HTTPS APIで接続します。
会社ドメインのDNSCloudflareメールはCloudflare DNSと接続します。SendGrid/SESでは現在のDNSを継続利用できます。
会社メールの受信サイトフォーム運用では現在のMXを維持します。会社アドレスの取り込みを選ぶと、受信MXがCloudflareへ切り替わります。

サイトを移転せずに接続する方法

既存サイトはVercel、Xserver、さくら、AWS、Cloudflareなど現在のホスティングのまま、公開HTTPS APIでSiteOpsと接続します。フレームワークを問わず接続方法は同一で、既存サイトへのコード生成やパッケージ追加はありません。

公開APIリファレンス

website manifest --jsonは次のURLを実際のSiteOps URLとSiteOps IDへ展開して返します。管理トークンを使わず、公開済みデータとフォーム受付へ接続できます。

GET  /api/public/sites/{siteKey}/content/{contentType}/entries
GET  /api/public/sites/{siteKey}/content/{contentType}/entries/{slug}
GET  /api/public/sites/{siteKey}/assets/{key}
GET  /api/public/sites/{siteKey}/forms/security
POST /api/public/sites/{siteKey}/forms/{formKey}/submissions
管理画面では、記事をどう既存サイトへ反映するかを選びます。ホスティング先の移転は発生せず、現在の公開手順を維持します。

記事の反映方式

公開APIは公開済みの記事を返します。静的サイトではビルド時に完全なHTMLを生成できるため、通常の静的CMSと同じSEO構成にできます。複数行コマンドの「\」はbashの行継続記号です。PowerShellでは1行へ連結して実行します。

APIから表示

SSR、サーバー、エッジでリクエスト時に公開記事を取得し、title、canonical、JSON-LDをサーバー側で生成します。

npx siteops connect live \
  --origin https://www.example.com

静的サイトを再ビルド

記事公開後に非公開ビルドURLを呼び出します。ビルド失敗時は前回成功したサイトを維持し、記事一覧は成功したビルドから配信します。

npx siteops connect webhook \
  --origin https://www.example.com \
  --webhook-url-env SITE_BUILD_HOOK

手動反映

移行中や別の承認工程を通す場合に使います。SiteOpsへ公開記事を保存し、承認後に既存サイトへ反映する運用です。

npx siteops connect manual --origin https://www.example.com
公開状態、本文、SEO項目を管理し、選んだ反映方式で既存サイトへ届けます。

フォーム保護の設定

SiteOpsはフォーム項目、通知条件、自動返信を管理します。公開サイトは見た目と入力UIを持ち、Turnstileで確認したトークンをSiteOpsへ渡します。

  1. Turnstileウィジェットを作成Cloudflareダッシュボードの「Turnstile」でウィジェットを作成し、公開する正確なホスト名(例: www.example.com。検証環境なら localhost や 127.0.0.1)を登録します。作成するとサイトキー(公開)とシークレットキー(秘密)の2つが発行されます。サイト自体がCloudflare以外のホスティングでも利用できます。
  2. 2つのキーをSiteOpsへ接続サイトキーはコマンドの引数で渡し、シークレットキーは非表示プロンプトへ入力するか、非対話環境では --secret-env で環境変数から渡します。ローカルの認証情報とWorker Secretへ保存されます。
  3. ウィジェットをフォームへ組み込む下記の組み込み例のとおり、/forms/security から取得したサイトキーとactionでウィジェットを描画し、応答トークンを turnstileToken として送信JSONへ含めます。
  4. テスト問い合わせを確認保存成功とメール送信結果を分けて確認します。メール失敗時も問い合わせデータは保存済みの状態を維持します。
# 対話環境(シークレットキーは非表示プロンプトへ入力)
npx siteops forms protect turnstile \
  --turnstile-site-key "0x4AAAA..."

# 非対話環境(CI・自動化)
TURNSTILE_SECRET="シークレットキー" npx siteops forms protect turnstile \
  --turnstile-site-key "0x4AAAA..." --secret-env TURNSTILE_SECRET

npx siteops deploy

複数行コマンドの「\」はbashの行継続記号です。PowerShellでは1行へ連結して実行します。方式・サイトキーの変更は siteops deploy で反映され、シークレットキーの差し替え(npx siteops secret set SITEOPS_PUBLIC_FORM_TURNSTILE_SECRET_KEY --from-env <環境変数名>)は配備済みの環境へ即時反映されます。

ウィジェットの組み込み例

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div id="turnstile-widget"></div>

// フォーム保護設定を取得してウィジェットを描画する
const { protection } = await (await fetch(api + "/forms/security")).json();
if (protection.mode === "turnstile") {
  turnstile.render("#turnstile-widget", {
    sitekey: protection.turnstileSiteKey,
    action: protection.action
  });
}
// 送信時: turnstile.getResponse() の値を turnstileToken としてJSONへ含めます

サーバー側の検証とエラー

SiteOpsは送信ごとに (1)トークンの存在 (2)Cloudflareでのトークン検証 (3)actionが siteops-form と一致 (4)送信元Originのホスト名がウィジェット登録ホスト名と一致、の4段階を検証します。

エラーキー意味対処
public_form_turnstile_token_requiredトークン未送信ウィジェットの応答値を turnstileToken で送ります
public_form_turnstile_verification_failedトークン検証に失敗サイトキーとシークレットキーが同じウィジェットの組か確認します
public_form_turnstile_action_invalidactionが不一致ウィジェット描画時のactionを siteops-form に合わせます
public_form_turnstile_hostname_invalid送信元ホスト名が不一致公開ホスト名をウィジェットへ登録します(下記のテストキーの注意も参照)
公式テストキーの限界

Cloudflare公式のテストキー(常に成功: サイトキー 1x00000000000000000000AA + シークレット 1x0000000000000000000000000000000AA、拒否確認用シークレット 2x0000000000000000000000000000000AA)で流れを確認できます。ただしテストキーの検証応答はホスト名が example.com 固定のため、ローカル環境からのブラウザ送信は必ず public_form_turnstile_hostname_invalid になります。ブラウザまで含めた受理確認は、実ウィジェットに公開ホスト名を登録して行います(Originヘッダーを送らないサーバー間送信ではホスト名検証の対象外)。

サイトのホスティング先に関係なく、フォーム保護状態を管理画面から確認できます。