AI予約サイトにメール通知を追加する
Supabase、Resend、Edge Functionsを使って、予約確認メールとオーナー宛の通知を送ります。
ロードマップと資料
初めての方へ:まずはここから
このガイドでは、AI予約サイトのプロジェクトに自動メール通知を追加します。 まだ予約サイトを作っていない場合は、先にメインのガイドから始めてください。
AIで予約サイトを作る
メインのガイドを開く
MAIN GUIDE
https://profitstudio.app/video/build-booking-website-ai
予約メール用にResendをセットアップする
Resend のアカウントを作成して設定する
Resendを開く
https://resend.com/
Resend のアカウントを作成する Resend の API キーを生成する API キーをコピーして安全に保管する
重要
Resend の API キーは非公開のままにしてください。フロントエンドのコードに追加したり、公開したりしないでください。
送信ドメインを追加して認証する
予約サイトのドメインを追加する 最も近いリージョンを選択する Resend から DNS レコードをコピーする ドメイン管理会社にレコードを追加する Resend に戻りドメインが認証されたことを確認する
DNS の認証には、ドメイン管理会社によって数分以上かかる場合があります。
予約メール送信用の関数を作成する
予約メール用の Edge Function を作成する
Supabaseを開く
https://supabase.com/?utm_source=partner&utm_medium=social&utm_campaign=supasquad&dub_id=faVfKdcLWvKCbYNp
Supabase プロジェクトを開く Edge Functions を開く ブラウザーのエディターで新しい関数を作成する 関数名を `send-booking-email` にする デフォルトのスターターコードを削除する
関数コードをカスタマイズして追加する
オーナーのメールアドレスを入力する 認証済みの Resend ドメインを入力する 送信元のビジネス名を入力する 必要に応じてメールテンプレートをカスタマイズする 生成された関数コードをコピーして貼り付ける
予約メール送信用の関数
import { serve } from "https://deno.land/std/http/server.ts"; serve(async (req) => { try { const body = await req.json(); const record = body.record || {}; const resendKey = Deno.env.get("RESEND_API_KEY"); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const supabaseServiceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!resendKey) { return new Response( JSON.stringify({ error: "Missing RESEND_API_KEY secret" }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } const ownerEmail = "YOUR_OWNER_EMAIL"; const customerEmail = record.email || ""; const customerName = record.full_name || record.name || "Customer"; const phone = record.phone || ""; const appointmentDate = record.appointment_date || record.date || ""; const startTime = record.start_time || record.time || ""; const endTime = record.end_time || ""; const notes = record.notes || ""; const status = record.status || "New"; let serviceName = record.service_name || record.service || record.service_title || ""; // If the appointment only has service_id, fetch the service name from the services table if (!serviceName && record.service_id && supabaseUrl && supabaseServiceRoleKey) { try { const serviceId = encodeURIComponent(record.service_id); const serviceResponse = await fetch( `${supabaseUrl}/rest/v1/services?id=eq.${serviceId}&select=name`, { method: "GET", headers: { apikey: supabaseServiceRoleKey, Authorization: `Bearer ${supabaseServiceRoleKey}`, "Content-Type": "application/json", }, } ); const serviceData = await serviceResponse.json(); if (Array.isArray(serviceData) && serviceData.length > 0) { serviceName = serviceData[0].name || ""; } } catch (_serviceError) { serviceName = ""; } } if (!serviceName) { serviceName = "Appointment"; } // 1) Email to business owner const ownerResponse = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${resendKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "Bookings <bookings@YOUR_DOMAIN_NAME>", to: [ownerEmail], subject: "OWNER_NOTIFICATION_SUBJECT", html: ` <h2>OWNER_NOTIFICATION_HEADING</h2> <p>OWNER_NOTIFICATION_INTRO</p> <p><b>Customer Name:</b> ${customerName}</p> <p><b>Email:</b> ${customerEmail}</p> <p><b>Phone:</b> ${phone}</p> <p><b>Service:</b> ${serviceName}</p> <p><b>Date:</b> ${appointmentDate}</p> <p><b>Start Time:</b> ${startTime}</p> <p><b>End Time:</b> ${endTime}</p> <p><b>Notes:</b> ${notes}</p> <p><b>Status:</b> ${status}</p> `, }), }); const ownerData = await ownerResponse.json(); // 2) Email to customer let customerData = null; if (customerEmail) { const customerResponse = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${resendKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "YOUR_BUSINESS_NAME <bookings@YOUR_DOMAIN_NAME>", to: [customerEmail], subject: "CUSTOMER_CONFIRMATION_SUBJECT", html: ` <h2>CUSTOMER_CONFIRMATION_HEADING</h2> <p>Hi ${customerName},</p> <p>CUSTOMER_CONFIRMATION_INTRO</p> <p><b>Service:</b> ${serviceName}</p> <p><b>Date:</b> ${appointmentDate}</p> <p><b>Start Time:</b> ${startTime}</p> <p><b>End Time:</b> ${endTime}</p> <p>If you need to make any changes, please contact us before your appointment.</p> <p>CUSTOMER_CONFIRMATION_CLOSING</p> `, }), }); customerData = await customerResponse.json(); } return new Response( JSON.stringify({ success: true, ownerEmailSent: ownerData, customerEmailSent: customerData, }), { headers: { "Content-Type": "application/json" }, } ); } catch (error) { return new Response( JSON.stringify({ success: false, error: String(error), }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } });
オーナーのメールアドレス
owner@example.com
認証済みのResendドメイン
yourdomain.com
送信者の店舗名
ブライト歯科クリニック
オーナー向けメールの件名
オーナー向けメールの見出し
オーナー向けメールの書き出し
お客様向けメールの件名
お客様向けメールの見出し
お客様向けメールの書き出し
お客様向けメールの結びの文
関数をデプロイして設定する
関数をデプロイする 関数の設定を開く `Verify JWT With Legacy Secret` をオフにする 変更を保存する Webhook の設定用に関数のエンドポイント URL をコピーして保存する
重要
Database Webhook が関数を呼び出せるように、「Verify JWT With Legacy Secret」をオフにします。
Resend APIキーをSupabaseに追加する
Resend の API キーを Supabase のシークレットとして追加する
Edge Function Secrets を開く 新しいシークレットを作成する 名前を `RESEND_API_KEY` に設定する Resend の API キーを貼り付ける シークレットを保存する
重要
シークレット名は正確に `RESEND_API_KEY` としてください。少しでも入力ミスがあると、関数は Resend にアクセスできません。
新規予約とメールを連携する
予約メール用の Webhook を作成する
新しい予約が追加されるたびに、予約メール関数を実行します。
Integrations Database Webhooks
Database Webhooks を開く Database Webhooks を有効にする 新しい webhook を作成する 名前を `send-booking-email-webhook` にする `public.appointments` テーブルを選択する イベントとして `Insert` を選ぶ タイプは `HTTP Request` のままにする リクエストメソッドは `POST` を使う Edge Function のエンドポイント URL を貼り付ける webhook を作成する
予約メールのしくみをテストする
予約メールのフロー全体をテストする
予約フロー全体をテストし、両方のメールが正しく配信されることを確認します。
公開中の予約サイトを開く テスト予約を送信する 予約が Supabase に保存されたことを確認する 顧客向けの確認メールを確認する オーナー向けの通知メールを確認する 両方のメールで予約内容が正しいことを確認する 予約が管理ダッシュボードに表示されることを確認する