关于 email-notifications
email-notifications 是帮助开发者在 Next.js 应用中实现邮件功能的技能,涵盖服务商配置、React Email 模板、触发条件、偏好设置、测试和送达跟踪。
主要功能
- 01配置 Resend、SendGrid 和 SMTP
- 02创建 React Email 模板
- 03设置注册与订单邮件触发
- 04处理邮件偏好与退订
- 05测试邮件并跟踪互动
适用场景
- 01发送注册欢迎邮件
- 02构建订单确认邮件
- 03创建定时邮件活动
- 04排查送达问题并预览模板
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill email-notifications安装后在 Claude Code 中输入/email-notifications确认已加载。
详细介绍
电子邮件通知技能
概述
此技能可帮助你在 Next.js 应用中实现电子邮件通知。从事务性邮件到定时营销活动,涵盖可靠发送邮件所需的一切。
电子邮件服务提供商
Resend(推荐用于 Next.js)
安装:
npm install resend
设置:
// lib/email.ts
import { Resend } from 'resend'
export const resend = new Resend(process.env.RESEND_API_KEY)
发送邮件:
// app/api/email/send/route.ts
import { resend } from '@/lib/email'
export async function POST(request: Request) {
const { to, subject, html } = await request.json()
try {
const { data, error } = await resend.emails.send({
from: '[email protected]',
to,
subject,
html,
})
if (error) {
return Response.json({ error }, { status: 400 })
}
return Response.json({ data })
} catch (error) {
return Response.json({ error }, { status: 500 })
}
}
SendGrid
安装:
npm install @sendgrid/mail
设置:
// lib/sendgrid.ts
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
export async function sendEmail({
to,
subject,
html,
}: {
to: string
subject: string
html: string
}) {
await sgMail.send({
from: '[email protected]',
to,
subject,
html,
})
}
Nodemailer(SMTP)
安装:
npm install nodemailer
设置:
// lib/nodemailer.ts
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT!),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
})
export async function sendEmail({
to,
subject,
html,
}: {
to: string
subject: string
html: string
}) {
await transporter.sendMail({
from: process.env.EMAIL_FROM,
to,
subject,
html,
})
}
React Email(电子邮件模板)
安装
npm install react-email @react-email/components
npm install -D @react-email/tailwind
创建电子邮件模板
// emails/welcome.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Link,
Preview,
Section,
Text,
Tailwind,
} from '@react-email/components'
interface WelcomeEmailProps {
name: string
loginUrl: string
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to our platform, {name}!</Preview>
<Tailwind>
<Body className="bg-gray-100 font-sans">
<Container className="bg-white mx-auto my-8 p-8 rounded-lg shadow-lg max-w-2xl">
<Heading className="text-2xl font-bold text-gray-900 mb-4">
Welcome, {name}!
</Heading>
<Text className="text-gray-700 mb-4">
We're excited to have you on board. Get started by logging in to your account.
</Text>
<Section className="text-center my-8">
<Button
href={loginUrl}
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold"
>
Log In to Your Account
</Button>
</Section>
<Text className="text-gray-600 text-sm">
If you didn't create this account, you can safely ignore this email.
</Text>
<Text className="text-gray-500 text-xs mt-8">
© 2024 Your Company. All rights reserved.
<br />
<Link href="https://yourcompany.com/unsubscribe">Unsubscribe</Link>
</Text>
</Container>
</Body>
</Tailwind>
</Html>
)
}
export default WelcomeEmail
渲染电子邮件模板
import { render } from '@react-email/components'
import { WelcomeEmail } from '@/emails/welcome'
import { resend } from '@/lib/email'
export async function sendWelcomeEmail(name: string, email: string) {
const html = render(
<WelcomeEmail
name={name}
loginUrl={`${process.env.NEXT_PUBLIC_APP_URL}/login`}
/>
)
await resend.emails.send({
from: '[email protected]',
to: email,
subject: `Welcome, ${name}!`,
html,
})
}
预览电子邮件(开发环境)
# Add script to package.json
{
"scripts": {
"email:dev": "email dev -p 3001"
}
}
# Run preview server
npm run email:dev
# Visit http://localhost:3001
常见电子邮件模板
密码重置
// emails/password-reset.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
Tailwind,
} from '@react-email/components'
interface PasswordResetEmailProps {
name: string
resetUrl: string
}
export function PasswordResetEmail({ name, resetUrl }: PasswordResetEmailProps) {
return (
<Html>
<Head />
<Preview>Reset your password</Preview>
<Tailwind>
<Body className="bg-gray-100 font-sans">
<Container className="bg-white mx-auto my-8 p-8 rounded-lg max-w-2xl">
<Heading className="text-2xl font-bold mb-4">
Reset Your Password
</Heading>
<Text className="text-gray-700 mb-4">
Hi {name}, we received a request to reset your password.
</Text>
<Section className="text-center my-8">
<Button
href={resetUrl}
className="bg-blue-600 text-white px-6 py-3 rounded-lg"
>
Reset Password
</Button>
</Section>
<Text className="text-gray-600 text-sm">
This link will expire in 1 hour. If you didn't request this, you can safely ignore this email.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
)
}
订单确认
// emails/order-confirmation.tsx
interface OrderConfirmationEmailProps {
name: string
orderId: string
items: Array<{ name: string; price: number; quantity: number }>
total: number
}
export function OrderConfirmationEmail({
name,
orderId,
items,
total,
}: OrderConfirmationEmailProps) {
return (
<Html>
<Head />
<Preview>Order confirmation #{orderId}</Preview>
<Tailwind>
<Body className="bg-gray-100 font-sans">
<Container className="bg-white mx-auto my-8 p-8 rounded-lg max-w-2xl">
<Heading className="text-2xl font-bold mb-4">
Order Confirmed!
</Heading>
<Text>Hi {name}, thanks for your order!</Text>
<Text className="text-gray-600">Order #{orderId}</Text>
<Section className="my-8">
{items.map((item, index) => (
<div key={index} className="flex justify-between py-2 border-b">
<Text>
{item.quantity}x {item.name}
</Text>
<Text>${item.price.toFixed(2)}</Text>
</div>
))}
<div className="flex justify-between py-4 font-bold">
<Text>Total</Text>
<Text>${total.toFixed(2)}</Text>
</div>
</Section>
<Button href={`${process.env.NEXT_PUBLIC_APP_URL}/orders/${orderId}`}>
View Order Details
</Button>
</Container>
</Body>
</Tailwind>
</Html>
)
}
电子邮件触发器
注册时发送欢迎邮件
// app/api/auth/signup/route.ts
import { sendWelcomeEmail } from '@/lib/emails/send'
export async function POST(request: Request) {
const { email, name } = await request.json()
// Create user
const user = await createUser({ email, name })
// Send welcome email (async, don't await)
sendWelcomeEmail(name, email).catch((error) => {
console.error('Failed to send welcome email:', error)
})
return Response.json({ user })
}
订单确认
// lib/emails/send.ts
import { render } from '@react-email/components'
import { OrderConfirmationEmail } from '@/emails/order-confirmation'
import { resend } from '@/lib/email'
export async function sendOrderConfirmation(order: Order) {
const html = render(
<OrderConfirmationEmail
name={order.customerName}
orderId={order.id}
items={order.items}
total={order.total}
/>
)
await resend.emails.send({
from: '[email protected]',
to: order.customerEmail,
subject: `Order Confirmation #${order.id}`,
html,
})
}
定时发送邮件
每日摘要
// app/api/cron/daily-digest/route.ts
import { sendDailyDigest } from '@/lib/emails/send'
export async function GET(request: Request) {
// Verify cron secret
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 })
}
// Get users who opted in for daily digest
const users = await db.users.findMany({
where: { emailPreferences: { dailyDigest: true } },
})
// Send emails in batches
for (const user of users) {
await sendDailyDigest(user)
}
return Response.json({ sent: users.length })
}
Vercel Cron 配置
// vercel.json
{
"crons": [
{
"path": "/api/cron/daily-digest",
"schedule": "0 9 * * *"
},
{
"path": "/api/cron/weekly-summary",
"schedule": "0 9 * * 1"
}
]
}
电子邮件队列(后台任务)
使用 Supabase Edge Functions
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { Resend } from 'npm:resend'
const resend = new Resend(Deno.env.get('RESEND_API_KEY'))
serve(async (req) => {
const { to, subject, html } = await req.json()
const { data, error } = await resend.emails.send({
from: '[email protected]',
to,
subject,
html,
})
return new Response(JSON.stringify({ data, error }), {
headers: { 'Content-Type': 'application/json' },
})
})
将邮件加入发送队列
import { createClient } from '@/lib/supabase/server'
export async function queueEmail({
to,
subject,
html,
}: {
to: string
subject: string
html: string
}) {
const supabase = createClient()
await supabase.from('email_queue').insert({
to,
subject,
html,
status: 'pending',
created_at: new Date().toISOString(),
})
}
// Process queue with cron
export async function processEmailQueue() {
const supabase = createClient()
const { data: emails } = await supabase
.from('email_queue')
.select('*')
.eq('status', 'pending')
.limit(10)
for (const email of emails || []) {
try {
await resend.emails.send({
from: '[email protected]',
to: email.to,
subject: email.subject,
html: email.html,
})
await supabase
.from('email_queue')
.update({ status: 'sent', sent_at: new Date().toISOString() })
.eq('id', email.id)
} catch (error) {
await supabase
.from('email_queue')
.update({
status: 'failed',
error: error.message,
attempts: email.attempts + 1,
})
.eq('id', email.id)
}
}
}
电子邮件偏好设置
用户电子邮件设置
// Database schema
type EmailPreferences = {
marketing: boolean
productUpdates: boolean
weeklyDigest: boolean
orderUpdates: boolean
}
// Update preferences
export async function updateEmailPreferences(
userId: string,
preferences: Partial<EmailPreferences>
) {
await db.users.update({
where: { id: userId },
data: { emailPreferences: preferences },
})
}
// Check before sending
export async function canSendEmail(
userId: string,
emailType: keyof EmailPreferences
): Promise<boolean> {
const user = await db.users.findUnique({
where: { id: userId },
select: { emailPreferences: true },
})
return user?.emailPreferences[emailType] ?? false
}
取消订阅链接
// app/api/unsubscribe/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const token = searchParams.get('token')
// Verify token and get user
const userId = await verifyUnsubscribeToken(token)
// Unsubscribe from all marketing emails
await db.users.update({
where: { id: userId },
data: {
emailPreferences: {
marketing: false,
productUpdates: false,
},
},
})
return new Response('You have been unsubscribed', { status: 200 })
}
// Generate unsubscribe link
function getUnsubscribeUrl(userId: string): string {
const token = generateUnsubscribeToken(userId)
return `${process.env.NEXT_PUBLIC_APP_URL}/api/unsubscribe?token=${token}`
}
电子邮件测试
在本地测试邮件
// lib/email-test.ts
import { sendWelcomeEmail } from '@/lib/emails/send'
async function testEmail() {
await sendWelcomeEmail('Test User', '[email protected]')
console.log('Test email sent!')
}
testEmail()
在浏览器中预览电子邮件
// app/email-preview/[template]/page.tsx
import { WelcomeEmail } from '@/emails/welcome'
export default function EmailPreview({
params,
}: {
params: { template: string }
}) {
const templates = {
welcome: <WelcomeEmail name="John Doe" loginUrl="/login" />,
// Add more templates
}
return (
<div className="p-8">
{templates[params.template]}
</div>
)
}
电子邮件分析
跟踪邮件打开情况
// emails/welcome.tsx
export function WelcomeEmail({ name, userId }: { name: string; userId: string }) {
const trackingPixel = `${process.env.NEXT_PUBLIC_APP_URL}/api/email/track/open?userId=${userId}&email=welcome`
return (
<Html>
{/* Email content */}
<img src={trackingPixel} width="1" height="1" alt="" />
</Html>
)
}
// app/api/email/track/open/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
const emailType = searchParams.get('email')
// Track email open
await db.emailEvents.create({
data: {
userId,
emailType,
event: 'open',
timestamp: new Date(),
},
})
// Return 1x1 transparent pixel
const pixel = Buffer.from(
'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
'base64'
)
return new Response(pixel, {
headers: {
'Content-Type': 'image/gif',
'Cache-Control': 'no-store',
},
})
}
跟踪链接点击情况
// Create tracked link
function createTrackedLink(url: string, userId: string, emailType: string): string {
return `${process.env.NEXT_PUBLIC_APP_URL}/api/email/track/click?url=${encodeURIComponent(url)}&userId=${userId}&email=${emailType}`
}
// Handle click tracking
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const url = searchParams.get('url')
const userId = searchParams.get('userId')
const emailType = searchParams.get('email')
// Track click
await db.emailEvents.create({
data: {
userId,
emailType,
event: 'click',
metadata: { url },
timestamp: new Date(),
},
})
// Redirect to actual URL
return Response.redirect(url!, 302)
}
最佳实践清单
- 使用事务性电子邮件服务提供商(Resend/SendGrid)
- 使用 React Email 创建电子邮件模板
- 实现取消订阅链接
- 尊重用户的电子邮件偏好设置
- 将邮件加入队列以便在后台处理
- 添加电子邮件跟踪(打开、点击)
- 发送前测试邮件
- 使用合适的发件人地址
- 包含纯文本版本
- 处理退信和投诉
- 为失败情况添加重试逻辑
- 监控邮件送达率
- 遵守电子邮件法规(CAN-SPAM、GDPR)
- 使用电子邮件身份验证(SPF、DKIM、DMARC)
何时使用此技能
在以下情况下调用此技能:
- 设置电子邮件通知
- 创建电子邮件模板
- 实现事务性邮件
- 构建电子邮件营销活动
- 设置定时邮件
- 实现电子邮件偏好设置
- 调试邮件送达问题
- 添加电子邮件跟踪
- 创建取消订阅流程
- 测试电子邮件模板
评论
支持 Markdown