שילוב Neobrutalism עם Payload CMS: מדריך מעשי ל-Next.js
ספריות ממשק יכולות להאיץ משמעותית את הפיתוח שלכם. במאמר הזה נחבר את Payload CMS כ-backend תוכן ואת Neobrutalism כערכת רכיבי הפרונט-אנד, ונשחרר רשימת בלוג נאו-ברוטליסטית ב-Next.js.
Dov Azencot
@DovAzencotמדריך מעשי, מקצה לקצה, לחיבור Payload CMS כ-backend התוכן שלכם ו-Neobrutalism כערכת רכיבי הפרונט-אנד - ואז לשחרר רשימת בלוג נאו-ברוטליסטית ונקייה ב-Next.js.
עובד מצוין אם ה-stack שלכם הוא Next.js + TailwindCSS. Payload היא Next-native ו-Neobrutalism היא Tailwind-first, כך שהן משתלבות יפה.
מה תבנו
- אפליקציית Payload CMS עם אוסף Posts
- פרונט-אנד ב-Next.js שמושך פוסטים מ-REST API של Payload
- עמוד
/blogשמרנדר רשימה רספונסיבית באמצעות רכיבי Neobrutalism (Cards, Badges, Buttons)
דרישות מקדימות
- Node.js 20.9+ (Payload דורשת Node 20.9 ומעלה)
- Next.js 15+ לפרונט-אנד (Payload היא Next-native)
- כל בסיס נתונים תואם: SQLite (פיתוח), Postgres או MongoDB
טיפ: לפיתוח מקומי, SQLite הוא הנתיב המהיר ביותר. עברו ל-Postgres/Mongo בעת הפריסה.
חלק 1 — הקמת Payload CMS
אפשר להוסיף את Payload לאפליקציית Next.js קיימת או ליצור אחת חדשה. נאתחל פרויקט Payload ייעודי כדי שה-API שלו יישאר נקי ונייד.
1) יצירת אפליקציית Payload חדשה
זה מפעיל את Payload ואת ה-Admin UI שלו (בדרך כלל בכתובת 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) זריעת כמה פוסטים
פתחו את ה-Admin UI ← 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 של Payload ומרנדרת רשימת בלוג באמצעות רכיבי Neobrutalism.
1) יצירת אפליקציית Next.js
2) התקנת TailwindCSS (אם לא בחרתם בתבנית Tailwind)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pהוסיפו את ./components/**/*.{ts,tsx} ואת ./app/**/*.{ts,tsx} לתוכן ב-tailwind.config.ts. כללו גם את הנתיבים של Neobrutalism אם המתקין דורש זאת.
3) התקנת Neobrutalism
השתמשו במתקין הרשמי (CLI) או בהתקנה ידנית. דוגמה (pnpm):
pnpm add retrouiלאחר מכן ייבאו רכיבים היכן שצריך. (אם 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>
);
}עבור rich text, השתמשו ב-Renderer המועדף עליכם של 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) כדי שכתובות התמונות ייפתרו כראוי. - הגנו על טיוטות: למעלה אפשרנו
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 שמושך עם כותרות draft-auth.
- 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; ושחררו לאוויר.