静的サイトにお問い合わせフォームを無料で付ける|Cloudflare Workers×Email Routing×Turnstile

目次
  1. 全体の仕組み
  2. 前提:Email Routing を設定しておく
  3. wrangler.jsonc の設定
  4. フォームの HTML
  5. Worker の処理
  6. Turnstile のトークンを確認する
  7. メールを組み立てて送る
  8. 動作確認
  9. まとめ

このブログをWordPressからCloudflare(Astro×Workers)に移行したときに、一番「どうしよう」と思ったのがお問い合わせフォームでした。

WordPressなら Contact Form 7 などのプラグインを入れるだけですが、静的サイトにはフォームの内容を受け取ってメールを送る仕組みがありません。外部のフォームサービスを使う手もありますが、無料プランだと広告が出たり、送信数に上限があったりします。

そこで、Cloudflareの無料プランだけでお問い合わせフォームを作りました。

関連記事WordPressをやめてCloudflareに移行した|Astro×Workersでサーバー代0円、記事はMarkdownで書く

全体の仕組み

登場するのは次の3つで、どれも無料プランで使えます。

役割使うもの
フォームの送信を受け取るCloudflare Workers
メールを送るEmail Routing(Worker からのメール送信)
スパム対策Turnstile

流れはこうです。

  1. フォームを送信する

    ブラウザから /api/contact に、入力内容と Turnstile のトークンを POST します。

  2. Worker がトークンを確認する

    Worker が Turnstile の API にトークンを送り、人間による送信かどうかを確認します。

  3. Worker がメールを送る

    確認できたら、Email Routing の仕組みを使って、自分のメールアドレスにお問い合わせ内容を送ります。

このブログは静的ファイル(HTML)を Workers の静的アセットとして配信していて、静的ファイルが見つからないリクエストだけ Worker のプログラムが動く設定にしています。/api/contact には対応するファイルが無いので、ここへの送信だけが Worker に届きます。

前提:Email Routing を設定しておく

Worker からメールを送るには、そのドメインで Email Routing が有効になっている必要があります。

このブログでは、もともと info@takeiho.com 宛てのメールを Email Routing で Gmail に転送していたので、その設定をそのまま使いました。

wrangler.jsonc の設定

Worker からメールを送るための設定(send_email バインディング)を追加します。

wrangler.jsonc
{
  "send_email": [{ "name": "MAIL", "destination_address": "転送先のアドレス@gmail.com" }],
  "vars": {
    "CONTACT_FROM": "info@takeiho.com",
    "CONTACT_TO": "転送先のアドレス@gmail.com"
  }
}

destination_address を指定しておくと、このバインディングからはそのアドレスにしか送れなくなります。万が一プログラムに不具合があっても、他人にメールが送られることはありません。

フォームの HTML

フォームはふつうの HTML です。ポイントは次の2つです。

  • Turnstile のウィジェット(cf-turnstile)を置く
  • ボット対策に、人間には見えない入力欄(ハニーポット)を1つ置く
inquiry.astro(抜粋)
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form id="contact-form" method="post" action="/api/contact">
  <input name="name" required maxlength="100" />
  <input name="email" type="email" required maxlength="200" />
  <input name="subject" maxlength="200" />
  <textarea name="message" required maxlength="5000"></textarea>
  <!-- 人間には見えない欄。ここに入力があればボットとみなす -->
  <input type="text" name="website" class="hp" tabindex="-1" autocomplete="off" aria-hidden="true" />
  <div class="cf-turnstile" data-sitekey="サイトキー" data-language="ja"></div>
  <button type="submit">送信する</button>
</form>

送信は JavaScript の fetch で行い、ページを移動せずに「送信しました」と表示するようにしています。失敗したときは turnstile.reset() でウィジェットをリセットして、もう一度送れるようにしておきます。

Worker の処理

Worker では、入力チェック → Turnstile の確認 → メール送信の順に処理します。

Turnstile のトークンを確認する

フォームから送られてきたトークン(cf-turnstile-response)を、シークレットキーと一緒に Turnstile の API に送って確認します。

worker/index.ts(抜粋)
const token = String(form.get('cf-turnstile-response') ?? '');
const verify = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
  method: 'POST',
  body: new URLSearchParams({ secret: env.TURNSTILE_SECRET_KEY, response: token }),
});
const result = (await verify.json()) as { success: boolean };
if (!result.success) return json({ ok: false, error: 'スパム対策の確認に失敗しました。' }, 400);

シークレットキーは設定ファイルに書かず、npx wrangler secret put TURNSTILE_SECRET_KEY で Worker に登録しています。

メールを組み立てて送る

Worker からのメール送信では、メールの中身をMIME 形式の文字列で自分で組み立てる必要があります。日本語の件名や本文が文字化けしないように、Base64 でエンコードしています。

worker/index.ts(抜粋)
import { EmailMessage } from 'cloudflare:email';

const raw = [
  `From: =?UTF-8?B?${base64Utf8('takeiho.com お問い合わせ')}?= <${env.CONTACT_FROM}>`,
  `To: <${env.CONTACT_TO}>`,
  `Reply-To: <${email}>`,
  `Subject: =?UTF-8?B?${base64Utf8(`【お問い合わせ】${subject}`)}?=`,
  `Message-ID: <${crypto.randomUUID()}@takeiho.com>`,
  `Date: ${new Date().toUTCString()}`,
  'MIME-Version: 1.0',
  'Content-Type: text/plain; charset=UTF-8',
  'Content-Transfer-Encoding: base64',
  '',
  base64Utf8(body).replace(/.{76}/g, '$&\r\n'),
].join('\r\n');

await env.MAIL.send(new EmailMessage(env.CONTACT_FROM, env.CONTACT_TO, raw));

Reply-To に問い合わせた人のメールアドレスを入れておくと、Gmail で「返信」を押すだけで相手に返信できます。

動作確認

npm run preview(wrangler dev)で、Cloudflare と同じ環境を手元で動かして確認しました。手元では実際のメールは届かないので、入力チェックや Turnstile の動き、エラーメッセージの表示を中心に確認しています。

本番に公開したあとは、実際にフォームから送信して、Gmail に届くこと、返信ボタンで相手のアドレスが入ることを確認して完了です。

よくある質問

無料プランで、問い合わせた人に自動返信メールを送れますか?

Email Routing の Worker からのメール送信は、認証済みの転送先アドレスにしか送れないため、問い合わせた人への自動返信はできません。自分宛てに届いたメールから返信する形になります。

Turnstile は無料ですか?

はい。Cloudflare のアカウントがあれば無料で使えます。画像を選ばせるような CAPTCHA と違い、ほとんどの場合は何も操作せずに確認が終わります。

フォームの送信で Worker の無料枠を使い切ることはありますか?

Workers の無料プランは 1 日 10 万リクエストまでです。このブログの構成では、Worker が動くのはお問い合わせの送信や旧 URL のリダイレクトなどに限られるため、個人ブログで上限に届くことはまずありません。

まとめ

  • 静的サイトでも、Cloudflare Workers を使えばお問い合わせフォームを無料で作れる
  • メールは Email Routing の仕組みで送る。宛先は認証済みの転送先アドレスに限られる
  • スパム対策は Turnstile とハニーポットの組み合わせで十分

人気記事のランキングも、同じように Cloudflare の無料の機能で作っています。

関連記事静的サイトで人気記事ランキングを自動更新する|Cloudflare Web AnalyticsのAPI×GitHub Actions

Related関連記事