إطار Next.js

البريد الإلكتروني والإشعارات

23 دقيقة الدرس 30 من 40

البريد الإلكتروني والإشعارات

أنظمة البريد الإلكتروني والإشعارات ضرورية لمشاركة المستخدم والتواصل. يغطي هذا الدرس إرسال رسائل البريد الإلكتروني، وتنفيذ أنظمة الإشعارات، واستخدام 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 صباحًا } ); }
أفضل ممارسات البريد الإلكتروني: تحقق دائمًا من عناوين البريد الإلكتروني. قم بتنفيذ وظيفة إلغاء الاشتراك. استخدم قوائم الانتظار لرسائل البريد الإلكتروني الجماعية. راقب معدلات التسليم ومعدلات الارتداد. قدم بدائل نصية عادية. اختبر رسائل البريد الإلكتروني عبر عملاء مختلفين.

تمرين: إنشاء نظام إشعارات

  1. قم بإعداد إرسال البريد الإلكتروني باستخدام Resend أو Nodemailer
  2. قم بإنشاء قوالب React Email للترحيب وإعادة تعيين كلمة المرور ورسائل البريد الإلكتروني للإشعارات
  3. قم ببناء مخطط قاعدة بيانات للإشعارات والتفضيلات
  4. قم بتنفيذ مكون جرس الإشعارات داخل التطبيق مع عدد غير مقروء
  5. قم بإنشاء مسارات API لجلب الإشعارات ووضع علامة عليها كمقروءة
  6. قم بإعداد BullMQ لمعالجة البريد الإلكتروني في الخلفية
  7. قم بتنفيذ رسائل البريد الإلكتروني المجدولة (الملخص اليومي، التذكيرات)
  8. أضف صفحة تفضيلات الإشعارات حيث يمكن للمستخدمين تخصيص الإعدادات
اعتبارات الإنتاج: قم بتنفيذ تحديد المعدل على إرسال البريد الإلكتروني. تعامل مع حالات الارتداد والشكاوى. قم بتخزين سجلات تسليم البريد الإلكتروني. التزم بقوانين مكافحة الرسائل غير المرغوب فيها (CAN-SPAM، GDPR). راقب صحة قائمة الانتظار وأعد محاولة المهام الفاشلة. استخدم قوائم انتظار منفصلة لأولويات مختلفة.