14 نوامبر 2025
یکپارچهسازی Neobrutalism با Payload CMS
کتابخانههای UI میتوانند توسعهٔ شما را بهطور چشمگیری سریعتر کنند. در این مقاله 5 مورد برتر را برای پروژهٔ React بعدی شما مرور میکنیم.
Dov Azencot
@DovAzencotیک راهنمای عملی و سرتاسری برای وصلکردن Payload CMS بهعنوان بکاند محتوا و Neobrutalism بهعنوان کیت مؤلفهٔ فرانتاند، و سپس عرضهٔ یک فهرست بلاگ نئوبروتالیستی و تمیز در Next.js.
اگر استک شما بر Next.js + TailwindCSS استوار باشد، نتیجه عالی است. Payload برای Next بومی است و Neobrutalism با اولویت Tailwind ساخته شده، پس این دو بیدرز در هم جفت میشوند.
چه خواهید ساخت
- یک اپ Payload CMS با یک کالکشن Posts
- یک فرانتاند Next.js که مقالهها را از REST API پیلود میگیرد
- یک صفحهٔ
/blogکه با مؤلفههای Neobrutalism (Cards، Badges، Buttons) یک فهرست واکنشگرا رندر میکند
پیشنیازها
- Node.js 20.9+ (Payload به Node 20.9 یا جدیدتر نیاز دارد)
- Next.js 15+ برای فرانتاند (Payload برای Next بومی است)
- هر پایگاهدادهٔ سازگار: SQLite (توسعه)، Postgres یا MongoDB
نکته: برای توسعهٔ محلی، SQLite سریعترین راه است. هنگام استقرار به Postgres/Mongo بروید.
بخش 1 — راهاندازی Payload CMS
میتوانید Payload را به یک اپ Next.js موجود بیفزایید یا یک اپ تازه بسازید. اینجا یک پروژهٔ اختصاصی Payload میسازیم تا API آن تمیز و قابلحمل بماند.
1) ساختن یک اپ Payload تازه
این Payload و رابط مدیریت آن را راه میاندازد (معمولاً در http://localhost:3000/admin).
2) تعریف یک کالکشن Posts
src/collections/Posts.ts را بسازید:
import { CollectionConfig } from 'payload/types';
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'publishedAt', 'status'],
},
access: {
read: () => true, // public read access for blog
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
},
{
name: 'excerpt',
type: 'textarea',
},
{
name: 'coverImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
required: true,
},
{
name: 'publishedAt',
type: 'date',
admin: { position: 'sidebar' },
},
{
name: 'content',
type: 'richText',
},
],
};
export default Posts;آن را به پیکربندی اصلی خود src/payload.config.ts بیفزایید:
import { buildConfig } from 'payload/config';
import Posts from './collections/Posts';
export default buildConfig({
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
admin: { user: 'users' },
collections: [
Posts,
// Media and Users collections if you need them
],
});اگر هنوز کالکشن Media یا Users ندارید،
create-payload-appرا با یک قالب بلاگ اجرا کنید یا بعداً کالکشنهای ساده برای آپلود و احراز هویت بیفزایید.
3) افزودن چند مقاله
رابط مدیریت → Posts → Create New را باز کنید و چند مقالهٔ منتشرشده با تاریخهای publishedAt گذشته بیفزایید.
4) اطمینان از کارکرد REST API
Payload بهطور پیشفرض یک REST API در /api/<collection> عرضه میکند.
به این نشانی بروید:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
باید یک JSON با مقالههای خود ببینید.
هنگام استقرار،
PAYLOAD_PUBLIC_SERVER_URLو متغیرهای محیطی پایگاهدادهٔ خود را تنظیم کنید. اگر Payload را جدا از Next.js میزبانی میکنید، CORS را برای مبدأ فرانتاند خود فعال کنید.
بخش 2 — راهاندازی فرانتاند Next.js + Neobrutalism
یک اپ Next.js میسازیم که REST API پیلود را مصرف میکند و با مؤلفههای Neobrutalism یک فهرست بلاگ رندر میکند.
1) ساختن اپ Next.js
2) نصب TailwindCSS (اگر قالب Tailwind را انتخاب نکردید)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p./components/**/*.{ts,tsx} و ./app/**/*.{ts,tsx} را به فیلد content فایل tailwind.config.ts بیفزایید. مسیرهای Neobrutalism را نیز اگر نصبکننده میخواهد، بگنجانید.
3) نصب Neobrutalism
از نصبکنندهٔ رسمی (CLI) یا نصب دستی استفاده کنید. مثال (pnpm):
pnpm add retrouiسپس مؤلفهها را هرجا که نیاز دارید import کنید. (اگر Neobrutalism یک CLI برای کپی مؤلفهها دارد، آن را اینجا اجرا و راهنماها را دنبال کنید.)
4) اتصال به Payload (env + یاور fetch)
.env.local را بسازید:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
یک ابزار کوچک fetch به نام lib/payload.ts بیفزایید:
export type Post = {
id: string;
title: string;
slug: string;
excerpt?: string;
coverImage?: { url: string; filename: string } | string | null;
status: 'draft' | 'published';
publishedAt?: string | null;
};
export async function getPublishedPosts(limit = 12) {
const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL;
const url = new URL('/api/posts', base);
url.searchParams.set('limit', String(limit));
url.searchParams.set('sort', '-publishedAt');
url.searchParams.set('where[status][equals]', 'published');
const res = await fetch(url.toString(), { next: { revalidate: 60 } });
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`);
const data = await res.json();
return data.docs as Post[];
}5) ساختن صفحهٔ فهرست بلاگ با Neobrutalism
app/blog/page.tsx را بسازید (App Router):
import Image from 'next/image';
import Link from 'next/link';
import { getPublishedPosts, type Post } from '@/lib/payload';
// Example Neobrutalism components — adjust paths/names per your install
import { Card } from 'neobrutalism/card';
import { Button } from 'neobrutalism/button';
import { Badge } from 'neobrutalism/badge';
export const revalidate = 60; // ISR cadence
export default async function BlogListPage() {
const posts = await getPublishedPosts(12);
return (
<main className="container mx-auto px-4 py-10">
<h1 className="text-4xl font-extrabold mb-6">Blog</h1>
<p className="text-muted-foreground mb-10">
Latest posts from the Payload CMS backend, styled with Neobrutalism.
</p>
<section className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Card key={post.id} className="p-0 overflow-hidden">
{typeof post.coverImage === 'object' && post.coverImage?.url ? (
<div className="relative h-44 w-full">
<Image
src={post.coverImage.url}
alt={post.title}
fill
className="object-cover"
/>
</div>
) : null}
<div className="p-5 space-y-3">
<div className="flex items-center gap-2">
<Badge variant="outline">Article</Badge>
{post.publishedAt ? (
<span className="text-xs opacity-70">
{new Date(post.publishedAt).toLocaleDateString()}
</span>
) : null}
</div>
<h2 className="text-2xl font-bold leading-tight">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
{post.excerpt ? (
<p className="text-sm text-muted-foreground line-clamp-3">
{post.excerpt}
</p>
) : null}
<div className="pt-2">
<Button asChild>
<Link href={`/blog/${post.slug}`}>Read more</Link>
</Button>
</div>
</div>
</Card>
))}
</section>
</main>
);
}اگر بستهٔ Neobrutalism شما نامها یا مسیرهای import متفاوتی عرضه میکند، خطوط
import { Card } from 'neobrutalism/card'را برای هماهنگی تنظیم کنید. میتوانید آنها را با هر مؤلفهٔ دیگر Neobrutalism هم جایگزین کنید (Tabs، Inputs و غیره).
6) صفحهٔ جزئیات مقاله (اختیاری)
app/blog/[slug]/page.tsx را بسازید:
import Image from 'next/image';
import Link from 'next/link';
async function getPost(slug: string) {
const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL!;
const url = new URL('/api/posts', base);
url.searchParams.set('limit', '1');
url.searchParams.set('where[slug][equals]', slug);
const res = await fetch(url.toString(), { next: { revalidate: 60 } });
if (!res.ok) throw new Error('Failed to load post');
const data = await res.json();
return data.docs[0];
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) return <div className="p-10">Not found</div>;
return (
<article className="container mx-auto px-4 py-10 max-w-3xl">
<Link href="/blog" className="underline">← Back to blog</Link>
<h1 className="text-4xl font-extrabold mt-3">{post.title}</h1>
{post.coverImage?.url ? (
<div className="relative h-80 w-full my-6">
<Image src={post.coverImage.url} alt={post.title} fill className="object-cover" />
</div>
) : null}
{post.content?.root ? (
// If using Payload Lexical richText renderer, render here
<div className="prose prose-neutral dark:prose-invert">
{/* Render your rich text */}
</div>
) : (
post.excerpt ? <p className="mt-4 text-lg">{post.excerpt}</p> : null
)}
</article>
);
}برای متن غنی، از رندرگر دلخواه Payload خود (مثلاً @payloadcms/richtext-lexical) استفاده و بر همان اساس رندر کنید.
بخش 3 — یادداشتهای cross-origin و استقرار
اگر اپ Next.js شما روی مبدأیی متفاوت از Payload اجرا میشود:
- CORS را در Payload فعال کنید:
cors: ["https://your-next-app.com", "http://localhost:3000"]را درpayload.config.tsتنظیم کنید. serverURL(وPAYLOAD_PUBLIC_SERVER_URL) را درست تنظیم کنید تا نشانیهای تصویر resolve شوند.- پیشنویسها را محافظت کنید: بالاتر
readعمومی را مجاز کردیم. برای محتوای خصوصی، آن را با یک تابعaccess.readجایگزین کنید که احراز هویت/نقشها را بررسی کند.
گزینههای استقرار:
- میزبانی خودی (اپ Node روی یک VPS) یا استقرار روی Vercel/Cloudflare برای یک DX مدرن.
- برای دموهای سریع از SQLite و برای تولید از Postgres/Mongo استفاده کنید.
بخش 4 — بهبودها
- جستوجو و فیلترها: از پارامترهای پرسوجوی Payload (مثلاً
where[title][like]) استفاده و یک Input + Tabs از Neobrutalism را برای فیلترکردن وصل کنید. - صفحهبندی: REST API مقادیر
totalDocs،limit،pageرا برمیگرداند؛ با یک Button از Neobrutalism یک «بارگذاری بیشتر» بسازید. - تصاویر:
coverImageرا به یک کالکشن اختصاصی Media منتقل و از آداپتور آپلود Payload استفاده کنید. - حالت پیشنمایش: در Next.js یک مسیر پیشنمایش پیشنویس بسازید که دادهها را با هدرهای احراز هویت پیشنویس بگیرد.
- GraphQL: Payload همچنین GraphQL عرضه میکند؛ اگر پرسوجوهای نوعدار را ترجیح میدهید، از آن استفاده کنید.
عیبیابی
- خطاهای CORS: پیکربندی
corsرا بررسی و مطمئن شویدserverURLتنظیم شده است. - نمایشنشدن تصاویر:
urlبازگرداندهشده برای آپلودها را تأیید و مطمئن شویدnext.config.jsدر Next.js دامنهٔ Payload را زیرimages.domainsمجاز میکند. - رندرنشدن هیچچیز: نشانی fetch را در
lib/payload.tsبازرسی کنید. مطمئن شوید رکوردها وجود دارند وstatusبرابرpublishedاست.
جمعبندی
اکنون اینها را دارید:
- یک اپ Payload CMS که مقالهها را از راه REST عرضه میکند
- یک سایت Next.js با سبک Neobrutalism
- یک صفحهٔ
/blogکه مقالهها را فهرست و به هر یک لینک میدهد
از اینجا، نویسندگان، دستهها و برچسبها بیفزایید؛ با مؤلفههای بیشتر Neobrutalism سبک بدهید؛ و آن را عرضه کنید.