در حال آمادهسازی
تجربه واقعی من از انتخاب Payload CMS 3 برای پروژههایی که به یک CMS مدرن، TypeScript، PostgreSQL و معماری یکپارچه مبتنی بر Next.js App Router نیاز دارند؛ از مزایای Local API و Code-First Development گرفته تا چالشهای Cache، Migration و مدیریت زیرساخت.

یک طرف ماجرا CMSهای سنتی مثل WordPress هستند؛ ابزارهایی که برای خیلی از پروژهها هنوز کاملاً منطقیاند، اما وقتی بخواهی معماری Headless، TypeScript، کنترل بیشتر روی API و یک تجربه توسعه مدرن داشته باشی، ممکن است کمکم احساس کنی داری با محدودیتهای معماری آنها دستوپنجه نرم میکنی.
طرف دیگر، CMSهای Headless ابری مثل Sanity یا Contentful قرار دارند. شروع کار با آنها معمولاً سریع و راحت است، اما در عوض بخشی از زیرساخت و دادههایت خارج از کنترل مستقیم خودت قرار میگیرد و با رشد پروژه، هزینهها و محدودیتهای پلن هم میتوانند به یک فاکتور مهم تبدیل شوند.
برای پروژههای اخیرم، بعد از بررسی گزینههای مختلف، در نهایت به سراغ Payload CMS 3 رفتم؛ نه صرفاً به این دلیل که یک CMS جدید و جذاب است، بلکه به خاطر معماریای که ارائه میدهد.
Payload 3 اساساً بر پایه Next.js و App Router ساخته شده است. یعنی من قرار نیست یک CMS را جداگانه کنار یک پروژه Next.js قرار بدهم و بعد تلاش کنم این دو را به هم متصل کنم. پنل مدیریت، API، سیستم احراز هویت، منطق بکاند و بخش فرانتاند میتوانند در یک اپلیکیشن Next.js و یک Runtime واحد کنار هم زندگی کنند.
در این مقاله میخواهم از تجربه خودم بگویم؛ اینکه چرا Payload را انتخاب کردم، چه چیزهایی در عمل برایم جذاب بود و کجاها، مخصوصاً در بحث کش Next.js و مدیریت دیتابیس، چندین ساعت وقتم صرف دیباگ و پیدا کردن راهحل شد.
اولین سؤال منطقی این است:
وقتی ابزارهایی مثل Strapi و Directus وجود دارند، چرا Payload؟
برای من چند دلیل اصلی وجود داشت.
یکی از مهمترین دلایلی که Payload 3 برایم جذاب شد، معماری یکپارچه آن بود.
در بسیاری از پروژهها، معمولاً با چنین ساختاری روبهرو هستیم:
اما Payload 3 مسیر متفاوتی را انتخاب کرده است.
چون Payload مستقیماً با Next.js کار میکند، بخشهای مختلف اپلیکیشن میتوانند در یک پروژه کنار هم قرار بگیرند. Admin Panel، Route Handlerها، Server Componentها و منطق CMS همگی بخشی از همان اپلیکیشن هستند.
برای من این موضوع فقط به معنی «کم شدن تعداد فولدرها» نبود.
سادگی در Deployment، مدیریت Environmentها، اشتراک Typeها و نگهداری پروژه، همگی با یکپارچه شدن معماری سادهتر شدند.
البته این مدل برای همه پروژهها بهترین انتخاب نیست. اگر CMS و Front-end قرار است کاملاً مستقل باشند یا چندین اپلیکیشن مختلف قرار است از یک CMS مشترک استفاده کنند، معماری جدا ممکن است منطقیتر باشد.
اما برای پروژههایی که Front-end و CMS بخشی از یک محصول هستند، این یکپارچگی برای من یک مزیت جدی بود.
من شخصاً ترجیح میدهم ساختار دیتابیس و مدلهای داده را داخل کد تعریف کنم، نه اینکه برای ساخت هر Collection وارد یک پنل شوم، روی چند دکمه کلیک کنم و در نهایت امیدوار باشم ساختار پروژه در محیطهای مختلف یکسان باقی بماند.
در Payload، تعریف ساختار داده کاملاً Code-First است.
مثلاً یک Collection ساده برای مقاله میتواند چیزی شبیه این باشد:
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
},
{
name: 'content',
type: 'richText',
},
],
}
این مدل برای من چند مزیت مهم داشت.
اول اینکه ساختار داده بخشی از Repository است.
یعنی تغییرات Schema همراه با کد Version Control میشوند، قابل Review هستند و دقیقاً مشخص است که چه تغییری در چه زمانی انجام شده است.
دوم اینکه TypeScript واقعاً به بخشی از جریان توسعه تبدیل میشود.
وقتی Schema تغییر میکند، Typeهای Payload هم میتوانند بر اساس ساختار پروژه تولید و بهروزرسانی شوند. در نتیجه هنگام کار با دادهها، هم در Server Component و هم در سایر بخشهای اپلیکیشن، تجربه Type Safety بسیار بهتری داری.
برای کسی مثل من که بیشتر زمانش را با React و TypeScript کار میکند، این مدل خیلی طبیعیتر از ساختن مدل داده از طریق یک UI بود.
یکی از ویژگیهایی که خیلی سریع متوجه ارزشش شدم، Local API بود.
فرض کنید داخل یک Server Component هستید و میخواهید یک مقاله را از CMS دریافت کنید.
در بسیاری از معماریها ممکن است چنین کاری انجام دهید:
await fetch('https://api.example.com/posts/my-post')
اما وقتی Payload داخل همان اپلیکیشن در حال اجراست، لازم نیست برای صحبت کردن با CMS یک HTTP Request داخلی ایجاد کنید.
میتوانید مستقیماً از Local API استفاده کنید:
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
where: {
slug: {
equals: slug,
},
},
})
const post = posts.docs[0]
// Render post...
}
نکته مهم اینجاست که Payload در این حالت یک API Server خارجی نیست که مجبور باشید برای هر درخواست از طریق شبکه با آن ارتباط برقرار کنید.
کد مستقیماً در محیط سرور اجرا میشود و از API داخلی Payload برای کار با دادهها استفاده میکند.
این موضوع به معنی «معجزه در Performance» نیست و همچنان باید به Queryها، Indexها و نحوه مدیریت Database توجه کنید، اما از نظر معماری، حذف یک درخواست HTTP داخلی غیرضروری، اشتراک Typeها و سادهتر شدن جریان داده برای من یک مزیت مهم بود.
یکی از بخشهایی که بیشترین زمان را از من گرفت، کش بود.
وقتی با Next.js App Router کار میکنید، نمیتوانید صرفاً با ذهنیت سنتی SSR به پروژه نگاه کنید.
باید دقیقاً بدانید:
فرض کنید یک مقاله را از طریق Payload دریافت میکنیم.
در نگاه اول همهچیز ساده است: مقاله را Query میکنیم و Render میکنیم.
اما اگر برای بهینهسازی، نتیجه Query را Cache کرده باشید، بعد از اینکه Editor مقاله را تغییر داد، باید یک مکانیزم مشخص برای Invalidating یا Revalidating آن داده داشته باشید.
اینجا یکی از چیزهایی که برایم اهمیت پیدا کرد، استفاده از Cache Tagها بود.
برای مثال میتوانیم Query مربوط به یک مقاله را با یک Tag مشخص Cache کنیم:
import { unstable_cache } from 'next/cache'
import { getPayload } from 'payload'
import config from '@payload-config'
export function getPost(slug: string) {
return unstable_cache(
async () => {
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'posts',
where: {
slug: {
equals: slug,
},
},
limit: 1,
})
return result.docs[0] ?? null
},
['post', slug],
{
tags: [`post:${slug}`],
},
)()
}
حالا وقتی مقاله تغییر میکند، میتوانیم از Hookهای Payload برای Revalidate کردن داده مرتبط استفاده کنیم.
بهصورت مفهومی:
import type { CollectionConfig } from 'payload'
import { revalidateTag } from 'next/cache'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
// Fields...
],
hooks: {
afterChange: [
({ doc }) => {
revalidateTag(`post:${doc.slug}`)
revalidateTag('posts')
return doc
},
],
afterDelete: [
({ doc }) => {
revalidateTag(`post:${doc.slug}`)
revalidateTag('posts')
},
],
},
}
ایده اصلی ساده است:
داده را Cache کن، اما یک راه مشخص برای باطل کردن همان Cache داشته باش.
این باعث میشود مجبور نباشید بین دو حالت افراطی یکی را انتخاب کنید:
برای من، پیدا کردن تعادل بین Performance و Fresh بودن دادهها یکی از مهمترین بخشهای کار با این معماری بود.
و واقعاً اگر بخواهم صادق باشم، این بخش جایی بود که چندین بار مجبور شدم برگردم و مدل Cache پروژه را از اول بررسی کنم.
در پروژههای کوچک، ممکن است تغییر دادن Schema دیتابیس چندان ترسناک نباشد.
یک فیلد اضافه میکنید، دیتابیس را Reset میکنید و همهچیز دوباره بالا میآید.
اما وقتی داده واقعی دارید، این روش دیگر جواب نمیدهد.
اگر یک فیلد اضافه، حذف یا تغییر نام داده شود، باید بدانید دقیقاً چه اتفاقی برای دیتابیس Production میافتد.
به همین دلیل Migration برای من بخش جدانشدنی Workflow شد.
در محیط Local معمولاً PostgreSQL را با Docker اجرا میکنم:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: dev_user
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: my_project_db
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
بعد از تغییر ساختار Collectionها، Migration مربوط به تغییرات را ایجاد و قبل از Deployment بررسی میکنم.
یک Workflow ساده میتواند چیزی شبیه این باشد:
pnpm payload migrate:create
و سپس:
pnpm payload migrate
مهمترین نکته برای من این نبود که Migration چه دستوری دارد.
مهم این بود که تغییرات Database دیگر نباید یک اتفاق تصادفی باشند.
Schema پروژه باید قابل Version Control باشد و Production باید بتواند از یک نسخه مشخص از Database به نسخه جدید منتقل شود، بدون اینکه دادهها قربانی یک تغییر ساده در کد شوند.
به نظر من، وقتی پروژه از مرحله Prototype عبور میکند، نداشتن یک Migration Workflow مشخص دیر یا زود تبدیل به دردسر میشود.
یکی دیگر از دلایلی که Payload برایم جذاب بود، سیستم Authentication داخلی آن بود.
بهجای اینکه برای یک پروژه ساده تا متوسط از همان ابتدا چند سیستم مختلف را کنار هم قرار بدهم، Payload خودش قابلیتهایی مثل:
را در اختیار قرار میدهد.
مثلاً اگر بخواهید در یک Server Component کاربر فعلی را بررسی کنید، میتوانید از اطلاعات Request برای احراز هویت استفاده کنید:
import { headers } from 'next/headers'
import { getPayload } from 'payload'
import config from '@payload-config'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const payload = await getPayload({
config,
})
const { user } = await payload.auth({
headers: await headers(),
})
if (!user) {
redirect('/login')
}
return (
<div>
<h1>خوش اومدی، {user.email}</h1>
</div>
)
}
اما چیزی که برای من جذابتر بود، Access Control بود.
شما میتوانید مشخص کنید چه کسی:
و حتی در سطح فیلدها هم محدودیت ایجاد کنید.
برای مثال:
{
name: 'isPublished',
type: 'checkbox',
access: {
update: ({ req }) => {
return req.user?.role === 'admin'
},
},
}
این یعنی منطق دسترسی بخشی از تعریف خود مدل داده است.
به جای اینکه همهچیز را در چند Middleware پراکنده یا داخل Controllerهای مختلف پخش کنید، بخش قابل توجهی از قوانین دسترسی میتواند نزدیک به خود Resource تعریف شود.
این مقاله قرار نیست تبلیغ Payload باشد.
واقعیت این است که این معماری هم هزینههای خودش را دارد.
وقتی CMS، Admin Panel و Front-end همگی بخشی از یک اپلیکیشن هستند، طبیعتاً پروژه نسبت به یک سایت ساده Next.js پیچیدهتر میشود.
باندلها بزرگتر میشوند، Dependencyهای بیشتری وارد پروژه میشوند و Build Time میتواند افزایش پیدا کند.
برای پروژه کوچک، ممکن است این همه قدرت واقعاً اضافه باشد.
اگر یک سایت کاملاً Static دارید، میتوانید آن را روی بسیاری از سرویسهای ساده Deploy کنید.
اما وقتی PostgreSQL، Node.js و Payload وارد ماجرا میشوند، مسئولیت بیشتری هم دارید.
باید به مواردی مثل این فکر کنید:
این لزوماً یک ایراد نیست.
اما اگر هدف شما ساخت یک وبسایت ساده است، شاید واقعاً نیازی به این سطح از زیرساخت نداشته باشید.
Payload 3 یک اکوسیستم مدرن دارد و معمولاً بخشهایی مثل:
باید با نسخههای سازگار کنار هم قرار بگیرند.
به همین دلیل، من شخصاً ترجیح میدهم Dependencyها را بدون دلیل و صرفاً به خاطر دیدن یک نسخه جدید آپدیت نکنم.
در چنین پروژهای، یک Update ساده گاهی میتواند روی Build، Typeها یا رفتار یک Adapter تأثیر بگذارد.
قطعاً نه.
اگر یک Landing Page ساده دارید یا فقط چند مقاله Markdown قرار است نمایش دهید، به نظر من رفتن سراغ Payload احتمالاً Overengineering است.
گاهی بهترین CMS همان فایل Markdown است.
اما Payload برای پروژههایی که:
میتواند انتخاب بسیار جذابی باشد.
دلیل اصلی انتخاب من Payload CMS 3 این نبود که بخواهم یک CMS را با Next.js ترکیب کنم.
اتفاقاً یکی از مهمترین دلایل انتخابم این بود که Payload 3 خودش بر پایه Next.js و App Router ساخته شده است.
این یعنی بهجای اینکه چند سرویس مختلف را به هم متصل کنم، میتوانم در بسیاری از پروژههایم یک معماری یکپارچه داشته باشم که CMS، Admin Panel، Authentication و Front-end را در کنار هم مدیریت میکند.
در کنار PostgreSQL، TypeScript و یک Migration Workflow مشخص، این ساختار برای نوع پروژههایی که من میسازم بسیار منطقیتر از چیزی بود که در ابتدا انتظار داشتم.
البته این مسیر بدون دردسر نبود.
بیشترین چالش من، درک درست Cache در Next.js و پیدا کردن یک استراتژی قابل اعتماد برای Revalidation دادهها بود. بعد از آن هم مدیریت Migrationها و مراقبت از سازگاری Dependencyها.
اما در نهایت، چیزی که از Payload گرفتم فقط یک CMS نبود.
برای من بیشتر شبیه یک بخش یکپارچه از معماری اپلیکیشن بود.
و دقیقاً همین موضوع، مهمترین دلیل من برای انتخاب آن است.

۵ تکنیک کاربردی و حرفهای برای افزایش کیفیت کدهای جاوااسکریپت، ریاکت و تایپاسکریپت؛ از شکستن صحیح کامپوننتها تا حذف کدهای تکراری و مدیریت تمیز خطاها.

بررسی انواع استیت در فرانتاند (Local، Shared، Server Cache و Global)، چرایی عبور از دردسرهای Prop Drilling و Context، و نحوه استفاده کاربردی از Zustand.