AgentSkillsCN

resend-integration

在将Resend集成至React Email模板,用于事务性邮件发送时加载此模块。适用于实施邮件发送、Webhook处理、域名验证,或批量邮件操作。

SKILL.md
--- frontmatter
name: resend-integration
description: Load when integrating Resend for transactional email with React Email templates. Applies when implementing email sending, webhook handling, domain verification, or batch email operations.

When This Rule Applies

Apply when implementing transactional email, marketing email, or any email sending with Resend.


React Email Templates

Template Component

typescript
// emails/order-confirmation.tsx
import { Html, Container, Heading, Text, Button } from '@react-email/components';

interface OrderProps {
  orderNumber: string;
  customerName: string;
  totalAmount: number;
}

export function OrderConfirmationEmail({ orderNumber, customerName, totalAmount }: OrderProps) {
  return (
    <Html lang="en">
      <Container style={{ maxWidth: '600px', margin: '0 auto' }}>
        <Heading>Thank you, {customerName}!</Heading>
        <Text>Order #{orderNumber} confirmed.</Text>
        <Text style={{ fontWeight: 'bold' }}>Total: ${totalAmount.toFixed(2)}</Text>
        <Button href={`https://yourapp.com/orders/${orderNumber}`}>
          View Order
        </Button>
      </Container>
    </Html>
  );
}

Sending with React Component

typescript
import { Resend } from 'resend';
import { OrderConfirmationEmail } from './emails/order-confirmation';

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: 'orders@yourdomain.com',
  to: customerEmail,
  subject: `Order Confirmation: #${orderNumber}`,
  react: <OrderConfirmationEmail {...props} />,
});

Webhook Handling

Next.js Webhook Endpoint

typescript
// app/api/webhooks/resend/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const signature = request.headers.get('x-resend-signature');
  const rawBody = await request.text();
  
  // Validate signature
  if (!validateSignature(rawBody, signature!, process.env.RESEND_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  
  const payload = JSON.parse(rawBody);
  
  switch (payload.type) {
    case 'email.delivered':
      await handleDelivered(payload.data);
      break;
    case 'email.bounced':
      await handleBounce(payload.data); // Remove from mailing list
      break;
    case 'email.complained':
      await handleComplaint(payload.data); // Unsubscribe user
      break;
  }
  
  return NextResponse.json({ success: true });
}

function validateSignature(payload: string, signature: string, secret: string): boolean {
  const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
}

Webhook Event Types

EventWhenAction
email.sentAPI request successfulLog attempt
email.deliveredReached recipient serverMark delivered
email.bouncedDelivery failedRemove from list
email.complainedMarked as spamUnsubscribe
email.delivery_delayedTemporary issueMonitor

Error Handling & Retries

Retry with Exponential Backoff

typescript
async function sendEmailWithRetry(params: EmailParams, maxAttempts = 5) {
  let delay = 500;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await resend.emails.send(params);
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      
      // Don't retry auth/validation errors
      if (isNonRetriable(error)) throw error;
      
      const jitter = Math.random() * delay * 0.1;
      await sleep(delay + jitter);
      delay = Math.min(delay * 2, 30000);
    }
  }
}

Idempotency Keys (Prevent Duplicates)

typescript
// Use deterministic key based on event + entity
const idempotencyKey = `order-confirmation/${orderId}`;

await resend.emails.send(
  { from, to, subject, react: <Template /> },
  { idempotencyKey }
);

// Same key + same payload = no duplicate send
// Same key + different payload = error (intentional)

Domain Verification

Required DNS Records

RecordTypeNameValue
SPFTXTsendv=spf1 include:amazonses.com ~all
DKIMTXTresend._domainkeyv=DKIM1; h=sha256; p=...
MXMXinboundinbound-smtp.us-east-1.amazonaws.com

Best Practice: Use Subdomains

typescript
// Use notifications.yourdomain.com instead of yourdomain.com
// - Isolates sending reputation
// - Makes intent clear to recipients
// - Reduces impact of deliverability issues

Disable Tracking for Transactional Emails

Open/click tracking can trigger spam filters. Disable for critical emails:

typescript
await resend.domains.create({
  name: 'notifications.yourdomain.com',
  // Disable tracking for better deliverability
  openTracking: false,
  clickTracking: false,
});

Batch Sending

Send Up to 100 Emails Per Request

typescript
const emails = recipients.map(recipient => ({
  from: 'marketing@yourdomain.com',
  to: recipient.email,
  subject: 'Newsletter',
  html: generateHTML(recipient.name),
}));

// Batch send (100 max per call)
const response = await resend.batch.send(emails);

Rate Limiting

Default: 2 requests/second. Check headers:

typescript
// Response headers
'ratelimit-limit': 2
'ratelimit-remaining': 1
'ratelimit-reset': 5
'retry-after': 5  // When rate limited

Common Gotchas

Webhook Duplicates

Resend uses "at least once" delivery. Implement idempotency:

typescript
const processed = await db.findEvent(webhookId);
if (processed) return; // Skip duplicate
await processWebhook(payload);
await db.markProcessed(webhookId);

60-Second Webhook Timeout

Return 200 immediately, process async:

typescript
await jobQueue.enqueue('processEmail', payload);
return NextResponse.json({ success: true }); // Return fast

Domain Not Verified

  • DNS propagation takes up to 24 hours
  • Use exact record values from Resend dashboard
  • Check for typos in record names

Emails Going to Spam

  • Enable SPF + DKIM (both required)
  • Disable tracking for transactional emails
  • Use consistent "from" address
  • Warm up new domains gradually

Quick Reference

TaskPattern
Send with React templatereact: <Component />
Prevent duplicates{ idempotencyKey } option
Batch sendresend.batch.send(emails)
Validate webhooksCheck x-resend-signature header
Handle bouncesWebhook → remove from list

References