بناء نظام بريد إلكتروني للمعاملات باستخدام Resend و React Email في Next.js

يحتاج كل تطبيق ويب جدي إلى إرسال رسائل بريد إلكتروني — رسائل ترحيب، إعادة تعيين كلمات المرور، تأكيدات الطلبات، إيصالات الفواتير، ودعوات الفريق. تقليدياً، كان بناء قوالب البريد الإلكتروني يعني التصارع مع CSS المضمّن، والجداول المتداخلة، والاختبار عبر عشرات عملاء البريد الإلكتروني. تجربة مطور بائسة حقاً.
React Email يغير هذا الواقع بالسماح لك ببناء قوالب البريد الإلكتروني باستخدام مكونات React — نفس النموذج الذهني الذي تستخدمه لواجهة المستخدم. Resend يوفر بنية تحتية للتسليم مع واجهة برمجة تطبيقات حديثة وقابلية تسليم ممتازة ومستوى مجاني سخي. مع Next.js، تحصل على نظام بريد إلكتروني متكامل حيث تعيش القوالب بجانب شفرة تطبيقك، وتكون آمنة النوع، ويمكن معاينتها في المتصفح قبل الإرسال.
في هذا الدرس، ستبني نظام بريد إلكتروني كامل للمعاملات لتطبيق SaaS. ستنشئ قوالب لرسائل الترحيب وإعادة تعيين كلمة المرور وإيصالات الفواتير، وتعاينها في المتصفح مع التحديث الفوري، وترسلها عبر مسارات API، وتنشر كل شيء في بيئة الإنتاج.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- حساب Resend — سجّل على موقع Resend (المستوى المجاني يتضمن 3,000 بريد إلكتروني/شهر)
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router
- نطاق موثّق (أو استخدم نطاق الاختبار من Resend للتطوير)
ما ستبنيه
بنهاية هذا الدرس، سيكون لديك:
- مشروع Next.js مع React Email مدمج
- ثلاثة قوالب بريد إلكتروني جاهزة للإنتاج (ترحيب، إعادة تعيين كلمة المرور، فاتورة)
- خادم معاينة محلي لتطوير واختبار البريد الإلكتروني بصرياً
- مسارات API لإرسال البريد الإلكتروني عبر Resend
- أدوات إرسال آمنة النوع مع معالجة الأخطاء
- نشر جاهز للإنتاج
الخطوة 1: إنشاء مشروع Next.js
ابدأ بإنشاء تطبيق Next.js جديد:
npx create-next-app@latest saas-emails --typescript --tailwind --eslint --app --src-dir
cd saas-emailsاختر الخيارات الافتراضية عند السؤال. هذا يعطيك مشروع Next.js 15 مع TypeScript و App Router.
الخطوة 2: تثبيت التبعيات
ثبّت React Email لتطوير القوالب و Resend للتسليم:
npm install resend @react-email/components react-emailحزمة @react-email/components توفر جميع الوحدات البنائية — Html، Head، Body، Container، Text، Button، Img، Link، Section، Row، Column، Hr، Preview، والمزيد.
أضف سكربت إلى ملف package.json لخادم المعاينة:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"email": "email dev --dir src/emails --port 3001"
}
}الخطوة 3: إعداد متغيرات البيئة
أنشئ ملف .env.local في جذر مشروعك:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxاحصل على مفتاح API من لوحة تحكم Resend تحت API Keys. للتطوير، يمكنك استخدام مفتاح API الاختباري الذي يرسل البريد الإلكتروني فقط إلى عنوان بريدك الإلكتروني الموثّق.
الخطوة 4: إنشاء هيكل مجلد البريد الإلكتروني
نظّم قوالب البريد الإلكتروني في مجلد مخصص:
mkdir -p src/emails/componentsسيبدو هيكل مشروعك هكذا:
src/
emails/
components/
email-header.tsx
email-footer.tsx
email-button.tsx
welcome.tsx
password-reset.tsx
invoice.tsx
app/
api/
email/
send/
route.ts
lib/
resend.ts
الخطوة 5: بناء مكونات البريد الإلكتروني المشتركة
قبل إنشاء القوالب الكاملة، ابنِ مكونات قابلة لإعادة الاستخدام تحافظ على هوية بصرية متسقة عبر جميع الرسائل.
مكون الرأس
أنشئ src/emails/components/email-header.tsx:
import { Img, Section, Text } from "@react-email/components";
interface EmailHeaderProps {
title?: string;
}
export function EmailHeader({ title }: EmailHeaderProps) {
return (
<Section style={headerStyle}>
<Img
src="https://yourdomain.com/logo.png"
width="140"
height="40"
alt="YourApp"
style={logoStyle}
/>
{title && <Text style={titleStyle}>{title}</Text>}
</Section>
);
}
const headerStyle = {
textAlign: "center" as const,
padding: "32px 0 24px",
};
const logoStyle = {
margin: "0 auto",
};
const titleStyle = {
fontSize: "24px",
fontWeight: "bold" as const,
color: "#111827",
margin: "16px 0 0",
};مكون التذييل
أنشئ src/emails/components/email-footer.tsx:
import { Hr, Link, Section, Text } from "@react-email/components";
export function EmailFooter() {
return (
<Section style={footerStyle}>
<Hr style={dividerStyle} />
<Text style={footerTextStyle}>
© 2026 YourApp. جميع الحقوق محفوظة.
</Text>
<Text style={footerLinksStyle}>
<Link href="https://yourdomain.com/privacy" style={linkStyle}>
سياسة الخصوصية
</Link>
{" • "}
<Link href="https://yourdomain.com/terms" style={linkStyle}>
شروط الخدمة
</Link>
{" • "}
<Link href="https://yourdomain.com/unsubscribe" style={linkStyle}>
إلغاء الاشتراك
</Link>
</Text>
</Section>
);
}
const footerStyle = {
padding: "0 0 32px",
};
const dividerStyle = {
borderColor: "#e5e7eb",
margin: "32px 0 24px",
};
const footerTextStyle = {
fontSize: "12px",
color: "#9ca3af",
textAlign: "center" as const,
};
const footerLinksStyle = {
fontSize: "12px",
color: "#9ca3af",
textAlign: "center" as const,
};
const linkStyle = {
color: "#6b7280",
textDecoration: "underline",
};مكون الزر القابل لإعادة الاستخدام
أنشئ src/emails/components/email-button.tsx:
import { Button } from "@react-email/components";
interface EmailButtonProps {
href: string;
children: React.ReactNode;
variant?: "primary" | "secondary";
}
export function EmailButton({
href,
children,
variant = "primary",
}: EmailButtonProps) {
const style =
variant === "primary" ? primaryButtonStyle : secondaryButtonStyle;
return (
<Button href={href} style={style}>
{children}
</Button>
);
}
const primaryButtonStyle = {
backgroundColor: "#2563eb",
borderRadius: "8px",
color: "#ffffff",
fontSize: "14px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "inline-block",
padding: "12px 24px",
};
const secondaryButtonStyle = {
backgroundColor: "#ffffff",
borderRadius: "8px",
border: "1px solid #d1d5db",
color: "#374151",
fontSize: "14px",
fontWeight: "600" as const,
textDecoration: "none",
textAlign: "center" as const,
display: "inline-block",
padding: "12px 24px",
};الخطوة 6: إنشاء قالب بريد الترحيب
أنشئ src/emails/welcome.tsx:
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
interface WelcomeEmailProps {
username: string;
loginUrl?: string;
}
export default function WelcomeEmail({
username = "there",
loginUrl = "https://yourdomain.com/login",
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>مرحباً بك في YourApp — لنبدأ!</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="مرحباً بك!" />
<Section style={contentStyle}>
<Text style={greetingStyle}>مرحباً {username}،</Text>
<Text style={paragraphStyle}>
شكراً لتسجيلك في YourApp! نحن متحمسون لانضمامك إلينا. حسابك
جاهز ويمكنك البدء بالاستكشاف فوراً.
</Text>
<Text style={paragraphStyle}>
إليك ما يمكنك فعله بعد ذلك:
</Text>
<Section style={listStyle}>
<Text style={listItemStyle}>
✅ إكمال إعدادات ملفك الشخصي
</Text>
<Text style={listItemStyle}>
✅ إنشاء مشروعك الأول
</Text>
<Text style={listItemStyle}>
✅ دعوة أعضاء فريقك
</Text>
<Text style={listItemStyle}>
✅ استكشاف التوثيق
</Text>
</Section>
<Section style={buttonContainerStyle}>
<EmailButton href={loginUrl}>
ابدأ الآن
</EmailButton>
</Section>
<Text style={paragraphStyle}>
إذا كان لديك أي أسئلة، رد على هذا البريد الإلكتروني — نقرأ
ونرد على كل رسالة.
</Text>
<Text style={signoffStyle}>
تطوير سعيد،
<br />
فريق YourApp
</Text>
</Section>
<EmailFooter />
</Container>
</Body>
</Html>
);
}
const bodyStyle = {
backgroundColor: "#f9fafb",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const containerStyle = {
backgroundColor: "#ffffff",
margin: "0 auto",
maxWidth: "600px",
borderRadius: "8px",
overflow: "hidden" as const,
};
const contentStyle = { padding: "0 32px" };
const greetingStyle = {
fontSize: "18px",
fontWeight: "600" as const,
color: "#111827",
margin: "0 0 16px",
};
const paragraphStyle = {
fontSize: "15px",
lineHeight: "24px",
color: "#374151",
margin: "0 0 16px",
};
const listStyle = {
margin: "0 0 24px",
padding: "16px 24px",
backgroundColor: "#f9fafb",
borderRadius: "8px",
};
const listItemStyle = {
fontSize: "14px",
lineHeight: "28px",
color: "#374151",
margin: "0",
};
const buttonContainerStyle = {
textAlign: "center" as const,
margin: "24px 0",
};
const signoffStyle = {
fontSize: "15px",
lineHeight: "24px",
color: "#374151",
margin: "24px 0 0",
};الخطوة 7: إنشاء قالب إعادة تعيين كلمة المرور
أنشئ src/emails/password-reset.tsx:
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
Code,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
interface PasswordResetEmailProps {
username: string;
resetUrl?: string;
expiresInMinutes?: number;
}
export default function PasswordResetEmail({
username = "there",
resetUrl = "https://yourdomain.com/reset?token=abc123",
expiresInMinutes = 60,
}: PasswordResetEmailProps) {
return (
<Html>
<Head />
<Preview>إعادة تعيين كلمة مرور YourApp</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="إعادة تعيين كلمة المرور" />
<Section style={contentStyle}>
<Text style={paragraphStyle}>مرحباً {username}،</Text>
<Text style={paragraphStyle}>
تلقينا طلباً لإعادة تعيين كلمة مرورك. انقر على الزر أدناه
لاختيار كلمة مرور جديدة:
</Text>
<Section style={buttonContainerStyle}>
<EmailButton href={resetUrl}>
إعادة تعيين كلمة المرور
</EmailButton>
</Section>
<Section style={warningBoxStyle}>
<Text style={warningTextStyle}>
⏰ تنتهي صلاحية هذا الرابط خلال {expiresInMinutes} دقيقة.
إذا لم تطلب إعادة تعيين كلمة المرور، يمكنك تجاهل هذا البريد
بأمان.
</Text>
</Section>
<Text style={paragraphStyle}>
إذا لم يعمل الزر، انسخ والصق هذا الرابط في متصفحك:
</Text>
<Section style={urlBoxStyle}>
<Code style={urlTextStyle}>{resetUrl}</Code>
</Section>
<Text style={securityNoteStyle}>
لأسباب أمنية، تم تلقي هذا الطلب من متصفح ويب. إذا لم تقم بهذا
الطلب، يرجى تغيير كلمة مرورك فوراً.
</Text>
</Section>
<EmailFooter />
</Container>
</Body>
</Html>
);
}
const bodyStyle = {
backgroundColor: "#f9fafb",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const containerStyle = {
backgroundColor: "#ffffff",
margin: "0 auto",
maxWidth: "600px",
borderRadius: "8px",
overflow: "hidden" as const,
};
const contentStyle = { padding: "0 32px" };
const paragraphStyle = {
fontSize: "15px",
lineHeight: "24px",
color: "#374151",
margin: "0 0 16px",
};
const buttonContainerStyle = {
textAlign: "center" as const,
margin: "24px 0",
};
const warningBoxStyle = {
backgroundColor: "#fef3c7",
borderRadius: "8px",
padding: "16px",
margin: "0 0 24px",
};
const warningTextStyle = {
fontSize: "13px",
lineHeight: "20px",
color: "#92400e",
margin: "0",
};
const urlBoxStyle = {
backgroundColor: "#f3f4f6",
borderRadius: "8px",
padding: "12px 16px",
margin: "0 0 24px",
};
const urlTextStyle = {
fontSize: "12px",
color: "#6b7280",
wordBreak: "break-all" as const,
};
const securityNoteStyle = {
fontSize: "13px",
lineHeight: "20px",
color: "#9ca3af",
margin: "0 0 16px",
fontStyle: "italic",
};الخطوة 8: إنشاء قالب بريد الفاتورة
أنشئ src/emails/invoice.tsx:
import {
Body,
Column,
Container,
Head,
Hr,
Html,
Preview,
Row,
Section,
Text,
} from "@react-email/components";
import { EmailHeader } from "./components/email-header";
import { EmailFooter } from "./components/email-footer";
import { EmailButton } from "./components/email-button";
interface InvoiceItem {
description: string;
quantity: number;
unitPrice: number;
}
interface InvoiceEmailProps {
customerName: string;
invoiceNumber: string;
invoiceDate: string;
dueDate: string;
items: InvoiceItem[];
currency?: string;
invoiceUrl?: string;
}
function formatCurrency(amount: number, currency: string) {
return new Intl.NumberFormat("ar-SA", {
style: "currency",
currency,
}).format(amount);
}
export default function InvoiceEmail({
customerName = "أحمد محمد",
invoiceNumber = "INV-2026-001",
invoiceDate = "15 مارس 2026",
dueDate = "15 أبريل 2026",
items = [
{ description: "الخطة الاحترافية - شهري", quantity: 1, unitPrice: 29 },
{ description: "مقاعد إضافية (3)", quantity: 3, unitPrice: 10 },
],
currency = "USD",
invoiceUrl = "https://yourdomain.com/invoices/INV-2026-001",
}: InvoiceEmailProps) {
const subtotal = items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice,
0
);
const tax = subtotal * 0.1;
const total = subtotal + tax;
return (
<Html>
<Head />
<Preview>
فاتورة {invoiceNumber} — {formatCurrency(total, currency)} مستحقة{" "}
{dueDate}
</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<EmailHeader title="فاتورة" />
<Section style={contentStyle}>
<Text style={paragraphStyle}>مرحباً {customerName}،</Text>
<Text style={paragraphStyle}>
إليك فاتورتك. يرجى الاطلاع على التفاصيل أدناه:
</Text>
{/* بيانات الفاتورة */}
<Section style={metaBoxStyle}>
<Row>
<Column>
<Text style={metaLabelStyle}>رقم الفاتورة</Text>
<Text style={metaValueStyle}>{invoiceNumber}</Text>
</Column>
<Column>
<Text style={metaLabelStyle}>التاريخ</Text>
<Text style={metaValueStyle}>{invoiceDate}</Text>
</Column>
<Column>
<Text style={metaLabelStyle}>تاريخ الاستحقاق</Text>
<Text style={metaValueStyle}>{dueDate}</Text>
</Column>
</Row>
</Section>
{/* رأس الجدول */}
<Section style={tableHeaderStyle}>
<Row>
<Column style={descColStyle}>
<Text style={headerCellStyle}>الوصف</Text>
</Column>
<Column style={qtyColStyle}>
<Text style={headerCellStyle}>الكمية</Text>
</Column>
<Column style={priceColStyle}>
<Text style={headerCellStyle}>السعر</Text>
</Column>
<Column style={totalColStyle}>
<Text style={headerCellStyle}>المجموع</Text>
</Column>
</Row>
</Section>
{/* بنود الفاتورة */}
{items.map((item, i) => (
<Section key={i} style={tableRowStyle}>
<Row>
<Column style={descColStyle}>
<Text style={cellStyle}>{item.description}</Text>
</Column>
<Column style={qtyColStyle}>
<Text style={cellStyle}>{item.quantity}</Text>
</Column>
<Column style={priceColStyle}>
<Text style={cellStyle}>
{formatCurrency(item.unitPrice, currency)}
</Text>
</Column>
<Column style={totalColStyle}>
<Text style={cellStyle}>
{formatCurrency(
item.quantity * item.unitPrice,
currency
)}
</Text>
</Column>
</Row>
</Section>
))}
{/* المجاميع */}
<Hr style={dividerStyle} />
<Section style={totalsStyle}>
<Row>
<Column style={totalsLabelColStyle}>
<Text style={totalsLabelStyle}>المجموع الفرعي</Text>
</Column>
<Column style={totalsValueColStyle}>
<Text style={totalsValueStyle}>
{formatCurrency(subtotal, currency)}
</Text>
</Column>
</Row>
<Row>
<Column style={totalsLabelColStyle}>
<Text style={totalsLabelStyle}>الضريبة (10%)</Text>
</Column>
<Column style={totalsValueColStyle}>
<Text style={totalsValueStyle}>
{formatCurrency(tax, currency)}
</Text>
</Column>
</Row>
<Hr style={dividerStyle} />
<Row>
<Column style={totalsLabelColStyle}>
<Text style={grandTotalLabelStyle}>الإجمالي المستحق</Text>
</Column>
<Column style={totalsValueColStyle}>
<Text style={grandTotalValueStyle}>
{formatCurrency(total, currency)}
</Text>
</Column>
</Row>
</Section>
<Section style={buttonContainerStyle}>
<EmailButton href={invoiceUrl}>
عرض الفاتورة عبر الإنترنت
</EmailButton>
</Section>
<Text style={noteStyle}>
الدفع مستحق بحلول {dueDate}. إذا كنت قد أرسلت الدفع بالفعل،
يرجى تجاهل هذا البريد الإلكتروني.
</Text>
</Section>
<EmailFooter />
</Container>
</Body>
</Html>
);
}
const bodyStyle = {
backgroundColor: "#f9fafb",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const containerStyle = {
backgroundColor: "#ffffff",
margin: "0 auto",
maxWidth: "600px",
borderRadius: "8px",
overflow: "hidden" as const,
};
const contentStyle = { padding: "0 32px" };
const paragraphStyle = {
fontSize: "15px",
lineHeight: "24px",
color: "#374151",
margin: "0 0 16px",
};
const metaBoxStyle = {
backgroundColor: "#f9fafb",
borderRadius: "8px",
padding: "16px",
margin: "0 0 24px",
};
const metaLabelStyle = {
fontSize: "11px",
fontWeight: "600" as const,
color: "#9ca3af",
textTransform: "uppercase" as const,
margin: "0 0 4px",
};
const metaValueStyle = {
fontSize: "14px",
fontWeight: "600" as const,
color: "#111827",
margin: "0",
};
const tableHeaderStyle = {
backgroundColor: "#f3f4f6",
borderRadius: "4px",
padding: "8px 12px",
};
const tableRowStyle = { padding: "8px 12px" };
const descColStyle = { width: "45%" };
const qtyColStyle = { width: "15%", textAlign: "center" as const };
const priceColStyle = { width: "20%", textAlign: "right" as const };
const totalColStyle = { width: "20%", textAlign: "right" as const };
const headerCellStyle = {
fontSize: "11px",
fontWeight: "600" as const,
color: "#6b7280",
textTransform: "uppercase" as const,
margin: "0",
};
const cellStyle = {
fontSize: "14px",
color: "#374151",
margin: "0",
};
const dividerStyle = { borderColor: "#e5e7eb", margin: "8px 0" };
const totalsStyle = { padding: "0 12px" };
const totalsLabelColStyle = { width: "70%" };
const totalsValueColStyle = { width: "30%", textAlign: "right" as const };
const totalsLabelStyle = {
fontSize: "14px",
color: "#6b7280",
margin: "4px 0",
};
const totalsValueStyle = {
fontSize: "14px",
color: "#374151",
margin: "4px 0",
};
const grandTotalLabelStyle = {
fontSize: "16px",
fontWeight: "bold" as const,
color: "#111827",
margin: "4px 0",
};
const grandTotalValueStyle = {
fontSize: "16px",
fontWeight: "bold" as const,
color: "#2563eb",
margin: "4px 0",
};
const buttonContainerStyle = {
textAlign: "center" as const,
margin: "24px 0",
};
const noteStyle = {
fontSize: "13px",
color: "#9ca3af",
textAlign: "center" as const,
margin: "0 0 16px",
};الخطوة 9: معاينة رسائل البريد الإلكتروني محلياً
شغّل خادم معاينة React Email:
npm run emailافتح http://localhost:3001 في متصفحك. سترى جميع قوالب البريد الإلكتروني الثلاثة مدرجة. انقر على أي قالب لرؤية معاينة مباشرة. ستتحدث التغييرات في ملفات القوالب فورياً.
يتيح لك خادم المعاينة:
- عرض البريد الإلكتروني المُصيّر تماماً كما سيظهر في عملاء البريد الإلكتروني
- التبديل بين العرض للحاسوب والجوال
- نسخ شفرة HTML المصدرية للاختبار في أدوات أخرى
- إرسال بريد إلكتروني تجريبي مباشرة من واجهة المعاينة
- التبديل بين الخصائص لاختبار حالات مختلفة
هذه واحدة من أكبر مزايا React Email — تطوّر البريد الإلكتروني بنفس سير العمل المستخدم لمكونات واجهة المستخدم.
الخطوة 10: إعداد عميل Resend
أنشئ src/lib/resend.ts:
import { Resend } from "resend";
if (!process.env.RESEND_API_KEY) {
throw new Error("متغير البيئة RESEND_API_KEY غير معيّن");
}
export const resend = new Resend(process.env.RESEND_API_KEY);
// المرسل الافتراضي — استخدم نطاقك الموثّق في الإنتاج
export const FROM_EMAIL = "YourApp <noreply@yourdomain.com>";الخطوة 11: إنشاء مسار API لإرسال البريد الإلكتروني
أنشئ src/app/api/email/send/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { resend, FROM_EMAIL } from "@/lib/resend";
import WelcomeEmail from "@/emails/welcome";
import PasswordResetEmail from "@/emails/password-reset";
import InvoiceEmail from "@/emails/invoice";
type EmailTemplate = "welcome" | "password-reset" | "invoice";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { template, to, data } = body as {
template: EmailTemplate;
to: string;
data: Record<string, unknown>;
};
if (!template || !to) {
return NextResponse.json(
{ error: "حقول مطلوبة مفقودة: template, to" },
{ status: 400 }
);
}
const emailConfig = getEmailConfig(template, data);
if (!emailConfig) {
return NextResponse.json(
{ error: `قالب غير معروف: ${template}` },
{ status: 400 }
);
}
const { data: result, error } = await resend.emails.send({
from: FROM_EMAIL,
to: [to],
subject: emailConfig.subject,
react: emailConfig.component,
});
if (error) {
console.error("خطأ Resend:", error);
return NextResponse.json(
{ error: "فشل إرسال البريد الإلكتروني" },
{ status: 500 }
);
}
return NextResponse.json({ success: true, id: result?.id });
} catch (err) {
console.error("خطأ في إرسال البريد:", err);
return NextResponse.json(
{ error: "خطأ داخلي في الخادم" },
{ status: 500 }
);
}
}
function getEmailConfig(
template: EmailTemplate,
data: Record<string, unknown>
) {
switch (template) {
case "welcome":
return {
subject: "مرحباً بك في YourApp!",
component: WelcomeEmail({
username: (data.username as string) || "there",
loginUrl: data.loginUrl as string,
}),
};
case "password-reset":
return {
subject: "إعادة تعيين كلمة المرور",
component: PasswordResetEmail({
username: (data.username as string) || "there",
resetUrl: data.resetUrl as string,
expiresInMinutes: (data.expiresInMinutes as number) || 60,
}),
};
case "invoice":
return {
subject: `فاتورة ${data.invoiceNumber || ""}`,
component: InvoiceEmail({
customerName: (data.customerName as string) || "العميل",
invoiceNumber: (data.invoiceNumber as string) || "INV-000",
invoiceDate: (data.invoiceDate as string) || new Date().toLocaleDateString(),
dueDate: (data.dueDate as string) || "",
items: (data.items as Array<{
description: string;
quantity: number;
unitPrice: number;
}>) || [],
currency: (data.currency as string) || "USD",
invoiceUrl: data.invoiceUrl as string,
}),
};
default:
return null;
}
}الخطوة 12: إنشاء أداة إرسال آمنة النوع
لتجربة مطور أفضل، أنشئ أداة مساعدة توفر أمان النوع عند إرسال البريد الإلكتروني. أنشئ src/lib/send-email.ts:
import { resend, FROM_EMAIL } from "./resend";
import WelcomeEmail from "@/emails/welcome";
import PasswordResetEmail from "@/emails/password-reset";
import InvoiceEmail from "@/emails/invoice";
interface WelcomeEmailData {
username: string;
loginUrl?: string;
}
interface PasswordResetData {
username: string;
resetUrl: string;
expiresInMinutes?: number;
}
interface InvoiceData {
customerName: string;
invoiceNumber: string;
invoiceDate: string;
dueDate: string;
items: Array<{
description: string;
quantity: number;
unitPrice: number;
}>;
currency?: string;
invoiceUrl?: string;
}
type EmailMap = {
welcome: WelcomeEmailData;
"password-reset": PasswordResetData;
invoice: InvoiceData;
};
const templateMap = {
welcome: {
subject: "مرحباً بك في YourApp!",
render: (data: WelcomeEmailData) => WelcomeEmail(data),
},
"password-reset": {
subject: "إعادة تعيين كلمة المرور",
render: (data: PasswordResetData) => PasswordResetEmail(data),
},
invoice: {
subject: (data: InvoiceData) => `فاتورة ${data.invoiceNumber}`,
render: (data: InvoiceData) => InvoiceEmail(data),
},
};
export async function sendEmail<T extends keyof EmailMap>(
template: T,
to: string,
data: EmailMap[T]
) {
const config = templateMap[template];
const subject =
typeof config.subject === "function"
? config.subject(data as InvoiceData)
: config.subject;
const { data: result, error } = await resend.emails.send({
from: FROM_EMAIL,
to: [to],
subject,
react: config.render(data as never),
});
if (error) {
throw new Error(`فشل إرسال بريد ${template}: ${error.message}`);
}
return result;
}الآن يمكنك استخدامها في أي مكان في شفرة الخادم مع أمان نوع كامل:
// في Server Action أو مسار API
import { sendEmail } from "@/lib/send-email";
// TypeScript يعرف بالضبط البيانات التي يحتاجها كل قالب
await sendEmail("welcome", "user@example.com", {
username: "أحمد",
loginUrl: "https://yourdomain.com/login",
});
await sendEmail("password-reset", "user@example.com", {
username: "أحمد",
resetUrl: "https://yourdomain.com/reset?token=xyz",
expiresInMinutes: 30,
});
await sendEmail("invoice", "billing@company.com", {
customerName: "شركة أكمي",
invoiceNumber: "INV-2026-042",
invoiceDate: "15 مارس 2026",
dueDate: "15 أبريل 2026",
items: [
{ description: "الخطة الاحترافية", quantity: 1, unitPrice: 29 },
],
});الخطوة 13: استخدام البريد الإلكتروني في Server Actions
أنشئ Server Action يرسل بريد ترحيب عندما يسجل مستخدم جديد. أنشئ src/app/actions/auth.ts:
"use server";
import { sendEmail } from "@/lib/send-email";
export async function signUpAction(formData: FormData) {
const email = formData.get("email") as string;
const name = formData.get("name") as string;
// ... منطق إنشاء المستخدم هنا ...
// إرسال بريد الترحيب
try {
await sendEmail("welcome", email, {
username: name,
loginUrl: `https://yourdomain.com/login`,
});
} catch (error) {
// تسجيل الخطأ دون إفشال التسجيل إذا فشل البريد
console.error("فشل إرسال بريد الترحيب:", error);
}
return { success: true };
}الخطوة 14: اختبار تسليم البريد الإلكتروني
اختبر مسار API باستخدام curl:
curl -X POST http://localhost:3000/api/email/send \
-H "Content-Type: application/json" \
-d '{
"template": "welcome",
"to": "your-email@example.com",
"data": {
"username": "مستخدم تجريبي"
}
}'يجب أن تتلقى بريد الترحيب في صندوق الوارد. تحقق من لوحة تحكم Resend لرؤية حالة التسليم ومعدلات الفتح ومعلومات الارتداد.
الخطوة 15: إضافة Webhooks لتتبع التسليم
يوفر Resend خطافات ويب لتتبع أحداث البريد الإلكتروني. أنشئ src/app/api/email/webhook/route.ts:
import { NextRequest, NextResponse } from "next/server";
interface ResendWebhookEvent {
type:
| "email.sent"
| "email.delivered"
| "email.bounced"
| "email.complained"
| "email.opened"
| "email.clicked";
data: {
email_id: string;
to: string[];
created_at: string;
};
}
export async function POST(request: NextRequest) {
const body = (await request.json()) as ResendWebhookEvent;
switch (body.type) {
case "email.delivered":
console.log(`تم تسليم البريد ${body.data.email_id} إلى ${body.data.to}`);
// تحديث قاعدة البيانات
break;
case "email.bounced":
console.log(`ارتد البريد ${body.data.email_id}`);
// تعليم عنوان البريد كغير صالح
break;
case "email.complained":
console.log(`شكوى بريد مزعج لـ ${body.data.email_id}`);
// إلغاء اشتراك المستخدم فوراً
break;
case "email.opened":
console.log(`تم فتح البريد ${body.data.email_id}`);
// تتبع التفاعل
break;
}
return NextResponse.json({ received: true });
}سجّل عنوان URL لهذا الـ webhook في لوحة تحكم Resend تحت Webhooks.
استكشاف الأخطاء وإصلاحها
المشاكل الشائعة وحلولها:
البريد الإلكتروني لا يصل إلى صندوق الوارد:
- تحقق من لوحة تحكم Resend لحالة التسليم
- تحقق من سجلات DNS لنطاقك (SPF، DKIM، DMARC)
- أثناء التطوير، استخدم نطاق اختبار Resend الذي يرسل فقط إلى العناوين الموثّقة
معاينة React Email لا تُحمّل:
- تأكد أن المنفذ 3001 غير مستخدم
- تحقق أن ملفات البريد تصدّر مكوناً افتراضياً
- تحقق أن جميع الاستيرادات من
@react-email/componentsصحيحة
أخطاء TypeScript في قوالب البريد:
- قوالب البريد تستخدم أنماطاً مضمّنة (وليس Tailwind) — استخدم أنواع
React.CSSProperties - خاصية
styleعلى مكونات React Email تتوقع أسماء خصائص CSS محددة
البريد الإلكتروني يظهر بشكل مختلف عبر العملاء:
- اختبر دائماً مع عملاء متعددين (Gmail، Outlook، Apple Mail)
- استخدم الجداول للتخطيطات المعقدة — بعض العملاء لا تدعم flexbox أو grid
- استخدم الأنماط المضمّنة — CSS الخارجي يُحذف من قبل معظم عملاء البريد
- احتفظ بالصور بعرض أقل من 600 بكسل
الخطوات التالية
- أضف الإرسال المجمّع باستخدام
resend.batch.send()للنشرات الإخبارية - نفّذ جدولة البريد الإلكتروني بمعامل
scheduledAt - أعد اختبارات A/B لعناوين الموضوع باستخدام الدعم المدمج في Resend
- أضف إدارة إلغاء الاشتراك مع ترويسات إلغاء الاشتراك بنقرة واحدة
- استكشف Resend Audiences لإدارة قوائم المشتركين
- ابنِ صفحة تفضيلات الإشعارات للسماح للمستخدمين بالتحكم في الرسائل التي يتلقونها
الخلاصة
لقد بنيت نظام بريد إلكتروني كامل للمعاملات باستخدام React Email و Resend و Next.js. رسائلك مبنية بمكونات React، آمنة النوع، قابلة للمعاينة في المتصفح أثناء التطوير، وتُسلّم بموثوقية عبر بنية Resend التحتية.
الجمع بين نموذج مكونات React Email وواجهة برمجة تطبيقات التسليم الحديثة من Resend يمنحك أفضل تجربة مطور للبريد الإلكتروني في نظام JavaScript البيئي. القوالب هي مجرد مكونات React — يمكنك تركيبها ومشاركة الخصائص واختبارها تماماً مثل شفرة واجهة المستخدم. لا مزيد من حيل الجداول المضمّنة أو نسخ ولصق HTML بين الأدوات.
مع نمو تطبيقك، تتوسع هذه الأساسات معك — أضف قوالب جديدة بإنشاء مكونات React جديدة، ووسّع أداة sendEmail بأنواع قوالب جديدة، وراقب كل شيء عبر لوحة تحكم Resend.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء واجهات برمجة تطبيقات آمنة الأنواع من البداية للنهاية مع tRPC و Next.js App Router
تعلم كيفية بناء واجهات برمجة تطبيقات آمنة الأنواع بالكامل مع tRPC و Next.js 15 App Router. يغطي هذا الدليل العملي إعداد الموجه والإجراءات والوسيط وتكامل React Query والاستدعاءات من جانب الخادم — كل ذلك دون كتابة أي مخطط API.

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

Better Auth مع Next.js 15: الدليل الشامل للمصادقة في 2026
تعلم كيفية تنفيذ نظام مصادقة متكامل في Next.js 15 باستخدام Better Auth. يغطي هذا الدليل تسجيل الدخول بالبريد الإلكتروني وOAuth والجلسات وحماية المسارات والتحكم بالأدوار.