- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
594 lines
14 KiB
Markdown
594 lines
14 KiB
Markdown
# Email Automation mit Resend & React Email
|
||
|
||
**Meta-Description:** Moderne Email Automation. React Email Templates, Resend API, Transactional Emails und Email Sequenzen.
|
||
|
||
**Keywords:** Email Automation, Resend, React Email, Transactional Email, Email Templates, Nodemailer, SMTP
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
Email bleibt der wichtigste Kommunikationskanal. Mit **React Email** und **Resend** werden Email-Templates zu First-Class React Components – testbar, typsicher und versioniert.
|
||
|
||
---
|
||
|
||
## Email Stack 2026
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ MODERN EMAIL STACK │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Template Layer: │
|
||
│ ├── React Email (JSX → HTML) │
|
||
│ ├── MJML Alternative │
|
||
│ └── Tailwind Email Support │
|
||
│ │
|
||
│ Delivery Layer: │
|
||
│ ├── Resend (Developer-First) │
|
||
│ ├── SendGrid │
|
||
│ ├── Postmark │
|
||
│ └── AWS SES │
|
||
│ │
|
||
│ Features: │
|
||
│ ├── Transactional (Welcome, Reset, etc.) │
|
||
│ ├── Marketing (Newsletters) │
|
||
│ ├── Sequences (Drip Campaigns) │
|
||
│ └── Webhooks (Delivery Status) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## React Email Setup
|
||
|
||
```bash
|
||
npm install @react-email/components react-email resend
|
||
```
|
||
|
||
```typescript
|
||
// emails/welcome.tsx
|
||
import {
|
||
Html,
|
||
Head,
|
||
Preview,
|
||
Body,
|
||
Container,
|
||
Section,
|
||
Text,
|
||
Button,
|
||
Img,
|
||
Hr,
|
||
Link
|
||
} from '@react-email/components';
|
||
|
||
interface WelcomeEmailProps {
|
||
userName: string;
|
||
verificationUrl: string;
|
||
}
|
||
|
||
export default function WelcomeEmail({
|
||
userName,
|
||
verificationUrl
|
||
}: WelcomeEmailProps) {
|
||
return (
|
||
<Html>
|
||
<Head />
|
||
<Preview>Willkommen bei unserer Plattform, {userName}!</Preview>
|
||
|
||
<Body style={main}>
|
||
<Container style={container}>
|
||
<Img
|
||
src="https://example.com/logo.png"
|
||
width="120"
|
||
height="40"
|
||
alt="Logo"
|
||
style={logo}
|
||
/>
|
||
|
||
<Section style={content}>
|
||
<Text style={heading}>Willkommen, {userName}! 👋</Text>
|
||
|
||
<Text style={paragraph}>
|
||
Vielen Dank für Ihre Registrierung. Bitte bestätigen Sie Ihre
|
||
E-Mail-Adresse, um alle Funktionen nutzen zu können.
|
||
</Text>
|
||
|
||
<Button style={button} href={verificationUrl}>
|
||
E-Mail bestätigen
|
||
</Button>
|
||
|
||
<Text style={paragraph}>
|
||
Oder kopieren Sie diesen Link in Ihren Browser:
|
||
</Text>
|
||
|
||
<Text style={link}>{verificationUrl}</Text>
|
||
</Section>
|
||
|
||
<Hr style={hr} />
|
||
|
||
<Section style={footer}>
|
||
<Text style={footerText}>
|
||
© 2026 Unser Unternehmen. Alle Rechte vorbehalten.
|
||
</Text>
|
||
<Link href="https://example.com/unsubscribe" style={footerLink}>
|
||
Abmelden
|
||
</Link>
|
||
</Section>
|
||
</Container>
|
||
</Body>
|
||
</Html>
|
||
);
|
||
}
|
||
|
||
// Styles
|
||
const main = {
|
||
backgroundColor: '#f6f9fc',
|
||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||
};
|
||
|
||
const container = {
|
||
backgroundColor: '#ffffff',
|
||
margin: '0 auto',
|
||
padding: '20px 0',
|
||
maxWidth: '600px'
|
||
};
|
||
|
||
const logo = {
|
||
margin: '0 auto 20px',
|
||
display: 'block'
|
||
};
|
||
|
||
const content = {
|
||
padding: '0 40px'
|
||
};
|
||
|
||
const heading = {
|
||
fontSize: '24px',
|
||
fontWeight: 'bold',
|
||
color: '#1a1a1a',
|
||
marginBottom: '20px'
|
||
};
|
||
|
||
const paragraph = {
|
||
fontSize: '16px',
|
||
lineHeight: '26px',
|
||
color: '#484848'
|
||
};
|
||
|
||
const button = {
|
||
backgroundColor: '#5046e5',
|
||
borderRadius: '8px',
|
||
color: '#ffffff',
|
||
fontSize: '16px',
|
||
fontWeight: 'bold',
|
||
textDecoration: 'none',
|
||
textAlign: 'center' as const,
|
||
display: 'block',
|
||
padding: '14px 24px',
|
||
margin: '20px 0'
|
||
};
|
||
|
||
const link = {
|
||
color: '#5046e5',
|
||
fontSize: '14px',
|
||
wordBreak: 'break-all' as const
|
||
};
|
||
|
||
const hr = {
|
||
borderColor: '#e6ebf1',
|
||
margin: '20px 0'
|
||
};
|
||
|
||
const footer = {
|
||
padding: '0 40px'
|
||
};
|
||
|
||
const footerText = {
|
||
fontSize: '12px',
|
||
color: '#9ca299'
|
||
};
|
||
|
||
const footerLink = {
|
||
color: '#9ca299',
|
||
fontSize: '12px'
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Resend Integration
|
||
|
||
```typescript
|
||
// lib/email.ts
|
||
import { Resend } from 'resend';
|
||
import WelcomeEmail from '@/emails/welcome';
|
||
import PasswordResetEmail from '@/emails/password-reset';
|
||
import OrderConfirmationEmail from '@/emails/order-confirmation';
|
||
|
||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||
|
||
// Single Email senden
|
||
export async function sendWelcomeEmail(
|
||
to: string,
|
||
userName: string,
|
||
verificationUrl: string
|
||
) {
|
||
const { data, error } = await resend.emails.send({
|
||
from: 'Unser Team <noreply@example.com>',
|
||
to,
|
||
subject: `Willkommen, ${userName}!`,
|
||
react: WelcomeEmail({ userName, verificationUrl })
|
||
});
|
||
|
||
if (error) {
|
||
console.error('Email send error:', error);
|
||
throw error;
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
// Password Reset
|
||
export async function sendPasswordResetEmail(
|
||
to: string,
|
||
resetUrl: string
|
||
) {
|
||
return resend.emails.send({
|
||
from: 'Sicherheit <security@example.com>',
|
||
to,
|
||
subject: 'Passwort zurücksetzen',
|
||
react: PasswordResetEmail({ resetUrl })
|
||
});
|
||
}
|
||
|
||
// Order Confirmation mit Anhang
|
||
export async function sendOrderConfirmation(
|
||
to: string,
|
||
order: Order,
|
||
invoicePdf: Buffer
|
||
) {
|
||
return resend.emails.send({
|
||
from: 'Shop <orders@example.com>',
|
||
to,
|
||
subject: `Bestellbestätigung #${order.id}`,
|
||
react: OrderConfirmationEmail({ order }),
|
||
attachments: [
|
||
{
|
||
filename: `Rechnung-${order.id}.pdf`,
|
||
content: invoicePdf
|
||
}
|
||
]
|
||
});
|
||
}
|
||
|
||
// Batch Send
|
||
export async function sendBulkEmails(
|
||
recipients: Array<{ email: string; data: any }>
|
||
) {
|
||
const emails = recipients.map(r => ({
|
||
from: 'Newsletter <news@example.com>',
|
||
to: r.email,
|
||
subject: 'Unser Newsletter',
|
||
react: NewsletterEmail(r.data)
|
||
}));
|
||
|
||
return resend.batch.send(emails);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Email Sequenzen (Drip Campaigns)
|
||
|
||
```typescript
|
||
import { Queue, Worker } from 'bullmq';
|
||
import IORedis from 'ioredis';
|
||
|
||
const connection = new IORedis(process.env.REDIS_URL!);
|
||
const emailQueue = new Queue('email-sequences', { connection });
|
||
|
||
// Sequence Definition
|
||
interface EmailSequence {
|
||
id: string;
|
||
name: string;
|
||
steps: Array<{
|
||
delay: number; // Millisekunden
|
||
template: string;
|
||
subject: string;
|
||
}>;
|
||
}
|
||
|
||
const onboardingSequence: EmailSequence = {
|
||
id: 'onboarding',
|
||
name: 'Onboarding Sequence',
|
||
steps: [
|
||
{
|
||
delay: 0,
|
||
template: 'welcome',
|
||
subject: 'Willkommen bei uns!'
|
||
},
|
||
{
|
||
delay: 24 * 60 * 60 * 1000, // 1 Tag
|
||
template: 'getting-started',
|
||
subject: 'Erste Schritte'
|
||
},
|
||
{
|
||
delay: 3 * 24 * 60 * 60 * 1000, // 3 Tage
|
||
template: 'tips',
|
||
subject: '5 Tipps für den Einstieg'
|
||
},
|
||
{
|
||
delay: 7 * 24 * 60 * 60 * 1000, // 7 Tage
|
||
template: 'feedback',
|
||
subject: 'Wie gefällt es Ihnen?'
|
||
}
|
||
]
|
||
};
|
||
|
||
// Sequence starten
|
||
async function startEmailSequence(
|
||
userId: string,
|
||
email: string,
|
||
sequenceId: string,
|
||
userData: Record<string, any>
|
||
) {
|
||
const sequence = getSequenceById(sequenceId);
|
||
if (!sequence) throw new Error(`Sequence ${sequenceId} not found`);
|
||
|
||
// Sequence State speichern
|
||
await db.emailSequenceState.create({
|
||
data: {
|
||
userId,
|
||
sequenceId,
|
||
currentStep: 0,
|
||
status: 'active'
|
||
}
|
||
});
|
||
|
||
// Jobs für jeden Step einplanen
|
||
for (let i = 0; i < sequence.steps.length; i++) {
|
||
const step = sequence.steps[i];
|
||
|
||
await emailQueue.add(
|
||
'send-sequence-email',
|
||
{
|
||
userId,
|
||
email,
|
||
sequenceId,
|
||
stepIndex: i,
|
||
template: step.template,
|
||
subject: step.subject,
|
||
userData
|
||
},
|
||
{
|
||
delay: step.delay,
|
||
jobId: `${userId}-${sequenceId}-${i}`
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
// Worker für Sequence Emails
|
||
const worker = new Worker('email-sequences', async (job) => {
|
||
const { userId, email, sequenceId, stepIndex, template, subject, userData } = job.data;
|
||
|
||
// Prüfen ob Sequence noch aktiv
|
||
const state = await db.emailSequenceState.findFirst({
|
||
where: { userId, sequenceId }
|
||
});
|
||
|
||
if (state?.status !== 'active') {
|
||
console.log(`Sequence ${sequenceId} for user ${userId} is not active, skipping`);
|
||
return;
|
||
}
|
||
|
||
// Email senden
|
||
const EmailComponent = getEmailTemplate(template);
|
||
|
||
await resend.emails.send({
|
||
from: 'Unser Team <team@example.com>',
|
||
to: email,
|
||
subject,
|
||
react: EmailComponent(userData)
|
||
});
|
||
|
||
// State updaten
|
||
await db.emailSequenceState.update({
|
||
where: { id: state.id },
|
||
data: { currentStep: stepIndex + 1 }
|
||
});
|
||
|
||
console.log(`Sent sequence email: ${template} to ${email}`);
|
||
}, { connection });
|
||
|
||
// Sequence abbrechen
|
||
async function cancelEmailSequence(userId: string, sequenceId: string) {
|
||
// State updaten
|
||
await db.emailSequenceState.updateMany({
|
||
where: { userId, sequenceId },
|
||
data: { status: 'cancelled' }
|
||
});
|
||
|
||
// Pending Jobs entfernen
|
||
const jobs = await emailQueue.getJobs(['delayed', 'waiting']);
|
||
for (const job of jobs) {
|
||
if (job.data.userId === userId && job.data.sequenceId === sequenceId) {
|
||
await job.remove();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Email Preview & Testing
|
||
|
||
```typescript
|
||
// email-preview.ts (Development)
|
||
import { render } from '@react-email/render';
|
||
import WelcomeEmail from '@/emails/welcome';
|
||
|
||
// HTML Preview generieren
|
||
const html = render(WelcomeEmail({
|
||
userName: 'Max Mustermann',
|
||
verificationUrl: 'https://example.com/verify?token=abc123'
|
||
}));
|
||
|
||
console.log(html);
|
||
|
||
// React Email Dev Server
|
||
// package.json
|
||
{
|
||
"scripts": {
|
||
"email:dev": "email dev --port 3001"
|
||
}
|
||
}
|
||
|
||
// Dann: npm run email:dev
|
||
// Browser: http://localhost:3001
|
||
```
|
||
|
||
```typescript
|
||
// Email Testing mit Jest
|
||
import { render } from '@react-email/render';
|
||
import WelcomeEmail from '@/emails/welcome';
|
||
|
||
describe('WelcomeEmail', () => {
|
||
it('renders with correct user name', () => {
|
||
const html = render(WelcomeEmail({
|
||
userName: 'Test User',
|
||
verificationUrl: 'https://example.com/verify'
|
||
}));
|
||
|
||
expect(html).toContain('Test User');
|
||
expect(html).toContain('Willkommen');
|
||
});
|
||
|
||
it('includes verification link', () => {
|
||
const html = render(WelcomeEmail({
|
||
userName: 'Test User',
|
||
verificationUrl: 'https://example.com/verify?token=123'
|
||
}));
|
||
|
||
expect(html).toContain('https://example.com/verify?token=123');
|
||
});
|
||
|
||
it('has valid HTML structure', () => {
|
||
const html = render(WelcomeEmail({
|
||
userName: 'Test User',
|
||
verificationUrl: 'https://example.com/verify'
|
||
}));
|
||
|
||
expect(html).toMatch(/<html/);
|
||
expect(html).toMatch(/<\/html>/);
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Webhook Handling (Delivery Status)
|
||
|
||
```typescript
|
||
// app/api/webhooks/resend/route.ts
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { Webhook } from 'svix';
|
||
|
||
const webhookSecret = process.env.RESEND_WEBHOOK_SECRET!;
|
||
|
||
export async function POST(request: NextRequest) {
|
||
const payload = await request.text();
|
||
const headers = {
|
||
'svix-id': request.headers.get('svix-id')!,
|
||
'svix-timestamp': request.headers.get('svix-timestamp')!,
|
||
'svix-signature': request.headers.get('svix-signature')!
|
||
};
|
||
|
||
const wh = new Webhook(webhookSecret);
|
||
|
||
let event: any;
|
||
try {
|
||
event = wh.verify(payload, headers);
|
||
} catch (err) {
|
||
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
||
}
|
||
|
||
// Event Type handling
|
||
switch (event.type) {
|
||
case 'email.sent':
|
||
await handleEmailSent(event.data);
|
||
break;
|
||
|
||
case 'email.delivered':
|
||
await handleEmailDelivered(event.data);
|
||
break;
|
||
|
||
case 'email.opened':
|
||
await handleEmailOpened(event.data);
|
||
break;
|
||
|
||
case 'email.clicked':
|
||
await handleEmailClicked(event.data);
|
||
break;
|
||
|
||
case 'email.bounced':
|
||
await handleEmailBounced(event.data);
|
||
break;
|
||
|
||
case 'email.complained':
|
||
await handleEmailComplained(event.data);
|
||
break;
|
||
}
|
||
|
||
return NextResponse.json({ received: true });
|
||
}
|
||
|
||
async function handleEmailBounced(data: any) {
|
||
// Email-Adresse als ungültig markieren
|
||
await db.user.update({
|
||
where: { email: data.to },
|
||
data: { emailStatus: 'bounced' }
|
||
});
|
||
}
|
||
|
||
async function handleEmailComplained(data: any) {
|
||
// Spam-Beschwerde: User aus Listen entfernen
|
||
await db.user.update({
|
||
where: { email: data.to },
|
||
data: { marketingOptIn: false }
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Moderne Email Automation braucht:
|
||
|
||
1. **React Email**: Component-basierte Templates
|
||
2. **Resend**: Developer-First API
|
||
3. **Sequences**: Automatisierte Drip Campaigns
|
||
4. **Webhooks**: Delivery Tracking
|
||
|
||
Email als Code – testbar, typsicher, versioniert.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Email template components assembling like Lego blocks, React Email concept"
|
||
2. "Email sequence timeline with automated sends, drip campaign visualization"
|
||
3. "Delivery status tracking with checkmarks, email analytics dashboard"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [React Email Documentation](https://react.email/)
|
||
- [Resend Documentation](https://resend.com/docs)
|
||
- [Email Accessibility Guidelines](https://www.emailonacid.com/blog/article/email-development/email-accessibilty-in-2019/)
|
||
- [Can I Email](https://www.caniemail.com/)
|