Naar inhoud

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.

Een 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

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

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-app uit met een blog-template of voeg later eenvoudige collecties toe voor uploads en auth.

3) Een paar posts aanmaken

Open de Admin-UI → PostsCreate 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_URL en 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

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 installeren (als je niet de Tailwind-template koos)

pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Voeg ./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 retroui

Importeer 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 in payload.config.ts.
  • Stel serverURL (en PAYLOAD_PUBLIC_SERVER_URL) correct in, zodat de afbeeldings-URL's oplossen.
  • Bescherm concepten: hierboven stonden we publieke read toe. Vervang dat voor privé-inhoud door een access.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, page terug; bouw een „Meer laden” met een Neobrutalism-Button.
  • Afbeeldingen: verplaats coverImage naar 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 dat serverURL is ingesteld.
  • Afbeeldingen verschijnen niet: Bevestig de url die voor uploads wordt teruggegeven en dat de next.config.js van Next.js het Payload-domein toestaat onder images.domains.
  • Er wordt niets gerenderd: Inspecteer de fetch-URL in lib/payload.ts. Zorg dat er records bestaan en dat status published is.

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.


← Terug naar de artikelen