الكتابات/tutorial/2026/05
Tutorial15 مايو 2026·28 دقيقة

بناء نظام إشعارات متكامل مع Novu و Next.js 15

تعلّم كيف تبني نظام إشعارات داخل التطبيق جاهزاً للإنتاج باستخدام Novu v2 و Next.js 15. يغطي هذا الدرس تعريف سير العمل بالكود، ومكوّن Inbox الجاهز، وقنوات البريد الإلكتروني، والتحديثات اللحظية عبر WebSocket.

تحتاج تطبيقات SaaS الحديثة إلى نظام إشعارات موثوق ومتعدد القنوات — تنبيهات داخل التطبيق، ورسائل بريد إلكتروني، وإشعارات فورية، كلها منسّقة ومتتبَّعة. بناء هذا من الصفر يعني التعامل مع خوادم WebSocket، ومزودي البريد الإلكتروني، ومخططات الإشعارات، وعدادات غير المقروء. Novu يحل كل هذا بمنصة واحدة مفتوحة المصدر.

مع Novu v2، تعرّف سير عمل الإشعارات مباشرةً بـ TypeScript، مخزّنةً بجانب كود تطبيقك. لا يوجد واجهة سحب وإفلات — سير عملك محفوظ في Git، آمن النوع، وقابل للاختبار كأي منطق أعمال آخر.

في هذا الدرس، ستبني تطبيق إدارة مهام مع نظام إشعارات متكامل: يتلقى المستخدمون تنبيهات فورية عند تعيين مهام لهم، مع رسائل بريد إلكتروني احتياطية في حالة عدم الاتصال.

المتطلبات المسبقة

قبل البداية، تأكد من توفر:

  • Node.js 20+ مثبّت على جهازك
  • مشروع Next.js 15 يعمل مع App Router و TypeScript
  • معرفة أساسية بـ React Server Components و API Routes
  • حساب Novu — سجّل مجاناً على novu.co (لا يلزم بطاقة ائتمانية)

ما ستبنيه

بنهاية هذا الدرس، سيحتوي تطبيقك على:

  • جرس إشعارات في شريط التنقل مع شارة عدد غير المقروء
  • لوحة Inbox تفتح عند النقر على الجرس، تعرض جميع الإشعارات مع إمكانية التعليم كمقروء
  • سير عمل task-assigned يرسل تنبيهاً فورياً داخل التطبيق وبريداً إلكترونياً بعد 10 دقائق
  • Server Action يطلق الإشعار عند إنشاء أو تعيين مهمة

الخطوة 1: إنشاء حساب Novu

انتقل إلى novu.co وأنشئ حساباً مجانياً. بعد تسجيل الدخول:

  1. أنشئ منظمة جديدة أو استخدم الافتراضية
  2. افتح Settings ثم API Keys
  3. انسخ Secret Key (يبدأ بـ novu_secret_)
  4. انسخ App Identifier (يبدأ بـ app_)

الخطوة 2: تثبيت مكتبات Novu

في مجلد مشروع Next.js، ثبّت الحزم الثلاث:

npm install @novu/framework @novu/react @novu/api
الحزمةالغرض
@novu/frameworkمحرك سير العمل بالكود ومساعد نقطة bridge
@novu/reactمكوّن Inbox الجاهز وخطاف useNotifications
@novu/apiعميل API جانب الخادم لإطلاق الإشعارات

الخطوة 3: إعداد متغيرات البيئة

أنشئ أو حدّث ملف .env.local:

# جانب الخادم فقط — لا تكشفه للمتصفح
NOVU_SECRET_KEY=novu_secret_your_key_here
 
# آمن للكشف للمتصفح
NEXT_PUBLIC_NOVU_APP_ID=app_your_identifier_here
 
# رابط تطبيقك العام
NEXT_PUBLIC_APP_URL=http://localhost:3000

الخطوة 4: تعريف سير عمل الإشعارات

نهج Novu v2 بالكود يتيح تعريف منطق الإشعارات كدوال TypeScript:

// lib/novu/workflows.ts
import { workflow } from '@novu/framework';
import { z } from 'zod';
 
export const taskAssignedWorkflow = workflow(
  'task-assigned',
  async ({ step, payload }) => {
    // إشعار فوري داخل التطبيق
    await step.inApp('in-app-alert', async () => ({
      subject: `مهمة جديدة: ${payload.taskTitle}`,
      body: `قام ${payload.assignerName} بتعيين مهمة لك بأولوية ${payload.priority}.`,
    }));
 
    // انتظر 10 دقائق ثم أرسل بريداً إلكترونياً
    await step.delay('wait-before-email', async () => ({
      amount: 10,
      unit: 'minutes',
    }));
 
    await step.email('email-fallback', async () => ({
      subject: `إجراء مطلوب: ${payload.taskTitle}`,
      body: `
        <h2>لديك مهمة جديدة</h2>
        <p>قام <strong>${payload.assignerName}</strong> بتعيينك على:</p>
        <h3>${payload.taskTitle}</h3>
        <p>الأولوية: ${payload.priority}</p>
        <a href="${payload.taskUrl}" style="color:#7C3AED">عرض المهمة</a>
      `,
    }));
  },
  {
    payloadSchema: z.object({
      taskTitle: z.string(),
      taskDescription: z.string().optional().default(''),
      assignerName: z.string(),
      priority: z.enum(['low', 'medium', 'high']),
      taskUrl: z.string().url(),
    }),
  }
);

نقاط مهمة:

  • معرّف سير العمل 'task-assigned' هو ما تستخدمه عند الإطلاق
  • step.inApp() يرسل تنبيهاً فورياً إلى صندوق Inbox
  • step.delay() يوقف التنفيذ 10 دقائق مع حفظ الحالة
  • payloadSchema بـ Zod يتحقق من البيانات وقت التشغيل

الخطوة 5: إنشاء نقطة Bridge

يستخدم Novu نقطة bridge في تطبيقك لاكتشاف وتنفيذ سير العمل:

// app/api/novu/route.ts
import { serve } from '@novu/framework/next';
import { taskAssignedWorkflow } from '@/lib/novu/workflows';
 
export const { GET, POST, PUT } = serve({
  workflows: [taskAssignedWorkflow],
});

بعد إنشاء هذا الملف، زامن سير عملك مع Novu Cloud. في التطوير المحلي، استخدم نفقاً لكشف خادمك:

# استخدم ngrok لكشف الخادم المحلي
ngrok http 3000
 
# ثم زامن مع رابط النفق
npx novu@latest sync --bridge-url https://your-ngrok-url.ngrok.io/api/novu

بعد المزامنة الناجحة، يظهر سير عمل task-assigned في لوحة Novu Cloud.

الخطوة 6: إضافة مكوّن Inbox

مكوّن Inbox الجاهز من Novu يعرض جرس إشعارات مع شارة غير المقروء:

// components/NotificationInbox.tsx
'use client';
 
import { NovuProvider, Inbox } from '@novu/react';
 
interface NotificationInboxProps {
  subscriberId: string;
}
 
export function NotificationInbox({ subscriberId }: NotificationInboxProps) {
  return (
    <NovuProvider
      applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
      subscriberId={subscriberId}
    >
      <Inbox
        appearance={{
          variables: {
            colorPrimary: '#7C3AED',
            colorBackground: '#1F2937',
            colorForeground: '#F9FAFB',
          },
        }}
      />
    </NovuProvider>
  );
}

subscriberId هو المعرّف الفريد لمستخدمك — عادةً ID المستخدم من قاعدة البيانات. Novu ينشئ سجل المشترك تلقائياً عند أول إشعار.

أضفه إلى شريط التنقل:

// components/Navbar.tsx
import { auth } from '@/lib/auth';
import { NotificationInbox } from './NotificationInbox';
 
export async function Navbar() {
  const session = await auth();
 
  return (
    <nav className="flex items-center justify-between px-6 py-4 border-b">
      <span className="text-xl font-bold">TaskFlow</span>
      <div className="flex items-center gap-4">
        {session?.user && (
          <NotificationInbox subscriberId={session.user.id} />
        )}
      </div>
    </nav>
  );
}

الخطوة 7: إطلاق الإشعارات من Server Actions

أنشئ أداة عميل Novu للاستخدام جانب الخادم:

// lib/novu/client.ts
import { Novu } from '@novu/api';
 
export const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY! });

ثم اطلق سير العمل من Server Action عند تعيين مهمة:

// app/actions/tasks.ts
'use server';
 
import { novu } from '@/lib/novu/client';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
 
export async function assignTask(taskId: string, assigneeId: string) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');
 
  const task = await db.task.update({
    where: { id: taskId },
    data: { assigneeId },
    include: { assignee: true },
  });
 
  // لا ترسل إشعاراً إذا عيّن المستخدم المهمة لنفسه
  if (assigneeId !== session.user.id) {
    await novu.trigger({
      name: 'task-assigned',
      to: {
        subscriberId: assigneeId,
        email: task.assignee.email,
        firstName: task.assignee.name ?? undefined,
      },
      payload: {
        taskTitle: task.title,
        taskDescription: task.description ?? '',
        assignerName: session.user.name ?? 'أحد زملائك',
        priority: task.priority,
        taskUrl: `${process.env.NEXT_PUBLIC_APP_URL}/tasks/${taskId}`,
      },
    });
  }
 
  revalidatePath('/tasks');
  return task;
}

الخطوة 8: واجهة مخصصة باستخدام useNotifications

إذا أردت تحكماً كاملاً في واجهة الإشعارات، استخدم خطاف useNotifications:

// components/CustomNotificationPanel.tsx
'use client';
 
import { useNotifications } from '@novu/react';
import { BellIcon } from 'lucide-react';
import { useState } from 'react';
 
export function CustomNotificationPanel() {
  const [open, setOpen] = useState(false);
  const { notifications, unreadCount, markAllAsRead, markAsRead, isLoading } =
    useNotifications();
 
  return (
    <div className="relative">
      <button
        onClick={() => setOpen(!open)}
        className="relative p-2 rounded-full hover:bg-gray-100"
      >
        <BellIcon className="w-5 h-5" />
        {unreadCount > 0 && (
          <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
            {unreadCount}
          </span>
        )}
      </button>
 
      {open && (
        <div className="absolute left-0 mt-2 w-80 bg-white rounded-lg shadow-xl border z-50">
          <div className="flex justify-between items-center p-4 border-b">
            <span className="font-semibold">الإشعارات</span>
            <button onClick={markAllAsRead} className="text-sm text-purple-600">
              تعليم الكل كمقروء
            </button>
          </div>
 
          {isLoading ? (
            <div className="p-4 text-center text-gray-500">جارٍ التحميل...</div>
          ) : notifications.length === 0 ? (
            <div className="p-4 text-center text-gray-500">لا توجد إشعارات بعد</div>
          ) : (
            <ul className="max-h-96 overflow-y-auto">
              {notifications.map((n) => (
                <li
                  key={n.id}
                  onClick={() => markAsRead(n.id)}
                  className={`p-4 border-b cursor-pointer hover:bg-gray-50 ${
                    !n.isRead ? 'bg-purple-50' : ''
                  }`}
                >
                  <p className="font-medium text-sm">{n.subject}</p>
                  <p className="text-xs text-gray-500 mt-1">{n.body}</p>
                </li>
              ))}
            </ul>
          )}
        </div>
      )}
    </div>
  );
}

الخطوة 9: إعدادات تفضيلات الإشعارات

أتح للمستخدمين التحكم في قنوات الإشعارات مع مكوّن Preferences الجاهز:

// components/NotificationSettings.tsx
'use client';
 
import { NovuProvider, Preferences } from '@novu/react';
 
export function NotificationSettings({ subscriberId }: { subscriberId: string }) {
  return (
    <NovuProvider
      applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
      subscriberId={subscriberId}
    >
      <Preferences />
    </NovuProvider>
  );
}

اعرض هذا المكوّن في صفحة إعدادات المستخدم. عندما يعطّل المستخدم البريد الإلكتروني لسير عمل معيّن، يتخطى Novu خطوة البريد تلقائياً دون أي تغييرات في الكود.

الخطوة 10: الاختبار والنشر

الاختبار المحلي مع Novu Local Studio

شغّل Local Studio بجانب خادم التطوير:

# طرفية 1
npm run dev
 
# طرفية 2
npx novu@latest dev

يفتح Local Studio على http://localhost:2022 ويتيح:

  • إطلاق إشعارات تجريبية مع بيانات مخصصة
  • فحص مخرجات كل خطوة
  • معاينة قوالب البريد الإلكتروني قبل ربط مزود البريد
  • مراقبة سجلات التنفيذ في الوقت الفعلي

سكريبت اختبار متكامل

// scripts/test-notification.ts
import { novu } from '../lib/novu/client';
 
async function main() {
  const result = await novu.trigger({
    name: 'task-assigned',
    to: {
      subscriberId: 'test-user-123',
      email: 'test@yourapp.com',
    },
    payload: {
      taskTitle: 'مراجعة الصفحة الرئيسية',
      taskDescription: 'تحقق من صياغة قسم البطل.',
      assignerName: 'سكريبت الاختبار',
      priority: 'medium',
      taskUrl: 'http://localhost:3000/tasks/test-001',
    },
  });
 
  console.log('تم الإطلاق:', result);
}
 
main().catch(console.error);
npx tsx scripts/test-notification.ts

افتح تطبيقك — يجب أن يظهر الإشعار في الوقت الفعلي دون إعادة تحميل الصفحة.

استكشاف الأخطاء وإصلاحها

Inbox لا يعرض الإشعارات

تحقق من أن subscriberId المُمرَّر إلى NovuProvider مطابق تماماً لـ to.subscriberId في نداء trigger. حتى اختلاف حالة الأحرف يُنشئ مشتركاً مختلفاً.

فشل novu sync

يجب أن يكون رابط bridge متاحاً من Novu Cloud. استخدم ngrok أو Cloudflare Tunnel لكشف خادمك المحلي.

خطوة البريد الإلكتروني لا تنفَّذ

ربط مزود بريد إلكتروني ضروري في لوحة Novu تحت SettingsIntegrations (Resend، SendGrid، Postmark).

الخطوات التالية

  • إشعارات الملخص — استخدم step.digest() لتجميع تحديثات متعددة في بريد يومي واحد
  • التوجيه الشرطي — تخطّ خطوة البريد إذا قرأ المشترك الإشعار داخل التطبيق خلال فترة الانتظار
  • سير عمل متعددة — أضف سير عمل لتعليقات المهام، وتذكيرات المواعيد النهائية، والإشارات
  • إشعارات الدفع — ربط Firebase Cloud Messaging لتوسيع نظامك إلى الأجهزة المحمولة
  • استضافة ذاتية — Novu مفتوح المصدر بالكامل وقابل للنشر بـ Docker

الخلاصة

بنيت الآن نظام إشعارات على مستوى الإنتاج باستخدام Novu v2 و Next.js 15. يحصل مستخدموك على تنبيهات فورية داخل التطبيق عبر WebSocket، وواجهة Inbox أنيقة دون كتابة CSS، وبريد إلكتروني احتياطي للمستخدمين غير المتصلين — كل هذا معرَّف في عشرات الأسطر من TypeScript المحفوظة في مستودعك. نهج workflow-as-code في Novu يجعل الإشعارات مواطناً من الدرجة الأولى في قاعدة الكود: مُصنَّفة بالنسخ، قابلة للاختبار، وسهلة التوسعة مع نمو تطبيقك.