静的サイトのお問い合わせフォーム2 分で読めます
Cloudflare Pages のサイトにお問い合わせフォームを追加する
Function を書かずに Cloudflare Pages にお問い合わせフォームを追加します。フォームバックエンドに送信し、_headers でヘッダーを設定し、自分の Turnstile キーを使います。
Cloudflare Pages の静的アセットは POST に 405 を返すので、Pages Function を書くか、フォームをフォームバックエンドに送信するかのどちらかです。Ikiform なら Function は不要です。フォームの action をエンドポイントに設定し、Allowed domains に your-project.pages.dev を追加すれば、すべてのプレビュー URL もカバーされます。_headers の Content-Security-Policy には Ikiform を含めてください。すでに Cloudflare を使っているなら、自分の Turnstile キーをフォームに設定することもできます。
Cloudflare Pages ならではの違い
Pages はビルド結果を Cloudflare のネットワークから配信します。静的アセットは GET に応えますが、POST にはボディなしの 405 を返します:
HTTP/2 405
server: cloudflare
content-length: 0Pages では、functions/ フォルダーの Pages Functions でコードを実行できます。functions/contact.ts で送信を受け付けることもできますが、そうなるとバックエンドを書くことになります。解析、バリデーション、メールプロバイダー、スパム除外、保存です。Cloudflare 自身のドキュメントも、新しいプロジェクトは Pages ではなく Workers で始めるよう勧めています。フォームバックエンドに送信すればどちらの問題も避けられ、後で Workers の静的アセットに移っても同じフォームがそのまま動きます。
| Pages の特徴 | フォームへの影響 |
|---|---|
プレビューの URL は abc123.your-project.pages.dev のようなサブドメイン | 許可ドメイン your-project.pages.dev 1 つで、本番とすべてのプレビューをカバーできる。Vercel や Netlify のプレビューはそれぞれ個別の登録が必要 |
ヘッダーはビルド結果の _headers ファイルで指定する | ルールは最大 100 個、1 行 2,000 文字まで。Pages Functions からの応答には適用されない |
| Turnstile は Cloudflare の製品 | 鍵なしで Ikiform のマネージドな確認を使うことも、Cloudflare のダッシュボードで作った自分のウィジェットを使うこともできます |
- Pages がお問い合わせページを配信し、_headers ファイルの Content-Security-Policy を付けます。
- Turnstile ウィジェットがサイトキーで動作し、フォームにトークンを入れます。
- フォームはフィールドとトークンを Ikiform に送信します。
- Ikiform はシークレットキーでトークンを検証し、トークンのホスト名を許可ドメインと照合します。
- 有効な送信は保存され、メールで通知されます。トークンがない、期限切れ、再利用の場合は 403 captcha_failed になり、ページがウィジェットをリセットします。
フォーム
この版では自分の Turnstile ウィジェットを使います。すでに Cloudflare アカウントで Turnstile を管理しているなら理にかなっています。フォームの設定で Spam check を Your own Turnstile keys にし、サイトキーとシークレットを貼り付けます。ウィジェットはフォームに cf-turnstile-response フィールドを追加し、team のセレクトで誰が返信すべきかがわかります:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team">Who should read this?</label>
<select id="team" name="team">
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="press">Press</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" minlength="10" required></textarea>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxx"></div>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script src="/contact.js" defer></script>スクリプトで訪問者をページにとどめます。Turnstile のトークンは 1 回しか使えないので、送信に失敗したら次の試行のためにウィジェットをリセットします:
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.textContent = "Sending";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) {
form.replaceWith(Object.assign(document.createElement("p"), { textContent: "Sent. We'll reply by email." }));
return;
}
window.turnstile?.reset();
if (result?.error?.code === "captcha_failed") {
status.textContent = "The human check expired. Complete it again, then send.";
return;
}
const fields = result?.error?.fields ?? {};
status.textContent = Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
});- Cloudflare の Turnstile ウィジェットの設定で、フォームを置くホスト名を登録します。Ikiform もトークンのホスト名をフォームの Allowed domains と照合します。
- 有効なトークンのない送信は
403 captcha_failedで拒否されます。ウィジェットの処理が終わる前に送られたプレーンな HTML の送信も含みます。 - 鍵を一切使いたくない場合は、Spam check を Ikiform にし、ウィジェットとそのスクリプトを削除すれば、HTML の送信は ikiform.com で確認を実行します。その場合、
fetch版には SDK のchallenge()が必要です。人間確認を参照してください。
自分のキーか Ikiform の確認か
| Item | Ikiform の確認 | 自分の Turnstile キー |
|---|---|---|
| 管理するキー | なし | Cloudflare のダッシュボードのサイトキーとシークレット |
| HTML フォームの変更 | なし。確認は ikiform.com のページで実行 | ウィジェットとスクリプトを追加 |
fetch 版 | SDK の challenge() を呼ぶ | ウィジェットの cf-turnstile-response を送る |
| Content-Security-Policy | Ikiform 用の form-action と connect-src だけ | script-src と frame-src に challenges.cloudflare.com も追加 |
| Turnstile の分析 | 自分のアカウントにはない | Cloudflare のダッシュボードで、他のウィジェットと並んで見られる |
Ikiform の確認のほうが設定は少なくて済みます。すでに Turnstile の分析を見ている場合や、ウィジェットの見た目や配置を自分で管理したい場合は、自分のキーが向いています。
ローカルでウィジェットをテストする
Cloudflare は、常に成功する、または常に失敗するテスト用のサイトキーとシークレットを公開しています。たとえば常に成功するペアは 1x00000000000000000000AA と 1x0000000000000000000000000000000AA です。本番用のシークレットはテスト用キーで作られたトークンを拒否するので、1 つのフォームで混ぜないでください。ローカル作業用に 2 つ目の Ikiform フォームを作ってテスト用のキーペアを設定し、data-sitekey とフォーム ID をセットで切り替えます。そのフォームの Allowed domains は空のままにしてください。Ikiform がトークンのホスト名を確認するのはリストが設定されているときだけで、テスト用トークンがどのホスト名を報告するかは Cloudflare が文書化していません。
_headers ファイル
_headers を public/ などの静的アセットのディレクトリに置き、ビルド出力に含まれるようにします。Turnstile には https://challenges.cloudflare.com 用の script-src と frame-src が、Ikiform には form-action と connect-src が必要です:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com後で同じプロジェクトに Pages Function を追加した場合、そのレスポンスにはこれらのヘッダーが付きません。Function のコードで設定してください。
Workers に移行する場合
サイトを Workers の静的アセットに移しても、変わるのはデプロイ方法で、フォームの動作は変わりません。フォームは引き続き ikiform.com に送信するので、Cloudflare 側で処理するものはありません。移行後は 2 点を確認してください。新しい workers.dev のホストやルートが Allowed domains にあること、そしてヘッダーが引き続きページに届いていることです。Workers にはヘッダーを設定する独自の方法があります。
your-project.pages.dev とカスタムドメインを Allowed domains に追加します。pages.dev だけを追加してはいけません。すべての Pages プロジェクトが一致してしまいます。動作を確認する
- ブランチをプッシュし、プレビュー URL を開きます。送信して、メッセージが届き、スパム扱いになっていないことを確認します。プレビューはプロジェクトのホストのサブドメインです。
- 本番でもう一度送信し、戻るボタンを押して、ウィジェットを完了せずに同じフォームを再送信します。送信データが保存されるのではなく、リセットのメッセージが出るはずです。
- ダッシュボードで Submissions を開きます。送ったすべてのフィールドを含む新しい送信データがあります。
- 通知の受信者それぞれの受信トレイを確認します。各自にフィールド入りのメールが届き、フォームに
emailフィールドがあれば Reply の宛先は送信者になります。 - 何も届かない場合は、ダッシュボードのステータスフィルターを Spam に切り替えてください。スパムにも本物の送信と同じ成功の応答が返ります。
ハマりどころ
| 問題 | 対処 |
|---|---|
空のボディで 405 | フォームが Pages の URL に送信されています。デプロイされた action と、/contact を書き換える Function やリダイレクトルールがないことを確認してください。 |
| Turnstile ウィジェットが表示されない | Content-Security-Policy が challenges.cloudflare.com をブロックしています。script-src と frame-src に追加し、_headers がビルド出力に含まれていることを確認してください。 |
毎回 403 captcha_failed になる | サイトキーとシークレットが別々のウィジェットのものか、ホスト名がウィジェットに登録されていないか、テスト用のシークレットと本番のサイトキーを組み合わせています。 |
403 origin_not_allowed が返るか、すべての送信がスパムになる | ページのドメインがフォームの Allowed domains にありません。ドメインと、ローカルのテスト用に localhost を追加してください。登録済みドメインのサブドメインは許可されます。 |
よくある質問
いいえ。Function は送信を受け付ける方法の 1 つですが、その場合はバリデーション、メール送信、スパムフィルタリングを自分で書くことになります。フォームバックエンドに送信すれば、プロジェクトは完全に静的なままです。
いいえ。Pages のプレビュー URL は your-project.pages.dev のサブドメインなので、そのホストを許可すればすべてカバーされます。
はい。フォームのスパムチェックを自分の Turnstile キーに設定し、サイトキーとシークレットを貼り付けます。Ikiform は各トークンを検証し、そのホスト名を許可ドメインと照合します。
自分の Pages の URL に送信したためです。そこは GET リクエストにしか応答しない静的アセットを配信しています。フォームの action をフォームバックエンドに向けるか、そのパスに Pages Function を追加してください。
いいえ、無料プランはありません。テストは無料で、支払いの前に localhost から月 50 件のテスト送信ができます。本番運用するには、月 $6 で 500 件からの Pro を選ぶか、1 回払いで 10,000 件 $49 または 50,000 件 $149 を購入できます。クレジットに有効期限はありません。
手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com