تقنية
بناء نظام إدارة محتوى مخصّص بـ Prisma و Next.js
نظرة في كيف يصنع الجمع بين App Router في Next.js و Prisma ORM بيئة تطوير سريعة وآمنة الأنواع لأنظمة إدارة المحتوى المخصّصة.

قوّة Prisma
يجعل Prisma نمذجة قاعدة البيانات بديهية. تصف بياناتك مرّة واحدة في ملف مخطّط، فتحصل على عميل كامل الأنواع — يعرف جداولك وأعمدتك وعلاقاتك، ويرفض أن يُترجم حين تطلب حقلاً غير موجود.
وبجمعه مع Server Components في Next.js تستطيع الاستعلام من ذلك العميل مباشرةً داخل المكوّن الذي يعرض البيانات. لا طبقة API، ولا خطوة تحويل، ولا قفزة عبر الشبكة داخل تطبيقك أنت.
ولمَ لا نستعمل WordPress ببساطة؟
سؤال وجيه، والجواب الصادق لمدوّنة هو غالباً: «تستطيع».
ويكفّ عن كونه الجواب الصحيح لحظة يصير محتواك شيئاً غير المقالات. فمكتب عقارات لديه عروض بأسعار ومواقع ومعارض صور. ومطعم لديه قائمة طعام تتغيّر. وورشة لديها أعمال تنتقل بين حالات. ونمذجة أيٍّ من هذه في نظام مصمَّم للتدوينات تعني إمّا ليّه عن شكله بحقول مخصّصة، أو تركيب ثلاث إضافات تريد كلٌّ منها اشتراكاً ولا تتّفق أيٌّ منها مع الأخرى على شيء.
أمّا نظام إدارة المحتوى المخصّص فيبدأ من شكل العمل الحقيقي:
model Project {
id String @id @default(cuid())
title String
slug String @unique
summary String
coverImageUrl String?
featured Boolean @default(false)
publishedAt DateTime?
updatedAt DateTime @updatedAt
features ProjectFeature[]
technologies ProjectTechnology[]
}
هذا هو التعريف كلّه. وslug فريد لأنّ مشروعين يتقاسمان رابطاً واحداً خطأٌ برمجي، والآن تفرض قاعدة البيانات ذلك بدل أن نأمل أن يفرضه النموذج.
قراءة البيانات بلا API بينهما
في App Router تكون الصفحة دالّة خادم غير متزامنة، فيعيش الاستعلام داخل الصفحة:
// src/app/(website)/projects/page.tsx
import { prisma } from "@/lib/db/client";
export default async function ProjectsPage() {
const projects = await prisma.project.findMany({
where: { publishedAt: { not: null } },
orderBy: [{ featured: "desc" }, { publishedAt: "desc" }],
include: { technologies: true },
});
return (
<ul>
{projects.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
projects كامل الأنواع، وp.technologies كامل الأنواع، ولم يُكتب شيء من ذلك يدوياً. أعِد تسمية title إلى name في المخطّط، فيفشل فوراً في الترجمة كل موضع كان يقرأه — بدل أن يعرض undefined على زبون بعد ستة أسابيع.
النصف الذي يقرّر إن كان سيُستعمل أصلاً
بناء الموقع العام هو الجزء السهل. أمّا نظام إدارة المحتوى فيحيا أو يموت بحسب ما إذا كان صاحبه يفتحه فعلاً، وذلك يعود إلى أمرين: أن تطابق شاشات التحرير الكلمات التي يستعملها هو أصلاً لوصف عمله، وألّا يكون في الضغط على أي شيء ما يخيف.
عمليات الكتابة تمرّ عبر Server Actions، وكلٌّ منها يحرس نفسه:
"use server";
export async function updateProject(id: string, formData: FormData) {
const adminId = await getAdminId();
if (!adminId) throw new Error("Unauthorised");
const parsed = ProjectSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { ok: false, error: "Some fields need attention." };
}
await prisma.project.update({ where: { id }, data: parsed.data });
revalidatePath("/projects");
revalidatePath(`/projects/${parsed.data.slug}`);
return { ok: true, error: null };
}
تفصيلان يستحقّان النقل. الأول أنّ التحقّق من الصلاحية يجلس داخل الدالّة، لا في المكوّن الذي يرسم الزرّ — فـ Server Action نقطة نهاية HTTP حقيقية، والواجهة ليست ما يحميها. والثاني أنّ استدعاءات revalidatePath هي ما يجعل الصفحة العامة تتحدّث لحظة يضغط صاحبها «حفظ»، بلا إعادة بناء وبلا تخزين مؤقّت تضطرّ إلى شرحه له.
كم يكلّف، بصراحة
نظام إدارة محتوى مخصّص عملٌ أكبر في البداية من تركيب قالب. وهو يستحقّ ذلك حين يكون للمحتوى بنية، أو حين يحرّره أحد أسبوعياً، أو حين يحتاج الموقع أن ينمو لاحقاً إلى حجوزات أو حسابات أو لوحة تحكّم. ولا يستحقّه لخمس صفحات لن تتغيّر أبداً — فلتلك، خمس صفحات هي الجواب الصحيح.
والمكسب هو الملكية. لا رسوم منصّة شهرية، ولا إضافة تتعطّل عند التحديث، والمصدر يجلس في مستودع باسمك. وأي مطوّر يعرف React يستطيع متابعته.
وهذه هي المقاربة وراء معظم عملي في تطوير المواقع لشركات طرابلس — كتالوجات منتجات، وحجوزات، وتتبّع طلبات، مع لوحة تحكّم يستطيع الموظفون استعمالها بلا تدريب. أمّا Server Actions التي تقوم بالكتابة أعلاه فمشروحة بعمق أكبر في دليل Server Actions في Next.js.