Kurs Next.js · Moduł 9: Integracje i zaawansowane funkcje
Next.js z Resend i React Email - Nowoczesny system mailowy
W tej lekcji13
W Metropolii Quantum, gdzie komunikacja odbywa się z prędkością światła, poznasz połączenie Next.js z Resend i React Email - najnowocześniejszym stackiem do wysyłania emaili. Ta kombinacja pozwala tworzyć piękne, responsywne emaile i wysyłać je z niezawodnością enterprise-grade.
Czym jest Resend?
Resend to nowoczesna platforma do wysyłania emaili, stworzona przez zespół deweloperów dla deweloperów:
- Developer Experience - czyste API i świetna dokumentacja
- React Email integration - natywne wsparcie dla komponentów React
- High deliverability - zaawansowane mechanizmy dostarczalności
- Analytics - szczegółowe statystyki dostarczania
- Webhooks - real-time powiadomienia o statusie emaili
- Templates - system szablonów z React
Czym jest React Email?
React Email to biblioteka do tworzenia emaili przy użyciu React:
- Component-based - tworzenie emaili jak zwykłych komponentów React
- TypeScript support - pełne wsparcie typowania
- Preview - podgląd emaili w przeglądarce
- Cross-client compatibility - kompatybilność z wszystkimi klientami email
- Export to HTML - konwersja do statycznego HTML
Instalacja i konfiguracja
Krok 1: Utworzenie konta Resend
- Zarejestruj się na resend.com
- Zweryfikuj domenę lub użyj domeny testowej
- Utwórz API key w Dashboard → API Keys
Krok 2: Instalacja pakietów
1# Zainstaluj Resend i React Email
2npm install resend react-email @react-email/components
3
4# Dla development dodaj React Email CLI
5npm install -D @react-email/cliKrok 3: Konfiguracja zmiennych środowiskowych
Utwórz plik .env.local:
1# Resend API Key
2RESEND_API_KEY=re_123456789
3
4# Email settings
5FROM_EMAIL=noreply@yourdomain.com
6REPLY_TO_EMAIL=support@yourdomain.comKrok 4: Setup React Email
Dodaj skrypty do package.json:
1{
2 "scripts": {
3 "email:dev": "email dev",
4 "email:export": "email export"
5 }
6}Utwórz konfigurację react-email.config.js:
1module.exports = {
2 outDir: './out',
3 pretty: true,
4 plainText: true,
5};Podstawowa integracja
Krok 1: Inicjalizacja Resend
Utwórz instancję Resend:
1// lib/resend.ts
2import { Resend } from 'resend';
3
4if (!process.env.RESEND_API_KEY) {
5 throw new Error('RESEND_API_KEY is not defined');
6}
7
8export const resend = new Resend(process.env.RESEND_API_KEY);Krok 2: Struktura folderów
Utwórz strukturę dla emaili:
1emails/
2├── components/
3│ ├── Header.tsx
4│ ├── Footer.tsx
5│ └── Button.tsx
6├── templates/
7│ ├── welcome.tsx
8│ ├── password-reset.tsx
9│ └── order-confirmation.tsx
10└── index.tsTworzenie komponentów emaili
Komponenty podstawowe
1// emails/components/Header.tsx
2import { Img, Section, Text } from '@react-email/components';
3
4interface HeaderProps {
5 logoUrl?: string;
6 companyName: string;
7}
8
9export function Header({ logoUrl, companyName }: HeaderProps) {
10 return (
11 <Section style={headerStyle}>
12 {logoUrl && (
13 <Img
14 src={logoUrl}
15 width="150"
16 height="50"
17 alt={companyName}
18 style={logoStyle}
19 />
20 )}
21 <Text style={companyStyle}>{companyName}</Text>
22 </Section>
23 );
24}
25
26const headerStyle = {
27 backgroundColor: '#f8f9fa',
28 padding: '20px',
29 textAlign: 'center' as const,
30};
31
32const logoStyle = {
33 margin: '0 auto',
34 marginBottom: '10px',
35};
36
37const companyStyle = {
38 fontSize: '24px',
39 fontWeight: 'bold',
40 color: '#333333',
41 margin: '0',
42};1// emails/components/Button.tsx
2import { Button as EmailButton } from '@react-email/components';
3
4interface ButtonProps {
5 href: string;
6 children: React.ReactNode;
7 variant?: 'primary' | 'secondary';
8}
9
10export function Button({ href, children, variant = 'primary' }: ButtonProps) {
11 const style = variant === 'primary' ? primaryButtonStyle : secondaryButtonStyle;
12
13 return (
14 <EmailButton href={href} style={style}>
15 {children}
16 </EmailButton>
17 );
18}
19
20const primaryButtonStyle = {
21 backgroundColor: '#007bff',
22 color: '#ffffff',
23 padding: '12px 24px',
24 borderRadius: '6px',
25 textDecoration: 'none',
26 display: 'inline-block',
27 fontWeight: 'bold',
28};
29
30const secondaryButtonStyle = {
31 backgroundColor: '#6c757d',
32 color: '#ffffff',
33 padding: '12px 24px',
34 borderRadius: '6px',
35 textDecoration: 'none',
36 display: 'inline-block',
37 fontWeight: 'bold',
38};1// emails/components/Footer.tsx
2import { Section, Text, Link } from '@react-email/components';
3
4interface FooterProps {
5 companyName: string;
6 unsubscribeUrl?: string;
7 address?: string;
8}
9
10export function Footer({ companyName, unsubscribeUrl, address }: FooterProps) {
11 return (
12 <Section style={footerStyle}>
13 <Text style={footerTextStyle}>
14 © 2024 {companyName}. Wszystkie prawa zastrzeżone.
15 </Text>
16
17 {address && (
18 <Text style={addressStyle}>
19 {address}
20 </Text>
21 )}
22
23 {unsubscribeUrl && (
24 <Text style={unsubscribeStyle}>
25 <Link href={unsubscribeUrl} style={linkStyle}>
26 Wypisz się z newslettera
27 </Link>
28 </Text>
29 )}
30 </Section>
31 );
32}
33
34const footerStyle = {
35 backgroundColor: '#f8f9fa',
36 padding: '20px',
37 textAlign: 'center' as const,
38 marginTop: '40px',
39};
40
41const footerTextStyle = {
42 fontSize: '14px',
43 color: '#6c757d',
44 margin: '0 0 10px',
45};
46
47const addressStyle = {
48 fontSize: '12px',
49 color: '#6c757d',
50 margin: '0 0 10px',
51};
52
53const unsubscribeStyle = {
54 fontSize: '12px',
55 color: '#6c757d',
56 margin: '0',
57};
58
59const linkStyle = {
60 color: '#007bff',
61 textDecoration: 'underline',
62};Szablony emaili
Email powitalny
1// emails/templates/welcome.tsx
2import {
3 Html,
4 Head,
5 Body,
6 Container,
7 Section,
8 Text,
9 Heading,
10 Hr,
11} from '@react-email/components';
12import { Header } from '../components/Header';
13import { Button } from '../components/Button';
14import { Footer } from '../components/Footer';
15
16interface WelcomeEmailProps {
17 userFirstName: string;
18 loginUrl: string;
19 companyName: string;
20}
21
22export default function WelcomeEmail({
23 userFirstName,
24 loginUrl,
25 companyName,
26}: WelcomeEmailProps) {
27 return (
28 <Html>
29 <Head />
30 <Body style={bodyStyle}>
31 <Container style={containerStyle}>
32 <Header companyName={companyName} />
33
34 <Section style={contentStyle}>
35 <Heading style={headingStyle}>
36 Witaj, {userFirstName}!
37 </Heading>
38
39 <Text style={textStyle}>
40 Dziękujemy za rejestrację w {companyName}! Jesteśmy podekscytowani,
41 że dołączyłeś do naszej społeczności.
42 </Text>
43
44 <Text style={textStyle}>
45 Aby rozpocząć korzystanie z naszej platformy, kliknij poniższy przycisk:
46 </Text>
47
48 <Section style={buttonSectionStyle}>
49 <Button href={loginUrl}>
50 Zaloguj się do konta
51 </Button>
52 </Section>
53
54 <Hr style={hrStyle} />
55
56 <Text style={helpTextStyle}>
57 Jeśli masz pytania, nie wahaj się <a href="mailto:support@example.com" style={linkStyle}>skontaktować z nami</a>.
58 </Text>
59 </Section>
60
61 <Footer
62 companyName={companyName}
63 address="ul. Przykładowa 123, 00-000 Warszawa"
64 />
65 </Container>
66 </Body>
67 </Html>
68 );
69}
70
71const bodyStyle = {
72 backgroundColor: '#ffffff',
73 fontFamily: 'Arial, sans-serif',
74};
75
76const containerStyle = {
77 maxWidth: '600px',
78 margin: '0 auto',
79};
80
81const contentStyle = {
82 padding: '40px 20px',
83};
84
85const headingStyle = {
86 fontSize: '28px',
87 color: '#333333',
88 textAlign: 'center' as const,
89 marginBottom: '30px',
90};
91
92const textStyle = {
93 fontSize: '16px',
94 color: '#333333',
95 lineHeight: '1.6',
96 marginBottom: '20px',
97};
98
99const buttonSectionStyle = {
100 textAlign: 'center' as const,
101 marginTop: '30px',
102 marginBottom: '30px',
103};
104
105const hrStyle = {
106 borderColor: '#e9ecef',
107 margin: '30px 0',
108};
109
110const helpTextStyle = {
111 fontSize: '14px',
112 color: '#6c757d',
113 textAlign: 'center' as const,
114};
115
116const linkStyle = {
117 color: '#007bff',
118 textDecoration: 'underline',
119};Email resetowania hasła
1// emails/templates/password-reset.tsx
2import {
3 Html,
4 Head,
5 Body,
6 Container,
7 Section,
8 Text,
9 Heading,
10 Code,
11} from '@react-email/components';
12import { Header } from '../components/Header';
13import { Button } from '../components/Button';
14import { Footer } from '../components/Footer';
15
16interface PasswordResetEmailProps {
17 userFirstName: string;
18 resetUrl: string;
19 resetCode?: string;
20 companyName: string;
21 expirationTime: string;
22}
23
24export default function PasswordResetEmail({
25 userFirstName,
26 resetUrl,
27 resetCode,
28 companyName,
29 expirationTime,
30}: PasswordResetEmailProps) {
31 return (
32 <Html>
33 <Head />
34 <Body style={bodyStyle}>
35 <Container style={containerStyle}>
36 <Header companyName={companyName} />
37
38 <Section style={contentStyle}>
39 <Heading style={headingStyle}>
40 Resetowanie hasła
41 </Heading>
42
43 <Text style={textStyle}>
44 Cześć {userFirstName},
45 </Text>
46
47 <Text style={textStyle}>
48 Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta w {companyName}.
49 Jeśli to nie Ty, zignoruj tego emaila.
50 </Text>
51
52 <Section style={buttonSectionStyle}>
53 <Button href={resetUrl}>
54 Zresetuj hasło
55 </Button>
56 </Section>
57
58 {resetCode && (
59 <Section style={codeSectionStyle}>
60 <Text style={codeTextStyle}>
61 Lub użyj tego kodu:
62 </Text>
63 <Code style={codeStyle}>{resetCode}</Code>
64 </Section>
65 )}
66
67 <Text style={warningTextStyle}>
68 Ten link wygaśnie za {expirationTime}.
69 </Text>
70
71 <Text style={securityTextStyle}>
72 Ze względów bezpieczeństwa, jeśli nie prosiłeś o reset hasła,
73 <a href="mailto:security@example.com" style={linkStyle}> skontaktuj się z nami natychmiast</a>.
74 </Text>
75 </Section>
76
77 <Footer companyName={companyName} />
78 </Container>
79 </Body>
80 </Html>
81 );
82}
83
84const bodyStyle = {
85 backgroundColor: '#ffffff',
86 fontFamily: 'Arial, sans-serif',
87};
88
89const containerStyle = {
90 maxWidth: '600px',
91 margin: '0 auto',
92};
93
94const contentStyle = {
95 padding: '40px 20px',
96};
97
98const headingStyle = {
99 fontSize: '28px',
100 color: '#333333',
101 textAlign: 'center' as const,
102 marginBottom: '30px',
103};
104
105const textStyle = {
106 fontSize: '16px',
107 color: '#333333',
108 lineHeight: '1.6',
109 marginBottom: '20px',
110};
111
112const buttonSectionStyle = {
113 textAlign: 'center' as const,
114 marginTop: '30px',
115 marginBottom: '30px',
116};
117
118const codeSectionStyle = {
119 textAlign: 'center' as const,
120 marginBottom: '30px',
121 padding: '20px',
122 backgroundColor: '#f8f9fa',
123 borderRadius: '8px',
124};
125
126const codeTextStyle = {
127 fontSize: '14px',
128 color: '#6c757d',
129 marginBottom: '10px',
130};
131
132const codeStyle = {
133 fontSize: '24px',
134 fontWeight: 'bold',
135 letterSpacing: '4px',
136 color: '#007bff',
137};
138
139const warningTextStyle = {
140 fontSize: '14px',
141 color: '#dc3545',
142 textAlign: 'center' as const,
143 marginBottom: '20px',
144 padding: '15px',
145 backgroundColor: '#f8d7da',
146 borderRadius: '6px',
147};
148
149const securityTextStyle = {
150 fontSize: '12px',
151 color: '#6c757d',
152 textAlign: 'center' as const,
153};
154
155const linkStyle = {
156 color: '#007bff',
157 textDecoration: 'underline',
158};API Routes dla wysyłania emaili
Podstawowe API
1// app/api/send-email/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { resend } from '@/lib/resend';
4import WelcomeEmail from '@/emails/templates/welcome';
5import PasswordResetEmail from '@/emails/templates/password-reset';
6
7export async function POST(request: NextRequest) {
8 try {
9 const { type, to, data } = await request.json();
10
11 // Walidacja podstawowa
12 if (!type || !to || !data) {
13 return NextResponse.json(
14 { error: 'Missing required fields: type, to, data' },
15 { status: 400 }
16 );
17 }
18
19 let emailComponent;
20 let subject;
21
22 // Wybór szablonu na podstawie typu
23 switch (type) {
24 case 'welcome':
25 emailComponent = WelcomeEmail({
26 userFirstName: data.userFirstName,
27 loginUrl: data.loginUrl,
28 companyName: data.companyName || 'Nasza Firma',
29 });
30 subject = `Witaj w ${data.companyName || 'Naszej Firmie'}!`;
31 break;
32
33 case 'password-reset':
34 emailComponent = PasswordResetEmail({
35 userFirstName: data.userFirstName,
36 resetUrl: data.resetUrl,
37 resetCode: data.resetCode,
38 companyName: data.companyName || 'Nasza Firma',
39 expirationTime: data.expirationTime || '24 godziny',
40 });
41 subject = 'Resetowanie hasła';
42 break;
43
44 default:
45 return NextResponse.json(
46 { error: `Unknown email type: ${type}` },
47 { status: 400 }
48 );
49 }
50
51 // Wysłanie emaila
52 const result = await resend.emails.send({
53 from: process.env.FROM_EMAIL!,
54 to: Array.isArray(to) ? to : [to],
55 subject,
56 react: emailComponent,
57 replyTo: process.env.REPLY_TO_EMAIL,
58 });
59
60 return NextResponse.json({
61 success: true,
62 messageId: result.data?.id
63 });
64
65 } catch (error) {
66 console.error('Email sending error:', error);
67 return NextResponse.json(
68 { error: 'Failed to send email' },
69 { status: 500 }
70 );
71 }
72}Wysyłanie email z załącznikami
1// app/api/send-email-with-attachment/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { resend } from '@/lib/resend';
4
5export async function POST(request: NextRequest) {
6 try {
7 const formData = await request.formData();
8 const to = formData.get('to') as string;
9 const subject = formData.get('subject') as string;
10 const htmlContent = formData.get('html') as string;
11 const attachment = formData.get('attachment') as File;
12
13 if (!to || !subject || !htmlContent) {
14 return NextResponse.json(
15 { error: 'Missing required fields' },
16 { status: 400 }
17 );
18 }
19
20 const emailData: any = {
21 from: process.env.FROM_EMAIL!,
22 to: [to],
23 subject,
24 html: htmlContent,
25 };
26
27 // Dodanie załącznika jeśli istnieje
28 if (attachment) {
29 const buffer = await attachment.arrayBuffer();
30 emailData.attachments = [
31 {
32 filename: attachment.name,
33 content: Buffer.from(buffer),
34 },
35 ];
36 }
37
38 const result = await resend.emails.send(emailData);
39
40 return NextResponse.json({
41 success: true,
42 messageId: result.data?.id
43 });
44
45 } catch (error) {
46 console.error('Email sending error:', error);
47 return NextResponse.json(
48 { error: 'Failed to send email' },
49 { status: 500 }
50 );
51 }
52}Utility functions
Email service
1// lib/email-service.ts
2import { resend } from '@/lib/resend';
3import WelcomeEmail from '@/emails/templates/welcome';
4import PasswordResetEmail from '@/emails/templates/password-reset';
5
6interface EmailOptions {
7 to: string | string[];
8 subject?: string;
9 replyTo?: string;
10}
11
12interface WelcomeEmailData {
13 userFirstName: string;
14 loginUrl: string;
15 companyName?: string;
16}
17
18interface PasswordResetEmailData {
19 userFirstName: string;
20 resetUrl: string;
21 resetCode?: string;
22 companyName?: string;
23 expirationTime?: string;
24}
25
26export class EmailService {
27 private static async sendEmail(
28 component: React.ReactElement,
29 options: EmailOptions
30 ) {
31 try {
32 const result = await resend.emails.send({
33 from: process.env.FROM_EMAIL!,
34 to: Array.isArray(options.to) ? options.to : [options.to],
35 subject: options.subject!,
36 react: component,
37 replyTo: options.replyTo || process.env.REPLY_TO_EMAIL,
38 });
39
40 return { success: true, messageId: result.data?.id };
41 } catch (error) {
42 console.error('Email sending failed:', error);
43 throw new Error('Failed to send email');
44 }
45 }
46
47 static async sendWelcomeEmail(
48 data: WelcomeEmailData,
49 options: EmailOptions
50 ) {
51 const component = WelcomeEmail({
52 ...data,
53 companyName: data.companyName || 'Nasza Firma',
54 });
55
56 return this.sendEmail(component, {
57 ...options,
58 subject: options.subject || `Witaj w ${data.companyName || 'Naszej Firmie'}!`,
59 });
60 }
61
62 static async sendPasswordResetEmail(
63 data: PasswordResetEmailData,
64 options: EmailOptions
65 ) {
66 const component = PasswordResetEmail({
67 ...data,
68 companyName: data.companyName || 'Nasza Firma',
69 expirationTime: data.expirationTime || '24 godziny',
70 });
71
72 return this.sendEmail(component, {
73 ...options,
74 subject: options.subject || 'Resetowanie hasła',
75 });
76 }
77
78 static async sendBulkEmails(
79 recipients: string[],
80 component: React.ReactElement,
81 subject: string
82 ) {
83 const promises = recipients.map(email =>
84 this.sendEmail(component, { to: email, subject })
85 );
86
87 try {
88 const results = await Promise.allSettled(promises);
89 const successful = results.filter(r => r.status === 'fulfilled').length;
90 const failed = results.filter(r => r.status === 'rejected').length;
91
92 return { successful, failed, total: recipients.length };
93 } catch (error) {
94 throw new Error('Bulk email sending failed');
95 }
96 }
97}Email validation
1// lib/email-validation.ts
2import { z } from 'zod';
3
4export const emailSchema = z.object({
5 type: z.enum(['welcome', 'password-reset', 'order-confirmation']),
6 to: z.union([
7 z.string().email(),
8 z.array(z.string().email()).min(1)
9 ]),
10 data: z.object({
11 userFirstName: z.string().min(1),
12 companyName: z.string().optional(),
13 }).and(
14 z.union([
15 z.object({
16 loginUrl: z.string().url(),
17 }),
18 z.object({
19 resetUrl: z.string().url(),
20 resetCode: z.string().optional(),
21 expirationTime: z.string().optional(),
22 }),
23 ])
24 ),
25});
26
27export function validateEmailRequest(data: unknown) {
28 return emailSchema.parse(data);
29}Hooks dla integracji z komponentami
useEmailSender hook
1// hooks/useEmailSender.ts
2import { useState } from 'react';
3
4interface EmailData {
5 type: string;
6 to: string | string[];
7 data: Record<string, any>;
8}
9
10export function useEmailSender() {
11 const [loading, setLoading] = useState(false);
12 const [error, setError] = useState<string | null>(null);
13
14 const sendEmail = async (emailData: EmailData) => {
15 setLoading(true);
16 setError(null);
17
18 try {
19 const response = await fetch('/api/send-email', {
20 method: 'POST',
21 headers: {
22 'Content-Type': 'application/json',
23 },
24 body: JSON.stringify(emailData),
25 });
26
27 const result = await response.json();
28
29 if (!response.ok) {
30 throw new Error(result.error || 'Failed to send email');
31 }
32
33 return result;
34 } catch (err) {
35 const errorMessage = err instanceof Error ? err.message : 'Unknown error';
36 setError(errorMessage);
37 throw err;
38 } finally {
39 setLoading(false);
40 }
41 };
42
43 return { sendEmail, loading, error };
44}Przykład użycia w komponencie
1// components/WelcomeEmailButton.tsx
2'use client';
3
4import { useEmailSender } from '@/hooks/useEmailSender';
5
6interface WelcomeEmailButtonProps {
7 userEmail: string;
8 userFirstName: string;
9}
10
11export function WelcomeEmailButton({ userEmail, userFirstName }: WelcomeEmailButtonProps) {
12 const { sendEmail, loading, error } = useEmailSender();
13
14 const handleSendWelcomeEmail = async () => {
15 try {
16 await sendEmail({
17 type: 'welcome',
18 to: userEmail,
19 data: {
20 userFirstName,
21 loginUrl: `${window.location.origin}/login`,
22 companyName: 'Metropolia Quantum',
23 },
24 });
25
26 alert('Email powitalny został wysłany!');
27 } catch (error) {
28 alert('Błąd podczas wysyłania emaila');
29 }
30 };
31
32 return (
33 <div>
34 <button
35 onClick={handleSendWelcomeEmail}
36 disabled={loading}
37 className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
38 >
39 {loading ? 'Wysyłanie...' : 'Wyślij email powitalny'}
40 </button>
41
42 {error && (
43 <p className="text-red-600 text-sm mt-2">{error}</p>
44 )}
45 </div>
46 );
47}Webhooks i analityka
Webhook handler
1// app/api/webhooks/resend/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function POST(request: NextRequest) {
5 try {
6 const payload = await request.json();
7 const { type, data } = payload;
8
9 // Logowanie eventów
10 console.log(`Resend webhook: ${type}`, data);
11
12 switch (type) {
13 case 'email.sent':
14 // Email został wysłany
15 await handleEmailSent(data);
16 break;
17
18 case 'email.delivered':
19 // Email został dostarczony
20 await handleEmailDelivered(data);
21 break;
22
23 case 'email.bounced':
24 // Email został odrzucony
25 await handleEmailBounced(data);
26 break;
27
28 case 'email.complained':
29 // Email został zgłoszony jako spam
30 await handleEmailComplained(data);
31 break;
32
33 default:
34 console.log(`Unhandled webhook type: ${type}`);
35 }
36
37 return NextResponse.json({ received: true });
38 } catch (error) {
39 console.error('Webhook error:', error);
40 return NextResponse.json(
41 { error: 'Webhook processing failed' },
42 { status: 500 }
43 );
44 }
45}
46
47async function handleEmailSent(data: any) {
48 // Implementacja logiki po wysłaniu emaila
49 console.log('Email sent:', data.email_id);
50}
51
52async function handleEmailDelivered(data: any) {
53 // Implementacja logiki po dostarczeniu emaila
54 console.log('Email delivered:', data.email_id);
55}
56
57async function handleEmailBounced(data: any) {
58 // Implementacja logiki po odrzuceniu emaila
59 console.log('Email bounced:', data.email_id, data.reason);
60
61 // Możesz zaktualizować status użytkownika w bazie danych
62 // await updateUserEmailStatus(data.to, 'bounced');
63}
64
65async function handleEmailComplained(data: any) {
66 // Implementacja logiki po zgłoszeniu spamu
67 console.log('Email complained:', data.email_id);
68
69 // Automatyczne wypisanie z newslettera
70 // await unsubscribeUser(data.to);
71}Development i testing
Preview emaili
Uruchom serwer preview:
1npm run email:devPrzejdź do http://localhost:3000 aby zobaczyć podgląd wszystkich emaili.
Export do HTML
1npm run email:exportTestowanie w różnych klientach
1// scripts/test-email-clients.ts
2import { render } from '@react-email/render';
3import WelcomeEmail from '../emails/templates/welcome';
4
5async function testEmailRendering() {
6 const html = render(WelcomeEmail({
7 userFirstName: 'Jan',
8 loginUrl: 'https://example.com/login',
9 companyName: 'Test Company',
10 }));
11
12 // Zapisz HTML do pliku dla testów
13 const fs = require('fs');
14 fs.writeFileSync('./test-output/welcome-email.html', html);
15
16 console.log('Email HTML generated for testing');
17}
18
19testEmailRendering();Najlepsze praktyki
1. Personalizacja
1// Zawsze używaj danych użytkownika
2const personalizedSubject = `Cześć ${firstName}, sprawdź swoje nowe funkcje!`;2. Responsive design
1// Używaj responsywnych stylów
2const responsiveStyle = {
3 width: '100%',
4 maxWidth: '600px',
5 '@media (max-width: 600px)': {
6 maxWidth: '100%',
7 padding: '20px 10px',
8 },
9};3. A/B testing
1// Testuj różne wersje emaili
2const subjectVariants = [
3 'Witaj w naszej aplikacji!',
4 'Twoje konto zostało utworzone',
5 'Zaczynajmy przygodę razem!',
6];
7
8const randomSubject = subjectVariants[Math.floor(Math.random() * subjectVariants.length)];4. Rate limiting
1// Ogranicz liczbę emaili na użytkownika
2const DAILY_EMAIL_LIMIT = 10;
3
4async function checkEmailLimit(userId: string) {
5 // Sprawdź ile emaili wysłano dzisiaj do tego użytkownika
6 // Implementacja zależna od bazy danych
7}Podsumowanie
Połączenie Next.js z Resend i React Email tworzy potężny stack do wysyłania emaili:
- React Email - komponentowe tworzenie emaili
- Resend - niezawodne dostarczanie
- TypeScript - bezpieczeństwo typów
- Webhooks - real-time analytics
- Preview - łatwy development
Ta kombinacja pozwala budować profesjonalne systemy email marketingu, powiadomień transakcyjnych i komunikacji z użytkownikami.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3interface EmailTemplate {
4 id: string;
5 name: string;
6 subject: string;
7 preview: React.ReactNode;
8}
9
10const WelcomeEmailPreview = () => (
11 <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '500px', margin: '0 auto', background: '#fff', color: '#333', borderRadius: '8px', overflow: 'hidden' }}>
12 <div style={{ background: 'linear-gradient(135deg, #7c4dff, #64ffda)', padding: '30px', textAlign: 'center' }}>
13 <h1 style={{ color: '#fff', margin: 0, fontSize: '24px' }}>Witaj w Metropolii Quantum!</h1>
14 </div>
15 <div style={{ padding: '24px' }}>
16 <p>Cześć <strong>{'{{name}}'}</strong>,</p>
17 <p>Twoje konto zostało aktywowane. Oto jak zacząć:</p>
18 <ol style={{ lineHeight: '2' }}>
19 <li>Dokończ swój profil</li>
20 <li>Zapoznaj się z dokumentacją</li>
21 <li>Stwórz pierwszy projekt</li>
22 </ol>
23 <div style={{ textAlign: 'center', margin: '20px 0' }}>
24 <span style={{ background: '#7c4dff', color: '#fff', padding: '12px 24px', borderRadius: '6px', display: 'inline-block' }}>Rozpocznij</span>
25 </div>
26 </div>
27 <div style={{ background: '#f5f5f5', padding: '16px', textAlign: 'center', fontSize: '12px', color: '#888' }}>
28 Metropolia Quantum © 2025
29 </div>
30 </div>
31);
32
33const ResetPasswordPreview = () => (
34 <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '500px', margin: '0 auto', background: '#fff', color: '#333', borderRadius: '8px', overflow: 'hidden' }}>
35 <div style={{ background: '#e94560', padding: '24px', textAlign: 'center' }}>
36 <h1 style={{ color: '#fff', margin: 0, fontSize: '20px' }}>Reset hasła</h1>
37 </div>
38 <div style={{ padding: '24px' }}>
39 <p>Otrzymaliśmy prośbę o zresetowanie hasła dla konta <strong>{'{{email}}'}</strong>.</p>
40 <p>Kliknij poniższy przycisk, aby ustawić nowe hasło. Link wygasa za 24h.</p>
41 <div style={{ textAlign: 'center', margin: '24px 0' }}>
42 <span style={{ background: '#e94560', color: '#fff', padding: '12px 24px', borderRadius: '6px', display: 'inline-block' }}>Zresetuj hasło</span>
43 </div>
44 <p style={{ fontSize: '13px', color: '#888' }}>Jeśli nie prosiłeś o reset, zignoruj ten email.</p>
45 </div>
46 </div>
47);
48
49const InvoicePreview = () => (
50 <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '500px', margin: '0 auto', background: '#fff', color: '#333', borderRadius: '8px', overflow: 'hidden' }}>
51 <div style={{ background: '#0f3460', padding: '24px', textAlign: 'center' }}>
52 <h1 style={{ color: '#fff', margin: 0, fontSize: '20px' }}>Potwierdzenie zamówienia</h1>
53 </div>
54 <div style={{ padding: '24px' }}>
55 <p>Dziękujemy za zakup! Zamówienie <strong>#QM-2025-0042</strong></p>
56 <table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '16px' }}>
57 <thead><tr style={{ borderBottom: '2px solid #eee' }}>
58 <th style={{ textAlign: 'left', padding: '8px' }}>Produkt</th>
59 <th style={{ textAlign: 'right', padding: '8px' }}>Cena</th>
60 </tr></thead>
61 <tbody>
62 <tr style={{ borderBottom: '1px solid #eee' }}><td style={{ padding: '8px' }}>Quantum Shield Pro</td><td style={{ textAlign: 'right', padding: '8px' }}>49.99 PLN</td></tr>
63 <tr style={{ borderBottom: '1px solid #eee' }}><td style={{ padding: '8px' }}>Cyber Toolkit</td><td style={{ textAlign: 'right', padding: '8px' }}>24.99 PLN</td></tr>
64 <tr><td style={{ padding: '8px', fontWeight: 'bold' }}>Suma</td><td style={{ textAlign: 'right', padding: '8px', fontWeight: 'bold' }}>74.98 PLN</td></tr>
65 </tbody>
66 </table>
67 </div>
68 </div>
69);
70
71const ResendDemo = () => {
72 const [selectedTemplate, setSelectedTemplate] = useState<string>('welcome');
73 const [sending, setSending] = useState(false);
74 const [logs, setLogs] = useState<string[]>([]);
75
76 const templates: EmailTemplate[] = [
77 { id: 'welcome', name: 'Welcome Email', subject: 'Witaj w Metropolii Quantum!', preview: <WelcomeEmailPreview /> },
78 { id: 'reset', name: 'Password Reset', subject: 'Reset hasła', preview: <ResetPasswordPreview /> },
79 { id: 'invoice', name: 'Order Confirmation', subject: 'Potwierdzenie zamówienia #QM-2025-0042', preview: <InvoicePreview /> },
80 ];
81
82 const currentTemplate = templates.find(t => t.id === selectedTemplate)!;
83
84 const handleSend = async () => {
85 setSending(true);
86 setLogs([]);
87 const addLog = (msg: string) => setLogs(prev => [...prev, msg]);
88 addLog('POST /api/send - Sending email via Resend...');
89 await new Promise(r => setTimeout(r, 400));
90 addLog('resend.emails.send({ from: "noreply@quantum.city", to: "user@example.com" })');
91 await new Promise(r => setTimeout(r, 500));
92 addLog('React Email template rendered to HTML');
93 await new Promise(r => setTimeout(r, 300));
94 addLog('Response: { id: "re_abc123", statusCode: 200 }');
95 addLog('Email delivered successfully!');
96 setSending(false);
97 };
98
99 return (
100 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
101 <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Next.js + Resend + React Email</h1>
102 <div style={{ maxWidth: '800px', margin: '20px auto' }}>
103 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px', justifyContent: 'center' }}>
104 {templates.map(t => (
105 <button key={t.id} onClick={() => { setSelectedTemplate(t.id); setLogs([]); }}
106 style={{ padding: '8px 16px', background: selectedTemplate === t.id ? '#7c4dff' : 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
107 {t.name}
108 </button>
109 ))}
110 </div>
111
112 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
113 <div>
114 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Email Preview</h3>
115 <div style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #333' }}>
116 {currentTemplate.preview}
117 </div>
118 </div>
119 <div>
120 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>API Console</h3>
121 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '16px', borderRadius: '10px', border: '1px solid #333' }}>
122 <p style={{ color: '#888', fontSize: '13px', marginBottom: '8px' }}>Subject: {currentTemplate.subject}</p>
123 <button onClick={handleSend} disabled={sending}
124 style={{ width: '100%', padding: '10px', background: sending ? '#555' : '#64ffda', border: 'none', borderRadius: '6px', color: '#0f0f23', fontWeight: 'bold', cursor: sending ? 'default' : 'pointer' }}>
125 {sending ? 'Wysylanie...' : 'Wyslij email'}
126 </button>
127 </div>
128 {logs.length > 0 && (
129 <div style={{ background: '#000', padding: '12px', borderRadius: '8px', marginTop: '10px', fontFamily: 'monospace', fontSize: '11px' }}>
130 {logs.map((log, i) => (<div key={i} style={{ color: '#0f0', padding: '2px 0' }}>{log}</div>))}
131 </div>
132 )}
133 </div>
134 </div>
135 </div>
136 </div>
137 );
138};
139
140export default ResendDemo;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. React Email pozwala na:
2. Który zestaw komponentów NIE jest częścią biblioteki React Email?
Zadania praktyczne w grze
- Klikanie w kolejności
Uporządkuj kroki integracji Resend z Next.js.
- Edytor kodu
Stwórz komponent emailowy wykorzystując React Email z szablonem potwierdzenia zamówienia
- Edytor kodu
Zbuduj welcome email z React Email components.