پرش به محتوا

یکپارچه‌سازی Neobrutalism با Payload CMS

کتابخانه‌های UI می‌توانند توسعهٔ شما را به‌طور چشمگیری سریع‌تر کنند. در این مقاله 5 مورد برتر را برای پروژهٔ React بعدی شما مرور می‌کنیم.

یک راهنمای عملی و سرتاسری برای وصل‌کردن 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 تازه

pnpm create payload-app
# Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc.
cd <your-payload-app>
pnpm dev  # or npm run dev / yarn dev
npx create-payload-app
# Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc.
cd <your-payload-app>
pnpm dev  # or npm run dev / yarn dev
yarn create payload-app
# Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc.
cd <your-payload-app>
pnpm dev  # or npm run dev / yarn dev
bunx --bun create-payload-app
# Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc.
cd <your-payload-app>
pnpm dev  # or npm run dev / yarn dev

این 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) افزودن چند مقاله

رابط مدیریت → PostsCreate 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

pnpm create next-app@latest retroui-payload-blog
cd retroui-payload-blog
npx create-next-app@latest retroui-payload-blog
cd retroui-payload-blog
yarn create next-app@latest retroui-payload-blog
cd retroui-payload-blog
bunx --bun create-next-app@latest retroui-payload-blog
cd retroui-payload-blog

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 تنظیم کنید.
  • serverURLPAYLOAD_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 سبک بدهید؛ و آن را عرضه کنید.


→ بازگشت به مطالب