開発・個人開発

問い合わせフォームをSupabase+Resend+Slackで作る 静的サイト向け実装

問い合わせフォームをSupabase+Resend+Slackで作る 静的サイト向け実装

この記事の要点

静的サイトの問い合わせフォームを、Supabase Edge Functions・Resendのメール送信・SlackのWebhookを使って実装する。サーバー不要・コストほぼゼロで動く問い合わせシステムの作り方を解説する。

結論

静的サイトの問い合わせフォームは、Supabase Edge FunctionsをAPIエンドポイントとして使い、Resendでメール送信・SlackのIncoming Webhookで通知という3つを組み合わせると実現できる。問い合わせ内容はSupabaseのデータベースにも保存するため、後から確認もできる。

アーキテクチャ全体像

[フォーム画面(Astro)]
       ↓ フォーム送信(fetch API)
[Supabase Edge Function]
  ├── データベースに保存(Supabase)
  ├── メール送信(Resend API)
  └── Slack通知(Incoming Webhook)

フロントエンドはフォームのUIと送信処理だけを担当し、実際の処理はEdge Functionsに集中させる。APIキー(Resend・Slack)はEdge Functions側の環境変数に設定するため、フロントエンドのコードには含まれない。

ステップ1:データベースのテーブル作成

Supabaseのダッシュボード(SQL Editor)で実行する。

CREATE TABLE inquiries (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name text NOT NULL,
  email text NOT NULL,
  message text NOT NULL,
  created_at timestamptz DEFAULT now()
);

ALTER TABLE inquiries ENABLE ROW LEVEL SECURITY;

-- 挿入のみ許可(読み取りは管理者のみ)
CREATE POLICY "Allow insert" ON inquiries
  FOR INSERT WITH CHECK (true);

問い合わせの読み取りはフロントエンドからは行わない設計なので、SELECTのポリシーは追加しない。管理者はSupabaseダッシュボードから直接確認する。

ステップ2:Resendのセットアップ

Resend(resend.com)でアカウントを作成し、APIキーを取得する。

  1. resend.com でアカウント作成
  2. 「API Keys」→「Create API Key」でAPIキーを生成
  3. ドメインの設定(送信元ドメインをResendに登録してDNSを設定)

ドメインを設定しなくてもonboarding@resend.devから送信できるが、自分のドメインから送信するには DNS 設定が必要だ。

ステップ3:Slack Webhookのセットアップ

Slack の Incoming Webhook を設定する。

  1. Slack の「アプリ管理」→「Incoming Webhooks」を検索してアプリを追加
  2. 通知を送りたいチャンネルを選択
  3. Webhook URLをコピーして保存する(https://hooks.slack.com/services/...の形式)

ステップ4:Supabase Edge Functionの作成

Supabase CLIをインストールしてEdge Functionを作成する。

npm install -g supabase
supabase login
supabase init

supabase/functions/send-inquiry/index.tsを作成する。

import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders });
  }

  try {
    const { name, email, message } = await req.json();

    // 入力バリデーション
    if (!name || !email || !message) {
      return new Response(
        JSON.stringify({ error: '必須フィールドが未入力です' }),
        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      );
    }

    // Supabaseクライアント(service_role keyを使用)
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    );

    // データベースに保存
    const { error: dbError } = await supabase
      .from('inquiries')
      .insert({ name, email, message });

    if (dbError) throw dbError;

    // Resendでメール送信
    const resendApiKey = Deno.env.get('RESEND_API_KEY')!;
    await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${resendApiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'お問い合わせ <noreply@your-domain.com>',
        to: ['your-email@example.com'],
        subject: `【お問い合わせ】${name}様より`,
        text: `お名前: ${name}\nメール: ${email}\n\nメッセージ:\n${message}`,
      }),
    });

    // Slack通知
    const slackWebhookUrl = Deno.env.get('SLACK_WEBHOOK_URL')!;
    await fetch(slackWebhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `📩 新しいお問い合わせ\n*氏名:* ${name}\n*メール:* ${email}\n*内容:* ${message}`,
      }),
    });

    return new Response(
      JSON.stringify({ success: true }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );

  } catch (error) {
    return new Response(
      JSON.stringify({ error: '送信に失敗しました' }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  }
});

ステップ5:環境変数の設定とデプロイ

Edge Functionで使う環境変数をSupabaseに設定する。

supabase secrets set RESEND_API_KEY=re_xxxxxxxxxxxx
supabase secrets set SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...

SUPABASE_URLSUPABASE_SERVICE_ROLE_KEYはEdge Functions内で自動的に利用可能になる。

Edge Functionをデプロイする。

supabase functions deploy send-inquiry

デプロイが完了すると、https://your-project.supabase.co/functions/v1/send-inquiryのエンドポイントでアクセスできるようになる。

ステップ6:フロントエンドのフォーム実装

src/components/ContactForm.astroを作成する。

<form id="contact-form">
  <div>
    <label for="name">お名前 <span>*</span></label>
    <input type="text" id="name" name="name" required />
  </div>
  <div>
    <label for="email">メールアドレス <span>*</span></label>
    <input type="email" id="email" name="email" required />
  </div>
  <div>
    <label for="message">お問い合わせ内容 <span>*</span></label>
    <textarea id="message" name="message" rows="6" required></textarea>
  </div>
  <button type="submit" id="submit-btn">送信する</button>
  <p id="result-message" hidden></p>
</form>

<script>
  const form = document.getElementById('contact-form');
  const submitBtn = document.getElementById('submit-btn');
  const resultMsg = document.getElementById('result-message');

  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    submitBtn.disabled = true;
    submitBtn.textContent = '送信中...';

    const data = {
      name: form.name.value,
      email: form.email.value,
      message: form.message.value,
    };

    try {
      const res = await fetch(
        'https://your-project.supabase.co/functions/v1/send-inquiry',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'apikey': import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
          },
          body: JSON.stringify(data),
        }
      );

      if (res.ok) {
        resultMsg.textContent = 'お問い合わせを受け付けました。';
        resultMsg.hidden = false;
        form.reset();
      } else {
        throw new Error('Server error');
      }
    } catch {
      resultMsg.textContent = '送信に失敗しました。時間をおいて再度お試しください。';
      resultMsg.hidden = false;
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = '送信する';
    }
  });
</script>

Slack Webhookの単体設定についてはSlack Incoming Webhookで通知を作るを、Resendの詳細設定はResendでメール送信を実装するで解説している。

まとめ

静的サイトの問い合わせフォームは、Supabase Edge Functions → Resend → Slack Webhookのパイプラインで実装できる。APIキーはEdge Functions側の環境変数に隠蔽されるため、フロントエンドのコードに秘密情報が含まれない設計になる。全て無料枠で動くため、個人開発のメディアサイトには最適な構成だ。

よくある質問

静的サイトで問い合わせフォームを作るにはどうすればいいですか

静的サイトはHTMLを配信するだけなので、フォームの送信処理にはバックエンドが必要です。Supabase Edge Functions(サーバーサイドで実行される関数)を使うと、サーバーを別途用意せずにメール送信・Slack通知・データ保存を処理できます。

この実装のコストはどのくらいかかりますか

Supabase Edge Functionsの無料枠は月50万回のリクエストまで。Resendの無料プランは月3,000件のメール送信まで。Slack Webhookは無料です。個人の問い合わせフォームであれば、全て無料枠内で賄えます。

Supabase Edge Functionsとは何ですか

Denoランタイムで実行されるサーバーサイドの関数で、Supabaseのインフラで動きます。APIエンドポイントを作成したり、外部サービスとの連携処理を行ったりするのに使います。フロントエンドから直接使いたくない処理(APIキーを必要とする外部API呼び出しなど)を安全に実行できます。

Resend以外のメール送信サービスは使えますか

SendGrid・Mailgun・Amazon SESなど他のサービスでも同じアーキテクチャで実装できます。Resendは開発者向けに設計されたAPIが使いやすく、無料プランで個人開発に十分な量を送れるため、最初の選択肢として勧めています。