إطار Next.js
البريد الإلكتروني والإشعارات
البريد الإلكتروني والإشعارات
أنظمة البريد الإلكتروني والإشعارات ضرورية لمشاركة المستخدم والتواصل. يغطي هذا الدرس إرسال رسائل البريد الإلكتروني، وتنفيذ أنظمة الإشعارات، واستخدام React Email لقوالب جميلة، وإدارة المهام في الخلفية.
خيارات إرسال البريد الإلكتروني
عدة أساليب لإرسال رسائل البريد الإلكتروني في Next.js:
/* 1. Nodemailer - SMTP مستضاف ذاتيًا */
الإيجابيات: تحكم كامل، لا حدود لواجهة برمجة التطبيقات
السلبيات: تعقيد التكوين، مخاوف قابلية التسليم
/* 2. SendGrid - خدمة البريد الإلكتروني التعاملي */
الإيجابيات: تسليم موثوق، تحليلات، قوالب
السلبيات: التسعير، حدود معدل واجهة برمجة التطبيقات
/* 3. Resend - واجهة برمجة تطبيقات بريد إلكتروني حديثة */
الإيجابيات: ودية للمطورين، دعم React Email
السلبيات: خدمة أحدث، نظام بيئي أصغر
/* 4. AWS SES - خدمة البريد الإلكتروني من أمازون */
الإيجابيات: فعال من حيث التكلفة، حجم كبير
السلبيات: تعقيد AWS، متطلبات التحقق
إعداد Nodemailer
أرسل رسائل البريد الإلكتروني باستخدام SMTP مع Nodemailer:
تثبيت وتكوين Nodemailer
# تثبيت Nodemailer
npm install nodemailer
# متغيرات البيئة
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
EMAIL_FROM=noreply@yourapp.com
أداة خدمة البريد الإلكتروني
// lib/email.ts
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: false, // true لـ 465، false للمنافذ الأخرى
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
export async function sendEmail({
to,
subject,
html,
text,
}: {
to: string;
subject: string;
html?: string;
text?: string;
}) {
try {
const info = await transporter.sendMail({
from: process.env.EMAIL_FROM,
to,
subject,
text,
html,
});
console.log('تم إرسال البريد الإلكتروني:', info.messageId);
return { success: true, messageId: info.messageId };
} catch (error) {
console.error('خطأ في البريد الإلكتروني:', error);
throw error;
}
}
مسار API لإرسال البريد الإلكتروني
// app/api/send-email/route.ts
import { NextRequest } from 'next/server';
import { sendEmail } from '@/lib/email';
export async function POST(request: NextRequest) {
try {
const { to, subject, message } = await request.json();
await sendEmail({
to,
subject,
text: message,
html: `<p>${message}</p>`,
});
return Response.json({ success: true });
} catch (error) {
return Response.json(
{ error: 'فشل إرسال البريد الإلكتروني' },
{ status: 500 }
);
}
}
تكامل Resend
توفر Resend واجهة برمجة تطبيقات بريد إلكتروني حديثة وودية للمطورين:
إعداد Resend
# تثبيت Resend SDK
npm install resend
# متغير البيئة
RESEND_API_KEY=re_your_api_key
خدمة البريد الإلكتروني Resend
// lib/resend.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendEmailWithResend({
to,
subject,
html,
from = 'onboarding@resend.dev',
}: {
to: string;
subject: string;
html: string;
from?: string;
}) {
try {
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
});
if (error) {
throw error;
}
return { success: true, id: data.id };
} catch (error) {
console.error('خطأ Resend:', error);
throw error;
}
}
React Email لقوالب جميلة
يتيح لك React Email إنشاء قوالب بريد إلكتروني متجاوبة باستخدام React:
تثبيت React Email
# تثبيت React Email
npm install react-email @react-email/components
# إضافة نص بريد إلكتروني للتطوير إلى package.json
{
"scripts": {
"email:dev": "email dev"
}
}
# معاينة رسائل البريد الإلكتروني في المتصفح
npm run email:dev
إنشاء قالب بريد إلكتروني
// emails/WelcomeEmail.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Link,
Preview,
Section,
Text,
} from '@react-email/components';
interface WelcomeEmailProps {
username: string;
loginUrl: string;
}
export default function WelcomeEmail({
username,
loginUrl,
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>مرحبًا بك في منصتنا!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>مرحبًا، {username}!</Heading>
<Text style={text}>
شكرًا على التسجيل. نحن متحمسون لانضمامك إلينا!
</Text>
<Section style={buttonContainer}>
<Button style={button} href={loginUrl}>
ابدأ الآن
</Button>
</Section>
<Text style={text}>
إذا كان لديك أي أسئلة، قم بالرد على هذا البريد الإلكتروني.
</Text>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
};
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '20px 0 48px',
marginBottom: '64px',
};
const h1 = {
color: '#333',
fontSize: '24px',
fontWeight: 'bold',
margin: '40px 0',
padding: '0',
};
const text = {
color: '#333',
fontSize: '16px',
lineHeight: '26px',
};
const buttonContainer = {
padding: '27px 0 27px',
};
const button = {
backgroundColor: '#5469d4',
borderRadius: '5px',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block',
padding: '12px 20px',
};
تقديم وإرسال قالب البريد الإلكتروني
// app/api/send-welcome/route.ts
import { NextRequest } from 'next/server';
import { render } from '@react-email/render';
import WelcomeEmail from '@/emails/WelcomeEmail';
import { sendEmailWithResend } from '@/lib/resend';
export async function POST(request: NextRequest) {
try {
const { email, username } = await request.json();
const emailHtml = render(
WelcomeEmail({
username,
loginUrl: 'https://yourapp.com/login',
})
);
await sendEmailWithResend({
to: email,
subject: 'مرحبًا بك في منصتنا!',
html: emailHtml,
});
return Response.json({ success: true });
} catch (error) {
return Response.json(
{ error: 'فشل إرسال بريد الترحيب الإلكتروني' },
{ status: 500 }
);
}
}
بنية نظام الإشعارات
صمم نظام إشعارات مرنًا يدعم قنوات متعددة:
مخطط قاعدة البيانات
// prisma/schema.prisma
model Notification {
id String @id @default(cuid())
userId String
type String // 'info', 'success', 'warning', 'error'
title String
message String
read Boolean @default(false)
actionUrl String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
model NotificationPreference {
id String @id @default(cuid())
userId String
email Boolean @default(true)
push Boolean @default(true)
sms Boolean @default(false)
user User @relation(fields: [userId], references: [id])
}
خدمة الإشعارات
// lib/notifications.ts
import { prisma } from '@/lib/prisma';
import { sendEmailWithResend } from '@/lib/resend';
export type NotificationType = 'info' | 'success' | 'warning' | 'error';
export async function createNotification({
userId,
type,
title,
message,
actionUrl,
sendEmail = false,
}: {
userId: string;
type: NotificationType;
title: string;
message: string;
actionUrl?: string;
sendEmail?: boolean;
}) {
// إنشاء إشعار داخل التطبيق
const notification = await prisma.notification.create({
data: {
userId,
type,
title,
message,
actionUrl,
},
});
// التحقق من تفضيلات المستخدم
const preferences = await prisma.notificationPreference.findUnique({
where: { userId },
});
// إرسال بريد إلكتروني إذا كان مفعلاً
if (sendEmail && preferences?.email) {
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (user?.email) {
await sendEmailWithResend({
to: user.email,
subject: title,
html: `
<h2>${title}</h2>
<p>${message}</p>
${actionUrl ? `<a href="${actionUrl}">عرض التفاصيل</a>` : ''}
`,
});
}
}
return notification;
}
export async function markAsRead(notificationId: string) {
return await prisma.notification.update({
where: { id: notificationId },
data: { read: true },
});
}
export async function getUnreadCount(userId: string) {
return await prisma.notification.count({
where: {
userId,
read: false,
},
});
}
مسارات API للإشعارات
// app/api/notifications/route.ts
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
const session = await getServerSession();
if (!session?.user?.id) {
return Response.json({ error: 'غير مصرح' }, { status: 401 });
}
const notifications = await prisma.notification.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' },
take: 20,
});
return Response.json({ notifications });
}
// app/api/notifications/[id]/read/route.ts
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession();
if (!session?.user?.id) {
return Response.json({ error: 'غير مصرح' }, { status: 401 });
}
await prisma.notification.update({
where: {
id: params.id,
userId: session.user.id,
},
data: { read: true },
});
return Response.json({ success: true });
}
مكون الإشعارات داخل التطبيق
قم ببناء قائمة منسدلة للإشعارات لواجهة المستخدم:
// components/NotificationBell.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
interface Notification {
id: string;
type: string;
title: string;
message: string;
read: boolean;
actionUrl?: string;
createdAt: string;
}
export default function NotificationBell() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const router = useRouter();
useEffect(() => {
fetchNotifications();
}, []);
const fetchNotifications = async () => {
const response = await fetch('/api/notifications');
const data = await response.json();
setNotifications(data.notifications);
setUnreadCount(data.notifications.filter((n: Notification) => !n.read).length);
};
const markAsRead = async (id: string) => {
await fetch(`/api/notifications/${id}/read`, { method: 'POST' });
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, read: true } : n))
);
setUnreadCount((prev) => Math.max(0, prev - 1));
};
const handleNotificationClick = (notification: Notification) => {
markAsRead(notification.id);
if (notification.actionUrl) {
router.push(notification.actionUrl);
}
setIsOpen(false);
};
return (
<div style={{ position: 'relative' }}>
<button onClick={() => setIsOpen(!isOpen)}>
🔔
{unreadCount > 0 && (
<span style={{
position: 'absolute',
top: '-5px',
right: '-5px',
background: 'red',
color: 'white',
borderRadius: '50%',
width: '20px',
height: '20px',
fontSize: '12px',
}}>
{unreadCount}
</span>
)}
</button>
{isOpen && (
<div style={{
position: 'absolute',
top: '100%',
right: 0,
width: '350px',
maxHeight: '400px',
overflow: 'auto',
background: 'white',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
}}>
{notifications.length === 0 ? (
<p style={{ padding: '20px', textAlign: 'center' }}>
لا توجد إشعارات
</p>
) : (
notifications.map((notification) => (
<div
key={notification.id}
onClick={() => handleNotificationClick(notification)}
style={{
padding: '12px',
borderBottom: '1px solid #eee',
cursor: 'pointer',
background: notification.read ? 'white' : '#f0f0f0',
}}
>
<strong>{notification.title}</strong>
<p style={{ margin: '5px 0', fontSize: '14px' }}>
{notification.message}
</p>
<small style={{ color: '#666' }}>
{new Date(notification.createdAt).toLocaleString()}
</small>
</div>
))
)}
</div>
)}
</div>
);
}
المهام في الخلفية باستخدام BullMQ
معالجة رسائل البريد الإلكتروني بشكل غير متزامن مع قوائم انتظار المهام:
إعداد BullMQ
# تثبيت BullMQ و Redis
npm install bullmq ioredis
# Docker Redis (للتطوير)
docker run -d -p 6379:6379 redis
# متغير البيئة
REDIS_URL=redis://localhost:6379
إعداد قائمة انتظار البريد الإلكتروني
// lib/queue.ts
import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';
import { sendEmailWithResend } from './resend';
const connection = new IORedis(process.env.REDIS_URL!);
export const emailQueue = new Queue('email', { connection });
// عامل البريد الإلكتروني
export const emailWorker = new Worker(
'email',
async (job) => {
const { to, subject, html } = job.data;
await sendEmailWithResend({ to, subject, html });
console.log(`تم إرسال البريد الإلكتروني إلى ${to}`);
},
{ connection }
);
emailWorker.on('completed', (job) => {
console.log(`اكتملت المهمة ${job.id}`);
});
emailWorker.on('failed', (job, err) => {
console.error(`فشلت المهمة ${job?.id}:`, err);
});
إضافة مهام البريد الإلكتروني إلى قائمة الانتظار
// app/api/queue-email/route.ts
import { NextRequest } from 'next/server';
import { emailQueue } from '@/lib/queue';
export async function POST(request: NextRequest) {
try {
const { to, subject, html } = await request.json();
await emailQueue.add('send-email', {
to,
subject,
html,
});
return Response.json({ success: true, message: 'تمت إضافة البريد الإلكتروني إلى قائمة الانتظار' });
} catch (error) {
return Response.json(
{ error: 'فشل إضافة البريد الإلكتروني إلى قائمة الانتظار' },
{ status: 500 }
);
}
}
رسائل البريد الإلكتروني المجدولة
أرسل رسائل البريد الإلكتروني في أوقات أو فترات محددة:
// lib/scheduled-emails.ts
import { emailQueue } from './queue';
export async function scheduleEmail({
to,
subject,
html,
sendAt,
}: {
to: string;
subject: string;
html: string;
sendAt: Date;
}) {
const delay = sendAt.getTime() - Date.now();
await emailQueue.add(
'send-email',
{ to, subject, html },
{ delay }
);
}
// إرسال الملخص اليومي
export async function scheduleDailyDigest(userId: string) {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0); // 9 صباحًا
await emailQueue.add(
'daily-digest',
{ userId },
{
delay: tomorrow.getTime() - Date.now(),
repeat: { pattern: '0 9 * * *' }, // Cron: يوميًا في الساعة 9 صباحًا
}
);
}
أفضل ممارسات البريد الإلكتروني: تحقق دائمًا من عناوين البريد الإلكتروني. قم بتنفيذ وظيفة إلغاء الاشتراك. استخدم قوائم الانتظار لرسائل البريد الإلكتروني الجماعية. راقب معدلات التسليم ومعدلات الارتداد. قدم بدائل نصية عادية. اختبر رسائل البريد الإلكتروني عبر عملاء مختلفين.
تمرين: إنشاء نظام إشعارات
- قم بإعداد إرسال البريد الإلكتروني باستخدام Resend أو Nodemailer
- قم بإنشاء قوالب React Email للترحيب وإعادة تعيين كلمة المرور ورسائل البريد الإلكتروني للإشعارات
- قم ببناء مخطط قاعدة بيانات للإشعارات والتفضيلات
- قم بتنفيذ مكون جرس الإشعارات داخل التطبيق مع عدد غير مقروء
- قم بإنشاء مسارات API لجلب الإشعارات ووضع علامة عليها كمقروءة
- قم بإعداد BullMQ لمعالجة البريد الإلكتروني في الخلفية
- قم بتنفيذ رسائل البريد الإلكتروني المجدولة (الملخص اليومي، التذكيرات)
- أضف صفحة تفضيلات الإشعارات حيث يمكن للمستخدمين تخصيص الإعدادات
اعتبارات الإنتاج: قم بتنفيذ تحديد المعدل على إرسال البريد الإلكتروني. تعامل مع حالات الارتداد والشكاوى. قم بتخزين سجلات تسليم البريد الإلكتروني. التزم بقوانين مكافحة الرسائل غير المرغوب فيها (CAN-SPAM، GDPR). راقب صحة قائمة الانتظار وأعد محاولة المهام الفاشلة. استخدم قوائم انتظار منفصلة لأولويات مختلفة.