HugoサイトにCloudflare WorkersとResendでお問い合わせフォームを実装した話
Hugoで作った静的サイトにお問い合わせフォームを実装しました。静的サイトはサーバーサイドの処理ができないため、フォームの送信処理には別の仕組みが必要です。今回は Cloudflare Workers(サーバーレス関数)と Resend(メール送信API)を組み合わせた構成にしました。
全体の構成
ブラウザ(フォーム送信)
↓ fetch (POST /JSON)
Cloudflare Workers(メール送信処理)
↓ Resend API
メール受信(Gmail)
Cloudflare Workers の無料枠(1日10万リクエスト)と Resend の無料枠(月3,000通)の範囲で十分運用できますので、小規模サイトであればコストゼロで実現できます。
1. Resend のセットアップ
アカウント作成とドメイン認証
resend.com でアカウントを作成し、送信元ドメインを登録します。
Domains → Add Domain でドメイン(例:jiru-labo.com)を追加すると、DNS に追加すべきレコードが表示されます。Cloudflare の DNS 管理画面でそれらのレコードを追加すると、数分で認証が完了します。
API キーの作成
API Keys → Create API Key で API キーを発行しておきます。後で Worker の環境変数に設定します。
2. Cloudflare Workers の作成
プロジェクト作成
mkdir contact-worker
cd contact-worker
wrangler.toml を作成します:
name = "contact-worker"
main = "src/index.js"
compatibility_date = "2024-01-01"
Worker の実装
src/index.js を作成します:
const ALLOWED_ORIGINS = [
'https://jiru-labo.com',
'http://localhost:1313',
];
export default {
async fetch(request, env) {
const origin = request.headers.get('Origin') || '';
const isAllowed = ALLOWED_ORIGINS.includes(origin) || origin.endsWith('.pages.dev');
const corsHeaders = {
'Access-Control-Allow-Origin': isAllowed ? origin : ALLOWED_ORIGINS[0],
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405, headers: corsHeaders });
}
const { name, email, subject, message } = await request.json();
if (!name || !email || !message) {
return new Response(JSON.stringify({ error: '必須項目が不足しています' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'お問い合わせフォーム <contact@jiru-labo.com>',
to: [env.TO_EMAIL],
reply_to: email,
subject: subject ? `[お問い合わせ] ${subject}` : `[お問い合わせ] ${name}様より`,
html: `<p>お名前:${name}</p><p>メール:${email}</p><p>メッセージ:${message}</p>`,
}),
});
if (!res.ok) {
return new Response(JSON.stringify({ error: 'メール送信に失敗しました' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
},
};
CORS 設定がポイントです。ブラウザからの fetch リクエストには Origin ヘッダーが付くため、許可するオリジンを明示しておかないと、本番サイトからのリクエストがブロックされます。ローカル開発用の localhost:1313 も忘れずに追加しておきます。
デプロイ
# Cloudflare にログイン(初回のみ)
npx wrangler login
# シークレットを設定
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put TO_EMAIL
# デプロイ
npx wrangler deploy
API キーなどの機密情報はコードに直接書かず、wrangler secret で環境変数として設定します。デプロイ後に https://contact-worker.<サブドメイン>.workers.dev という URL が発行されます。
なお、npm install -g wrangler でグローバルインストールしようとしたところ、/usr/local/lib/node_modules が root 所有のため権限エラーになりました。npx wrangler を使えばインストール不要で動作しますので、こちらをお勧めします。
3. Hugo 側のフォーム実装
content/pages/contact/index.md に HTML フォームと送信スクリプトを記述します:
<form id="contact-form" novalidate>
<input type="text" name="name" required>
<input type="email" name="email" required>
<input type="text" name="subject">
<textarea name="message" required></textarea>
<button type="submit">送信する</button>
<div id="cf-status"></div>
</form>
<script>
var WORKER_URL = 'https://contact-worker.jiru-labo.workers.dev';
document.getElementById('contact-form').addEventListener('submit', function(e) {
e.preventDefault();
fetch(WORKER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: this.elements['name'].value,
email: this.elements['email'].value,
subject: this.elements['subject'].value,
message: this.elements['message'].value,
})
})
.then(function(res) { return res.json(); })
.then(function(data) {
document.getElementById('cf-status').textContent =
data.ok ? '送信しました。' : data.error;
});
});
</script>
ハマったポイント
送信元ドメインの typo:Worker の from アドレスを contact@jiru-lab.com と書いていましたが、Resend に登録したドメインは jiru-labo.com(labo)でした。Resend からは validation_error が返るだけでわかりにくかったため、エラーレスポンスに詳細を含めてデバッグしました。
CORS エラー:ローカルで動作確認したとき、localhost がオリジン許可リストに入っておらずブロックされていました。curl での直接テストは通るのにブラウザからだけ失敗するときは CORS を疑うとよいです。
まとめ
Cloudflare Workers と Resend を組み合わせることで、静的サイトでも無料でお問い合わせフォームを実装できました。設定のポイントは以下の3点です。
| ポイント | 内容 |
|---|---|
| ドメイン認証 | Resend で送信元ドメインを必ず認証する |
| CORS 設定 | 本番・ローカル両方のオリジンを許可リストに追加する |
| シークレット管理 | API キーはコードに書かず wrangler secret で設定する |