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 に保存されたことを確認する 顧客向けの確認メールを確認する オーナー向けの通知メールを確認する 両方のメールで予約内容が正しいことを確認する 予約が管理ダッシュボードに表示されることを確認する