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.
Resend to nowoczesna platforma do wysyłania emaili, stworzona przez zespół deweloperów dla deweloperów:
React Email to biblioteka do tworzenia emaili przy użyciu React:
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/cliUtwó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.comDodaj 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};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);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.ts1// 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};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};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};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}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}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}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}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}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}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}Uruchom serwer preview:
1npm run email:devPrzejdź do
http://localhost:3000 aby zobaczyć podgląd wszystkich emaili.1npm run email:export1// 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();1// Zawsze używaj danych użytkownika
2const personalizedSubject = `Cześć ${firstName}, sprawdź swoje nowe funkcje!`;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};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)];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}Połączenie Next.js z Resend i React Email tworzy potężny stack do wysyłania emaili:
Ta kombinacja pozwala budować profesjonalne systemy email marketingu, powiadomień transakcyjnych i komunikacji z użytkownikami.