14 November 2025
Mengintegrasikan Neobrutalism dengan Payload CMS
Pustaka UI dapat mempercepat pengembangan Anda secara signifikan. Dalam artikel ini kami mengulas 5 yang terbaik untuk proyek React Anda berikutnya.
Dov Azencot
@DovAzencotPanduan praktis dari awal hingga akhir untuk menghubungkan Payload CMS sebagai backend konten dan Neobrutalism sebagai kit komponen front-end, lalu merilis sebuah daftar artikel neobrutalis yang rapi di Next.js.
Hasilnya sangat baik jika stack Anda adalah Next.js + TailwindCSS. Payload nativ untuk Next dan Neobrutalism mengutamakan Tailwind, sehingga keduanya menyatu dengan mulus.
Yang akan Anda bangun
- Aplikasi Payload CMS dengan koleksi Posts
- Front-end Next.js yang mengambil artikel dari REST API Payload
- Halaman
/blogyang merender daftar responsif memakai komponen Neobrutalism (Cards, Badges, Buttons)
Prasyarat
- Node.js 20.9+ (Payload mensyaratkan Node 20.9 atau yang lebih baru)
- Next.js 15+ untuk front-end (Payload nativ untuk Next)
- Basis data kompatibel apa pun: SQLite (dev), Postgres, atau MongoDB
Kiat: untuk pengembangan lokal, SQLite adalah jalur tercepat. Beralihlah ke Postgres/Mongo saat men-deploy.
Bagian 1 — Menyiapkan Payload CMS
Anda bisa menambahkan Payload ke aplikasi Next.js yang sudah ada atau membuat yang baru. Di sini kita akan membuat proyek Payload tersendiri agar API-nya tetap rapi dan portabel.
1) Membuat aplikasi Payload baru
Ini menjalankan Payload dan Admin UI-nya (biasanya di http://localhost:3000/admin).
2) Mendefinisikan koleksi Posts
Buat 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;Tambahkan ke konfigurasi utama Anda 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
],
});Jika Anda belum punya koleksi Media atau Users, jalankan
create-payload-appdengan template blog atau tambahkan koleksi sederhana untuk unggahan dan autentikasi nanti.
3) Mengisi beberapa artikel
Buka Admin UI → Posts → Create New dan tambahkan beberapa artikel yang sudah terbit dengan tanggal publishedAt yang telah lampau.
4) Memastikan REST API berfungsi
Payload secara bawaan mengekspos REST API di /api/<collection>.
Kunjungi:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Anda semestinya melihat JSON berisi artikel-artikel Anda.
Saat men-deploy, atur
PAYLOAD_PUBLIC_SERVER_URLdan variabel lingkungan basis data Anda. Jika Anda menghosting Payload terpisah dari Next.js, aktifkan CORS untuk origin front-end Anda.
Bagian 2 — Menyiapkan front-end Next.js + Neobrutalism
Kita akan membangun aplikasi Next.js yang mengonsumsi REST API Payload dan merender daftar artikel memakai komponen Neobrutalism.
1) Membuat aplikasi Next.js
2) Memasang TailwindCSS (jika Anda tidak memilih template Tailwind)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pTambahkan ./components/**/*.{ts,tsx} dan ./app/**/*.{ts,tsx} ke bagian content tailwind.config.ts. Sertakan pula path Neobrutalism jika pemasang memerlukannya.
3) Memasang Neobrutalism
Gunakan pemasang resmi (CLI) atau pemasangan manual. Contoh (pnpm):
pnpm add retrouiLalu impor komponen di tempat yang Anda perlukan. (Jika Neobrutalism menyertakan CLI untuk menyalin komponen, jalankan di sini dan ikuti petunjuknya.)
4) Menghubungkan ke Payload (env + helper fetch)
Buat .env.local:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Tambahkan utilitas fetch kecil 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) Membangun halaman daftar artikel dengan Neobrutalism
Buat 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>
);
}Jika paket Neobrutalism Anda mengekspos nama atau path impor yang berbeda, sesuaikan baris
import { Card } from 'neobrutalism/card'agar cocok. Anda juga bisa menggantinya dengan komponen Neobrutalism lain mana pun (Tabs, Inputs, dsb.).
6) Halaman detail artikel (opsional)
Buat 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>
);
}Untuk rich text, gunakan renderer Payload pilihan Anda (misalnya @payloadcms/richtext-lexical) dan render sesuai kebutuhan.
Bagian 3 — Catatan cross-origin & deployment
Jika aplikasi Next.js Anda berjalan pada origin yang berbeda dari Payload:
- Aktifkan CORS di Payload: atur
cors: ["https://your-next-app.com", "http://localhost:3000"]dipayload.config.ts. - Atur
serverURL(danPAYLOAD_PUBLIC_SERVER_URL) dengan benar agar URL gambar teratasi. - Lindungi draf: di atas kita mengizinkan
readpublik. Untuk konten privat, ganti dengan fungsiaccess.readyang memeriksa autentikasi/peran.
Opsi deployment:
- Self-host (aplikasi Node di VPS) atau deploy ke Vercel/Cloudflare untuk DX yang modern.
- Gunakan SQLite untuk demo cepat, Postgres/Mongo untuk produksi.
Bagian 4 — Peningkatan
- Pencarian & filter: gunakan parameter kueri Payload (misalnya
where[title][like]) dan hubungkan Input + Tabs Neobrutalism untuk memfilter. - Paginasi: REST API mengembalikan
totalDocs,limit,page; bangun "Muat lebih banyak" dengan Button Neobrutalism. - Gambar: pindahkan
coverImageke koleksi Media tersendiri dan gunakan adapter unggahan Payload. - Mode pratinjau: siapkan rute pratinjau draf di Next.js yang mengambil data dengan header autentikasi draf.
- GraphQL: Payload juga mengekspos GraphQL; gunakan jika Anda lebih suka kueri bertipe.
Pemecahan masalah
- Galat CORS: periksa konfigurasi
corsdan pastikanserverURLtelah diatur. - Gambar tidak muncul: konfirmasi
urlyang dikembalikan untuk unggahan dan bahwanext.config.jsNext.js mengizinkan domain Payload di bawahimages.domains. - Tidak ada yang terender: periksa URL fetch di
lib/payload.ts. Pastikan record ada danstatus-nyapublished.
Ikhtisar
Kini Anda memiliki:
- Aplikasi Payload CMS yang mengekspos artikel via REST
- Situs Next.js dengan gaya Neobrutalism
- Halaman
/blogyang mendaftar dan menautkan ke tiap artikel
Dari sini, tambahkan penulis, kategori, dan tag; beri gaya dengan lebih banyak komponen Neobrutalism; lalu rilis.