ship-email について
既存アプリに Resend の設定、トランザクションメールのテンプレート、ユーザーセグメント、管理者向け配信 UI を追加するコーディングスキルです。フレームワーク、データベース、認証方式を確認し、プロジェクトに合わせて構成します。
主な機能
- 01フレームワーク、DB、認証方式の確認
- 02Resend と共通メールユーティリティの設定
- 03トランザクションメールテンプレートの作成
- 04キャンペーン向けユーザーセグメント
- 05認証付き管理者配信 UI
- 06配信停止処理と検証手順
活用シーン
- 01登録時のウェルカムメールなどの追加
- 02対象ユーザーセグメントへのキャンペーン配信
- 03メール障害によるアプリ処理の中断防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill ship-emailインストール後、Claude Code で/ship-emailを入力して読み込まれたか確認します。
同梱ファイルreferences/guide.md
詳細
メールインフラ全体を構築します。Resend のセットアップ、トランザクションメールテンプレート、ユーザーセグメンテーション、管理者向け送信 UI を含みます。まずプロジェクトを読み込み、既存の認証とデータベースに組み込みます。
Resend 連携を気付かないうちに壊してしまう 3 つのこと
- 本番稼働前にドメインを認証しない。 Resend はデフォルトで
@resend.devから送信します。メールが迷惑メールに振り分けられたり、完全にブロックされたりします。実際のユーザーにメールを送る前に、Resend ダッシュボードでドメインを認証し、fromアドレスが認証済みドメインと一致していることを確認してください。 - メール送信の失敗時に例外をスローする。
sendEmailが例外をスローし、それを捕捉しないと、ウェルカムメールの送信失敗で登録フローがクラッシュします。メール送信はファイア・アンド・フォーゲットです。必ず例外を捕捉し、必ず{ success, error }を返し、例外は決してスローしないでください。 reply-toを設定せず、noreplyアドレスから送信する。 ユーザーは質問があると返信します。返信先に確認されている受信トレイがなければ、ユーザーを失うことになります。fromがnoreply@でも、replyToには実際の受信トレイを設定してください。
フェーズ 1:プロジェクトを検出する
何かを書く前に、コードベースを読み込みます。
1.1 スタックの検出
- フレームワーク:Next.js/Express/FastAPI/その他?
- データベース:Supabase/Prisma/Drizzle/Mongoose/生 SQL?
- 認証:ユーザーを識別するフィールドは何ですか?(
clerk_user_id、email、id) - ユーザーテーブルのスキーマ:どのフィールドがありますか?(
email、name、credits、plan、created_at、last_active_at)
1.2 ユーザーに確認する
構築を始める前に、次の点を確認します。
I'll wire email for your [framework] app with [database].
Quick decisions:
1. What emails do you need? (transactional, campaigns, or both)
2. Sender: From name and from email? (e.g., "Tushar from Bangers Only <[email protected]>")
3. Domain verified in Resend? (yes / need to set it up)
Defaults: welcome email + re-engagement campaign, Resend.
フェーズ 2:プロバイダーのセットアップ
Resend のインストールと設定
npm install resend
.env.example に追加します。
RESEND_API_KEY=re_xxxxx
メールユーティリティを作成します。コードベース内のすべてのメールは、このユーティリティを経由します。
// lib/email.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailParams {
to: string | string[];
subject: string;
html: string;
from?: string;
replyTo?: string;
}
export async function sendEmail({
to,
subject,
html,
from = 'Your Name <[email protected]>',
replyTo,
}: SendEmailParams) {
try {
const result = await resend.emails.send({ from, to, subject, html, reply_to: replyTo });
return { success: true, id: result.data?.id };
} catch (error) {
console.error('Email send failed:', error);
return { success: false, error };
}
}
メールユーティリティから例外をスローしないでください。 メール送信の失敗でメインのリクエストフローをクラッシュさせてはいけません。{ success, error } を返し、呼び出し元で判断します。
ドメイン認証のチェックリスト
本番環境に移行する前に、Resend ダッシュボードで次の作業を完了するようユーザーに伝えてください。
[ ] Add MX record to DNS
[ ] Add SPF TXT record: v=spf1 include:resend.com ~all
[ ] Add DKIM TXT records (Resend provides these in dashboard)
[ ] Verify domain status shows "Verified" in Resend dashboard
[ ] Set reply-to to a monitored inbox (not noreply)
ドメインが認証されていないと、メールが迷惑メールに振り分けられたり、まったく送信されなかったりします。
フェーズ 3:メールテンプレート
コードベースに存在するイベント向けのテンプレートを作成します。最低限、次のテンプレートを用意します。
ウェルカムメール(ユーザー登録時に送信)
// lib/emails/welcome.ts
export function welcomeEmail({ name, ctaUrl }: { name: string; ctaUrl: string }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>You're in. Here's what to do first:</p>
<p>
<a href="${ctaUrl}"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Get started
</a>
</p>
<p>Any questions — just reply to this email.</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}
再エンゲージメントメール(7 日以上利用していないユーザーに送信)
// lib/emails/reengagement.ts
export function reengagementEmail({ name, daysSinceActive }: { name: string; daysSinceActive: number }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>Haven't seen you in ${daysSinceActive} days. [Product] has [one improvement since they last used it].</p>
<p>
<a href="[APP_URL]"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Pick up where you left off
</a>
</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}
テンプレートのルール
- 飾りすぎず、プレーンテキストのような印象にする
- 1 通につき CTA は 1 つにする
- 「The Team」ではなく、人名で署名する
- キャンペーン向けのテンプレートには、すべて配信停止リンクを含める
- インラインスタイルのみを使用し、外部スタイルシートは使用しない(スパムフィルターによって削除されるため)
ウェルカムメールを登録フローに組み込む
登録/認証の同期エンドポイントを見つけ、ユーザーの作成後に sendEmail を呼び出します。
// After creating the new user:
if (isNewUser) {
await sendEmail({
to: user.email,
subject: 'Welcome',
html: welcomeEmail({ name: user.name || 'there', ctaUrl: process.env.APP_URL! }),
});
}
フェーズ 4:ユーザーセグメンテーション
ユーザーテーブルのスキーマを読み込み、利用可能なフィールドに基づいてセグメントを生成します。
| テーブルにあるフィールド... | 生成するセグメント |
|---|---|
credits/usage_count | パワーユーザー(上位 20%)、アクティブ(中位 60%)、非アクティブ(下位 20%) |
created_at | 新規(7 日未満)、定着(7〜30 日)、ベテラン(30 日以上) |
plan/subscription_tier | 無料、有料、解約済み |
last_active_at | アクティブ(7 日未満)、休眠(7〜30 日)、解約済み(30 日以上) |
存在する各セグメントについて、実際の SQL/ORM クエリを生成します。クレジットベースのアプリの例:
-- Dormant users: used some credits but went quiet
SELECT email, name
FROM users
WHERE (initial_credits - credits) > 0
AND last_active_at < NOW() - INTERVAL '7 days'
AND email_unsubscribed = false;
フェーズ 5:管理者向けキャンペーン UI
キャンペーンが必要な場合は、管理者用のルートを構築します。
POST /api/admin/send-campaign
Body: { segment: string, templateId: string }
ハンドラーのロジック:
- 管理者認証を確認する。この確認は絶対に省略しないでください
- セグメント内のユーザーをクエリする
- ループして送信する。メールごとのエラーを捕捉し、1 件の失敗でバッチ全体を中断しない
{ sentCount, failedEmails, errors }を返す
// api/admin/send-campaign/route.ts
export async function POST(req: Request) {
const adminSecret = req.headers.get('x-admin-secret');
if (adminSecret !== process.env.ADMIN_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { segment, templateId } = await req.json();
const users = await getUsersInSegment(segment);
let sentCount = 0;
const failedEmails: string[] = [];
for (const user of users) {
const result = await sendEmail({
to: user.email,
subject: getSubjectForTemplate(templateId),
html: renderTemplate(templateId, user),
});
if (result.success) {
sentCount++;
} else {
failedEmails.push(user.email);
}
}
return Response.json({ sentCount, failedEmails });
}
レート制限に注意:Resend の無料プランは 1 日 100 通です。有料プランは月額 20 ドルで 1 日 50,000 通から利用できます。セグメントのユーザー数が 1 日の上限を超える場合は、バッチ送信に遅延を追加するか、ユーザーに警告してください。
フェーズ 6:配信停止の仕組み
すべてのキャンペーンメールには、機能する配信停止リンクが必要です。これは任意ではありません。法律上の要件であり、配信可能性を保つためにも必要です。
// lib/unsubscribe.ts
import { createHmac } from 'crypto';
export function generateUnsubToken(email: string): string {
return createHmac('sha256', process.env.UNSUB_SECRET!)
.update(email)
.digest('hex')
.slice(0, 16);
}
export function unsubUrl(email: string): string {
const token = generateUnsubToken(email);
return `${process.env.APP_URL}/unsubscribe?email=${encodeURIComponent(email)}&token=${token}`;
}
存在しない場合は、users テーブルに email_unsubscribed boolean DEFAULT false を追加します。配信停止ルートではこれを true に設定し、すべてのセグメントクエリで email_unsubscribed = false による絞り込みを行う必要があります。
フェーズ 7:確認
Flow 1: Transactional
[ ] User signs up → welcome email arrives in inbox (not spam)
[ ] Email shows from verified domain, not @resend.dev
[ ] Reply-to is a real inbox
Flow 2: Campaigns
[ ] Admin send-campaign endpoint requires auth
[ ] Segment query returns correct users
[ ] Failed individual emails don't abort the batch
[ ] Response shows sentCount and failedEmails
Flow 3: Unsubscribe
[ ] Unsubscribe link in every campaign email
[ ] Clicking it marks user as unsubscribed in DB
[ ] Unsubscribed users excluded from future segments
Flow 4: Edge Cases
[ ] sendEmail never throws — returns { success: false } on failure
[ ] Email sending doesn't block the main signup flow
[ ] RESEND_API_KEY in .env.example (not in code)
重要な注意事項
- メールアドレスをハードコードしないでください。 送信元と返信先には環境変数を使用してください。
- ウェルカムメールの送信タイミングは重要です。 バックグラウンドで送信し、メールの配信が完了するまで登録を待たせないでください。
- ライブ API キーに切り替える前に、Resend のテストモードでテストしてください。
- 1 つのイベントにつきメールは 1 通です。 同じトリガーで複数のメールを送信しないでください。ユーザーは気づきます。
セグメントごとのメール文面、件名のパターン、レート制限の方法については、references/guide.md を参照してください。