بناء موقع محتوى متكامل باستخدام Payload CMS 3 و Next.js

Payload CMS 3 هو أول نظام إدارة محتوى headless يعمل داخل تطبيق Next.js. لا حاجة لخادم منفصل أو API خارجي — لوحة الإدارة والواجهة البرمجية والواجهة الأمامية كلها تعمل كتطبيق Next.js موحد. في هذا الدرس، ستبني موقع محتوى متكامل من الصفر.
ما ستبنيه
منصة TechPulse Blog — موقع محتوى كامل الميزات يتضمن:
- لوحة إدارة Payload CMS 3 مدمجة مباشرة في Next.js App Router
- مجموعات للمقالات والتصنيفات والوسائط
- محرر نصوص غني مع Lexical (المحرر الافتراضي لـ Payload)
- رفع صور مع تحسين تلقائي
- مصادقة المستخدمين والتحكم بالوصول حسب الأدوار
- سير عمل المسودة/النشر
- واجهات REST و GraphQL (تُنشأ تلقائياً)
- إدارة بيانات SEO الوصفية
- واجهة أمامية متجاوبة مع Tailwind CSS
- نشر في بيئة الإنتاج مع PostgreSQL
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- npm أو pnpm كمدير حزم
- معرفة أساسية بـ React و TypeScript و Next.js
- محرر أكواد (يُوصى بـ VS Code)
- Docker مثبت (لـ PostgreSQL محلي) أو رابط قاعدة بيانات سحابية
- فهم أساسي لأنظمة إدارة المحتوى
لماذا Payload CMS 3؟
يمثل Payload CMS 3 تحولاً جذرياً في طريقة عمل أنظمة إدارة المحتوى headless. بدلاً من العمل كخدمة خلفية منفصلة، يندمج Payload 3 مباشرة في تطبيق Next.js:
| الميزة | Payload CMS 3 | Strapi | Sanity | Contentful |
|---|---|---|---|---|
| البنية | داخل Next.js | خادم منفصل | SaaS مستضاف | SaaS مستضاف |
| واجهة الإدارة | مسار Next.js | تطبيق منفصل | Studio (React) | لوحة سحابية |
| قاعدة البيانات | Postgres/MongoDB | Postgres/MySQL | مستضافة | مستضافة |
| أمان الأنواع | TypeScript كامل | جزئي | أنواع GROQ | أنواع SDK |
| استضافة ذاتية | نعم (مضمن) | نعم (منفصل) | لا | لا |
| API | REST + GraphQL | REST + GraphQL | GROQ | REST + GraphQL |
| التكلفة | مجاني ومفتوح المصدر | طبقة مجانية | طبقة مجانية | طبقة مجانية |
الميزة الأساسية: قاعدة كود واحدة، نشر واحد، نموذج ذهني واحد. نظام إدارة المحتوى هو مجرد جزء آخر من تطبيق Next.js.
الخطوة 1: إنشاء المشروع
يوفر Payload أداة CLI رسمية create-payload-app لبناء مشروع كامل:
npx create-payload-app@latest techpulseعند السؤال، اختر:
- اسم المشروع:
techpulse - قاعدة البيانات: PostgreSQL (موصى به للإنتاج)
- القالب:
website(يتضمن مجموعات شائعة)
لاستخدام PostgreSQL محلي عبر Docker:
docker run -d \
--name payload-postgres \
-e POSTGRES_USER=payload \
-e POSTGRES_PASSWORD=payload123 \
-e POSTGRES_DB=techpulse \
-p 5432:5432 \
postgres:16-alpineالآن قم بالإعداد وتشغيل خادم التطوير:
cd techpulse
cp .env.example .envحدّث ملف .env:
DATABASE_URI=postgresql://payload:payload123@localhost:5432/techpulse
PAYLOAD_SECRET=your-super-secret-key-change-this-in-production
NEXT_PUBLIC_SITE_URL=http://localhost:3000npm run devزر http://localhost:3000/admin لإنشاء أول مستخدم مسؤول. واجهتك الأمامية ستكون على http://localhost:3000.
الخطوة 2: فهم بنية المشروع
إليك البنية الأساسية لمشروع Payload 3 + Next.js:
techpulse/
├── app/
│ ├── (frontend)/ # مسارات موقعك العام
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── posts/
│ │ └── [slug]/
│ │ └── page.tsx
│ ├── (payload)/ # مسارات لوحة إدارة Payload
│ │ └── admin/
│ │ └── [[...segments]]/
│ │ └── page.tsx
│ ├── api/ # واجهة REST + GraphQL لـ Payload
│ │ └── [...slug]/
│ │ └── route.ts
│ └── layout.tsx # التخطيط الجذري
├── collections/ # إعدادات مجموعات Payload
│ ├── Posts.ts
│ ├── Categories.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/ # إعدادات Payload العامة
│ └── SiteSettings.ts
├── payload.config.ts # إعداد Payload الرئيسي
├── payload-types.ts # أنواع TypeScript المُنشأة تلقائياً
└── tailwind.config.ts
لاحظ كيف يستخدم Payload مجموعات مسارات Next.js: (payload) للوحة الإدارة و(frontend) لموقعك العام. يتعايشان في نفس تطبيق Next.js.
الخطوة 3: إعداد Payload
قلب نظام إدارة المحتوى هو payload.config.ts. دعنا نقوم بإعداده:
// payload.config.ts
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { Posts } from './collections/Posts'
import { Categories } from './collections/Categories'
import { Media } from './collections/Media'
import { Users } from './collections/Users'
import { SiteSettings } from './globals/SiteSettings'
import path from 'path'
import { fileURLToPath } from 'url'
import sharp from 'sharp'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: Users.slug,
meta: {
titleSuffix: '- TechPulse Admin',
},
},
collections: [Posts, Categories, Media, Users],
globals: [SiteSettings],
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI || '',
},
}),
editor: lexicalEditor(),
sharp,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
secret: process.env.PAYLOAD_SECRET || '',
})ملف الإعداد هذا يخبر Payload بكل شيء عن نظام إدارة المحتوى: أنواع المحتوى الموجودة، قاعدة البيانات المستخدمة، وكيف يجب أن تعمل لوحة الإدارة.
الخطوة 4: تعريف المجموعات
المجموعات هي اللبنات الأساسية لـ Payload. كل مجموعة تحدد نوع محتوى بحقوله والتحكم بالوصول والخطافات.
مجموعة المقالات
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
import { lexicalEditor, HTMLConverterFeature } from '@payloadcms/richtext-lexical'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'category', 'status', 'publishedAt'],
preview: (doc) => {
if (doc?.slug) {
return `${process.env.NEXT_PUBLIC_SITE_URL}/posts/${doc.slug}`
}
return null
},
},
access: {
read: ({ req }) => {
if (req.user) return true
return { status: { equals: 'published' } }
},
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
versions: {
drafts: {
autosave: { interval: 30000 },
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
minLength: 10,
maxLength: 120,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
admin: {
position: 'sidebar',
description: 'معرّف صديق لعناوين URL',
},
hooks: {
beforeValidate: [
({ value, data }) => {
if (!value && data?.title) {
return data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
}
return value
},
],
},
},
{
name: 'excerpt',
type: 'textarea',
required: true,
maxLength: 300,
},
{
name: 'coverImage',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'content',
type: 'richText',
required: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HTMLConverterFeature({}),
],
}),
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
required: true,
hasMany: false,
admin: { position: 'sidebar' },
},
{
name: 'tags',
type: 'array',
admin: { position: 'sidebar' },
fields: [
{ name: 'tag', type: 'text', required: true },
],
},
{
name: 'status',
type: 'select',
defaultValue: 'draft',
options: [
{ label: 'مسودة', value: 'draft' },
{ label: 'منشور', value: 'published' },
],
admin: { position: 'sidebar' },
},
{
name: 'publishedAt',
type: 'date',
admin: {
position: 'sidebar',
date: { pickerAppearance: 'dayAndTime' },
},
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text', maxLength: 60 },
{ name: 'metaDescription', type: 'textarea', maxLength: 160 },
{ name: 'ogImage', type: 'upload', relationTo: 'media' },
],
},
],
hooks: {
beforeChange: [
({ data }) => {
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date().toISOString()
}
return data
},
],
},
}مجموعة التصنيفات
// collections/Categories.ts
import type { CollectionConfig } from 'payload'
export const Categories: CollectionConfig = {
slug: 'categories',
admin: { useAsTitle: 'name' },
access: {
read: () => true,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
fields: [
{ name: 'name', type: 'text', required: true, unique: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'description', type: 'textarea' },
{ name: 'color', type: 'text' },
],
}مجموعة الوسائط
// collections/Media.ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
admin: { useAsTitle: 'alt' },
access: {
read: () => true,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
upload: {
staticDir: 'public/media',
mimeTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml'],
imageSizes: [
{ name: 'thumbnail', width: 400, height: 300, position: 'centre' },
{ name: 'card', width: 768, height: 432, position: 'centre' },
{ name: 'hero', width: 1920, height: undefined, position: 'centre' },
],
},
fields: [
{ name: 'alt', type: 'text', required: true },
{ name: 'caption', type: 'text' },
],
}مجموعة المستخدمين
// collections/Users.ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: { useAsTitle: 'email' },
access: {
read: () => true,
create: ({ req }) => req.user?.role === 'admin',
update: ({ req, id }) => {
if (req.user?.role === 'admin') return true
return req.user?.id === id
},
delete: ({ req }) => req.user?.role === 'admin',
},
fields: [
{ name: 'name', type: 'text', required: true },
{
name: 'role',
type: 'select',
defaultValue: 'editor',
options: [
{ label: 'مسؤول', value: 'admin' },
{ label: 'محرر', value: 'editor' },
],
access: { update: ({ req }) => req.user?.role === 'admin' },
admin: { position: 'sidebar' },
},
{ name: 'avatar', type: 'upload', relationTo: 'media' },
],
}الخطوة 5: إنشاء الإعدادات العامة
الإعدادات العامة (Globals) هي مستندات فردية — محتوى موجود مرة واحدة فقط، مثل إعدادات الموقع:
// globals/SiteSettings.ts
import type { GlobalConfig } from 'payload'
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
access: {
read: () => true,
update: ({ req }) => !!req.user,
},
fields: [
{ name: 'siteName', type: 'text', required: true, defaultValue: 'TechPulse' },
{ name: 'siteDescription', type: 'textarea', defaultValue: 'مصدرك اليومي للرؤى والدروس التقنية.' },
{ name: 'logo', type: 'upload', relationTo: 'media' },
{
name: 'socialLinks',
type: 'group',
fields: [
{ name: 'twitter', type: 'text' },
{ name: 'github', type: 'text' },
{ name: 'linkedin', type: 'text' },
],
},
{
name: 'footer',
type: 'group',
fields: [
{ name: 'copyright', type: 'text', defaultValue: '© 2026 TechPulse. جميع الحقوق محفوظة.' },
{
name: 'links',
type: 'array',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'url', type: 'text', required: true },
],
},
],
},
],
}الخطوة 6: بناء الواجهة الأمامية
الآن لنبنِ الموقع العام. يوفر Payload دالة getPayload التي تمنحك وصولاً مباشراً لبيانات نظام إدارة المحتوى — لا حاجة لاستدعاءات API لأن Payload يعمل داخل تطبيق Next.js.
مكون التخطيط
// app/(frontend)/layout.tsx
import type { Metadata } from 'next'
import { getPayload } from 'payload'
import config from '@payload-config'
import Link from 'next/link'
import './globals.css'
export async function generateMetadata(): Promise<Metadata> {
const payload = await getPayload({ config })
const settings = await payload.findGlobal({ slug: 'site-settings' })
return {
title: {
default: settings.siteName,
template: `%s | ${settings.siteName}`,
},
description: settings.siteDescription,
}
}
export default async function FrontendLayout({
children,
}: {
children: React.ReactNode
}) {
const payload = await getPayload({ config })
const settings = await payload.findGlobal({ slug: 'site-settings' })
const categories = await payload.find({
collection: 'categories',
limit: 10,
sort: 'name',
})
return (
<div className="min-h-screen flex flex-col">
<header className="border-b bg-white sticky top-0 z-50">
<nav className="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-gray-900">
{settings.siteName}
</Link>
<div className="hidden md:flex items-center gap-6">
{categories.docs.map((category) => (
<Link
key={category.id}
href={`/category/${category.slug}`}
className="text-sm text-gray-600 hover:text-gray-900 transition-colors"
>
{category.name}
</Link>
))}
</div>
</nav>
</header>
<main className="flex-1">{children}</main>
<footer className="border-t bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4 text-center text-sm text-gray-500">
{settings.footer?.copyright}
</div>
</footer>
</div>
)
}الصفحة الرئيسية — قائمة المقالات
// app/(frontend)/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import Link from 'next/link'
import Image from 'next/image'
import type { Post, Media, Category } from '@/payload-types'
export default async function HomePage() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
sort: '-publishedAt',
limit: 12,
depth: 2,
})
const [featured, ...rest] = posts.docs
return (
<div className="max-w-6xl mx-auto px-4 py-12">
{featured && <FeaturedPost post={featured} />}
<section className="mt-16">
<h2 className="text-2xl font-bold mb-8">أحدث المقالات</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{rest.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
</section>
</div>
)
}
function FeaturedPost({ post }: { post: Post }) {
const coverImage = post.coverImage as Media
const category = post.category as Category
return (
<Link href={`/posts/${post.slug}`} className="group block">
<article className="grid md:grid-cols-2 gap-8 items-center">
<div className="relative aspect-video rounded-xl overflow-hidden">
{coverImage?.url && (
<Image
src={coverImage.url}
alt={coverImage.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500"
priority
/>
)}
</div>
<div>
{category && (
<span
className="inline-block px-3 py-1 text-xs font-medium rounded-full text-white mb-4"
style={{ backgroundColor: category.color || '#3B82F6' }}
>
{category.name}
</span>
)}
<h1 className="text-3xl font-bold mb-4 group-hover:text-blue-600 transition-colors">
{post.title}
</h1>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
</div>
</article>
</Link>
)
}
function PostCard({ post }: { post: Post }) {
const coverImage = post.coverImage as Media
const category = post.category as Category
return (
<Link href={`/posts/${post.slug}`} className="group block">
<article>
<div className="relative aspect-video rounded-lg overflow-hidden mb-4">
{coverImage?.url && (
<Image
src={coverImage.url}
alt={coverImage.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
)}
</div>
{category && (
<span
className="inline-block px-2 py-0.5 text-xs font-medium rounded-full text-white mb-2"
style={{ backgroundColor: category.color || '#3B82F6' }}
>
{category.name}
</span>
)}
<h3 className="font-semibold mb-2 group-hover:text-blue-600 transition-colors line-clamp-2">
{post.title}
</h3>
<p className="text-sm text-gray-600 line-clamp-2">{post.excerpt}</p>
</article>
</Link>
)
}صفحة المقال الفردي
// app/(frontend)/posts/[slug]/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { notFound } from 'next/navigation'
import Image from 'next/image'
import type { Metadata } from 'next'
import type { Post, Media, Category } from '@/payload-types'
import { RichText } from '@payloadcms/richtext-lexical/react'
type Params = Promise<{ slug: string }>
export async function generateStaticParams() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
limit: 100,
select: { slug: true },
})
return posts.docs.map((post) => ({ slug: post.slug }))
}
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
const { slug } = await params
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
limit: 1,
})
const post = posts.docs[0]
if (!post) return { title: 'مقال غير موجود' }
const coverImage = post.coverImage as Media
return {
title: post.seo?.metaTitle || post.title,
description: post.seo?.metaDescription || post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: coverImage?.url ? [{ url: coverImage.url }] : [],
},
}
}
export default async function PostPage({ params }: { params: Params }) {
const { slug } = await params
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
where: {
slug: { equals: slug },
status: { equals: 'published' },
},
limit: 1,
depth: 2,
})
const post = posts.docs[0]
if (!post) notFound()
const coverImage = post.coverImage as Media
const category = post.category as Category
return (
<article className="max-w-3xl mx-auto px-4 py-12">
<header className="mb-8">
{category && (
<span
className="inline-block px-3 py-1 text-xs font-medium rounded-full text-white mb-4"
style={{ backgroundColor: category.color || '#3B82F6' }}
>
{category.name}
</span>
)}
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<p className="text-lg text-gray-600 mb-4">{post.excerpt}</p>
</header>
{coverImage?.url && (
<div className="relative aspect-video rounded-xl overflow-hidden mb-12">
<Image src={coverImage.url} alt={coverImage.alt} fill className="object-cover" priority />
</div>
)}
<div className="prose prose-lg max-w-none">
<RichText data={post.content} />
</div>
{post.tags && post.tags.length > 0 && (
<div className="mt-12 pt-8 border-t flex flex-wrap gap-2">
{post.tags.map((item, i) => (
<span key={i} className="px-3 py-1 bg-gray-100 text-sm text-gray-600 rounded-full">
#{item.tag}
</span>
))}
</div>
)}
</article>
)
}الخطوة 7: إضافة مسار API لـ Payload
ينشئ Payload تلقائياً واجهات REST و GraphQL. تحتاج مسار catch-all:
// app/api/[...slug]/route.ts
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import config from '@payload-config'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const PATCH = REST_PATCH(config)
export const DELETE = REST_DELETE(config)هذا يمنحك نقاط نهاية API تلقائية:
GET /api/posts— قائمة جميع المقالاتGET /api/posts/:id— الحصول على مقال واحدPOST /api/posts— إنشاء مقالPATCH /api/posts/:id— تحديث مقالDELETE /api/posts/:id— حذف مقالPOST /api/users/login— المصادقة
الخطوة 8: بذر البيانات الأولية
أنشئ سكريبت بذر لملء نظام إدارة المحتوى ببيانات أولية:
// scripts/seed.ts
import { getPayload } from 'payload'
import config from '@payload-config'
async function seed() {
const payload = await getPayload({ config })
console.log('جارٍ بذر قاعدة البيانات...')
const categories = [
{ name: 'هندسة البرمجيات', slug: 'engineering', color: '#3B82F6', description: 'هندسة وتطوير البرمجيات' },
{ name: 'الذكاء الاصطناعي', slug: 'ai-ml', color: '#8B5CF6', description: 'الذكاء الاصطناعي وتعلم الآلة' },
{ name: 'DevOps', slug: 'devops', color: '#10B981', description: 'البنية التحتية والعمليات' },
{ name: 'التصميم', slug: 'design', color: '#F59E0B', description: 'تصميم واجهة المستخدم وتجربة المستخدم' },
]
for (const category of categories) {
await payload.create({ collection: 'categories', data: category })
console.log(`تم إنشاء التصنيف: ${category.name}`)
}
console.log('اكتمل البذر!')
process.exit(0)
}
seed()شغّله:
npx tsx scripts/seed.tsالخطوة 9: إضافة أنماط التحكم بالوصول
التحكم بالوصول في Payload هو أحد أقوى ميزاته. إليك الأنماط الشائعة:
// lib/access.ts
import type { Access } from 'payload'
// المسؤولون فقط يمكنهم تنفيذ هذا الإجراء
export const isAdmin: Access = ({ req }) => {
return req.user?.role === 'admin'
}
// المسؤولون يرون كل شيء، الآخرون يرون مستنداتهم فقط
export const isAdminOrSelf: Access = ({ req }) => {
if (req.user?.role === 'admin') return true
return { id: { equals: req.user?.id } }
}
// المحتوى المنشور عام، المسودات تتطلب مصادقة
export const publishedOrAuth: Access = ({ req }) => {
if (req.user) return true
return { status: { equals: 'published' } }
}الخطوة 10: إضافة خطافات المجموعات
الخطافات تتيح لك تنفيذ منطق في نقاط مختلفة من دورة حياة المستند:
hooks: {
beforeChange: [
({ data }) => {
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date().toISOString()
}
return data
},
],
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create' && doc.status === 'published') {
try {
await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.REVALIDATION_SECRET,
path: `/posts/${doc.slug}`,
}),
})
} catch (error) {
console.error('فشل إعادة التحقق:', error)
}
}
},
],
}الخطوة 11: إعداد إعادة التحقق عند الطلب
أنشئ مسار API لإعادة التحقق حتى تنعكس تحديثات المحتوى فوراً:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const body = await request.json()
if (body.secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'رمز سري غير صالح' }, { status: 401 })
}
if (body.path) {
revalidatePath(body.path)
return NextResponse.json({ revalidated: true, path: body.path })
}
revalidatePath('/')
return NextResponse.json({ revalidated: true, path: '/' })
}الخطوة 12: النشر في بيئة الإنتاج
الخيار أ: النشر على Vercel
Payload CMS 3 يعمل بسلاسة مع Vercel. ستحتاج قاعدة بيانات PostgreSQL مستضافة:
npm i -g vercel
vercelعيّن متغيرات البيئة في لوحة Vercel:
DATABASE_URI=postgresql://...
PAYLOAD_SECRET=your-production-secret
NEXT_PUBLIC_SITE_URL=https://your-domain.com
REVALIDATION_SECRET=your-revalidation-secret
الخيار ب: النشر مع Docker
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]docker compose up -dاختبار التطبيق
- لوحة الإدارة: زر
/admin، أنشئ مستخدماً، وسجّل الدخول - إنشاء محتوى: أضف تصنيفاً، ارفع صورة، أنشئ مقالاً
- الواجهة الأمامية: زر الصفحة الرئيسية لرؤية مقالاتك
- API: اختبر واجهة REST على
/api/posts - المسودة/النشر: أنشئ مقالاً كمسودة وتحقق من ظهوره فقط عند النشر
- التحكم بالوصول: سجّل الخروج وتحقق من إخفاء المسودات
حل المشاكل الشائعة
"Cannot find module '@payload-config'"
تأكد من وجود المسار في tsconfig.json:
{
"compilerOptions": {
"paths": {
"@payload-config": ["./payload.config.ts"]
}
}
}أخطاء الاتصال بقاعدة البيانات
تحقق من صحة DATABASE_URI وأن خادم PostgreSQL يعمل.
الصور لا تظهر
تأكد من وجود مجلد public/media وأن لديه صلاحيات الكتابة.
أخطاء الأنواع بعد تغيير المخطط أعد توليد الأنواع:
npx payload generate:typesالخطوات التالية
- إضافة بحث باستخدام إضافة البحث المدمجة في Payload أو دمج Algolia
- إضافة i18n مع ميزة التعريب في Payload للمحتوى متعدد اللغات
- إعداد وضع المعاينة للمحررين لمعاينة المسودات على الموقع المباشر
- إضافة مكونات مخصصة للوحة الإدارة
- تنفيذ webhooks لإشعار الخدمات الخارجية عند تغيير المحتوى
الخلاصة
لقد بنيت موقع محتوى متكامل مع Payload CMS 3 يعمل مباشرة داخل Next.js. هذه البنية تمنحك أفضل ما في العالمين: لوحة إدارة قوية وواجهة برمجية بدون تعقيد في النشر — كل شيء يعمل كتطبيق واحد.
النقاط الرئيسية:
- Payload CMS 3 يعيش داخل Next.js — لا حاجة لخلفية منفصلة
- المجموعات تحدد نموذج محتواك مع أنواع TypeScript كاملة
- التحكم بالوصول قائم على الكود — مرن وقابل للاختبار
- الخطافات توفر أتمتة دورة الحياة — لا حاجة لسير عمل خارجي
- استعلامات مباشرة لقاعدة البيانات عبر
getPayload()— لا عبء REST على الخادم - واجهات API تُنشأ تلقائياً للمستهلكين الخارجيين
مزيج Payload + Next.js هو أحد أكثر الأنظمة إنتاجية لبناء تطبيقات ثقيلة المحتوى في 2026.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

بناء روبوت دردشة ذكاء اصطناعي محلي باستخدام Ollama و Next.js: الدليل الشامل
ابنِ روبوت دردشة ذكاء اصطناعي خاص يعمل بالكامل على جهازك المحلي باستخدام Ollama و Next.js. يغطي هذا الدليل العملي التثبيت والبث المباشر واختيار النماذج وبناء واجهة دردشة جاهزة للإنتاج — كل ذلك دون إرسال بياناتك إلى السحابة.

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