14 november 2025
Neobrutalism integreren met Payload CMS
UI-bibliotheken kunnen je ontwikkeling flink versnellen. In dit artikel nemen we de 5 beste door voor je volgende React-project.
Dov Azencot
@DovAzencotEen praktische, end-to-end handleiding om Payload CMS als content-backend en Neobrutalism als frontend-componentenkit aan elkaar te knopen, en vervolgens een schone, neobrutalistische bloglijst in Next.js uit te brengen.
Het resultaat is uitstekend als je stack op Next.js + TailwindCSS leunt. Payload is Next-native en Neobrutalism is Tailwind-first, dus de twee klikken naadloos in elkaar.
Wat je gaat bouwen
- Een Payload CMS-app met een Posts-collectie
- Een Next.js-frontend die de posts ophaalt uit de REST API van Payload
- Een
/blog-pagina die een responsieve lijst rendert met Neobrutalism-componenten (Cards, Badges, Buttons)
Vereisten
- Node.js 20.9+ (Payload vereist Node 20.9 of nieuwer)
- Next.js 15+ voor de frontend (Payload is Next-native)
- Elke compatibele DB: SQLite (dev), Postgres of MongoDB
Tip: Voor lokale ontwikkeling is SQLite de snelste weg. Schakel bij het deployen over op Postgres/Mongo.
Deel 1 — Payload CMS opzetten
Je kunt Payload aan een bestaande Next.js-app toevoegen of een nieuwe opzetten. Hier zetten we een apart Payload-project op, zodat de API ervan schoon en portabel blijft.
1) Een nieuwe Payload-app maken
Dit start Payload en zijn Admin-UI (meestal op http://localhost:3000/admin).
2) Een Posts-collectie definiëren
Maak 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;Voeg haar toe aan je hoofdconfiguratie 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
],
});Als je nog geen Media- of Users-collectie hebt, voer dan
create-payload-appuit met een blog-template of voeg later eenvoudige collecties toe voor uploads en auth.
3) Een paar posts aanmaken
Open de Admin-UI → Posts → Create New en voeg een paar gepubliceerde posts toe met publishedAt-datums in het verleden.
4) Bevestigen dat de REST API werkt
Payload stelt standaard een REST API beschikbaar op /api/<collection>.
Ga naar:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Je zou JSON met je posts moeten zien.
Stel bij het deployen
PAYLOAD_PUBLIC_SERVER_URLen je DB-omgevingsvariabelen in. Als je Payload los van Next.js host, schakel dan CORS in voor de origin van je frontend.
Deel 2 — Next.js + Neobrutalism-frontend opzetten
We bouwen een Next.js-app die de REST API van Payload consumeert en een bloglijst rendert met Neobrutalism-componenten.
1) De Next.js-app maken
2) TailwindCSS installeren (als je niet de Tailwind-template koos)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pVoeg ./components/**/*.{ts,tsx} en ./app/**/*.{ts,tsx} toe aan het content-veld van tailwind.config.ts. Neem ook de paden van Neobrutalism mee als de installer dat vereist.
3) Neobrutalism installeren
Gebruik de officiële installer (CLI) of de handmatige installatie. Voorbeeld (pnpm):
pnpm add retrouiImporteer daarna de componenten waar je ze nodig hebt. (Levert Neobrutalism een CLI om componenten te kopiëren, voer die dan hier uit en volg de aanwijzingen.)
4) Verbinden met Payload (env + fetch-helper)
Maak .env.local:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Voeg een klein fetch-hulpmiddel lib/payload.ts toe:
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) De bloglijstpagina bouwen met Neobrutalism
Maak 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>
);
}Als je Neobrutalism-pakket andere importnamen/-paden blootstelt, pas de regels
import { Card } from 'neobrutalism/card'dan aan zodat ze kloppen. Je kunt ook eender welk ander Neobrutalism-component inzetten (Tabs, Inputs enz.).
6) Postdetailpagina (optioneel)
Maak 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>
);
}Gebruik voor rich text de Payload-renderer van je voorkeur (bijv. @payloadcms/richtext-lexical) en render dienovereenkomstig.
Deel 3 — Cross-origin- & deploymentnotities
Als je Next.js-app op een andere origin draait dan Payload:
- Schakel CORS in Payload in: stel
cors: ["https://your-next-app.com", "http://localhost:3000"]in inpayload.config.ts. - Stel
serverURL(enPAYLOAD_PUBLIC_SERVER_URL) correct in, zodat de afbeeldings-URL's oplossen. - Bescherm concepten: hierboven stonden we publieke
readtoe. Vervang dat voor privé-inhoud door eenaccess.read-functie die auth/rollen controleert.
Deploymentopties:
- Self-hosting (Node-app op een VPS) of deployen op Vercel/Cloudflare voor een moderne DX.
- Gebruik SQLite voor snelle demo's, Postgres/Mongo voor prod.
Deel 4 — Uitbreidingen
- Zoeken & filters: gebruik de query-parameters van Payload (bijv.
where[title][like]) en knoop een Neobrutalism-Input + Tabs aan om te filteren. - Paginatie: de REST API geeft
totalDocs,limit,pageterug; bouw een „Meer laden” met een Neobrutalism-Button. - Afbeeldingen: verplaats
coverImagenaar een aparte Media-collectie en gebruik de upload-adapter van Payload. - Voorbeeldmodus: zet in Next.js een concept-voorbeeldroute op die data ophaalt met concept-auth-headers.
- GraphQL: Payload stelt ook GraphQL beschikbaar; gebruik het als je getypte queries verkiest.
Problemen oplossen
- CORS-fouten: Controleer de
cors-configuratie en zorg datserverURLis ingesteld. - Afbeeldingen verschijnen niet: Bevestig de
urldie voor uploads wordt teruggegeven en dat denext.config.jsvan Next.js het Payload-domein toestaat onderimages.domains. - Er wordt niets gerenderd: Inspecteer de fetch-URL in
lib/payload.ts. Zorg dat er records bestaan en datstatuspublishedis.
Samenvatting
Je hebt nu:
- Een Payload CMS-app die posts via REST beschikbaar stelt
- Een Next.js-site gestyled met Neobrutalism
- Een
/blog-pagina die posts opsomt en naar elke afzonderlijke post linkt
Voeg vanaf hier auteurs, categorieën en tags toe; style met meer Neobrutalism-componenten; en breng het uit.