14. November 2025
Neobrutalism mit Payload CMS integrieren
UI-Bibliotheken können deine Entwicklung deutlich beschleunigen. In diesem Artikel gehen wir die 5 besten für dein nächstes React-Projekt durch.
Dov Azencot
@DovAzencotEine praktische End-to-End-Anleitung, um Payload CMS als Content-Backend und Neobrutalism als Frontend-Komponenten-Kit zu verdrahten, und dann eine saubere, neobrutalistische Blog-Liste in Next.js zu veröffentlichen.
Das Ergebnis ist hervorragend, wenn dein Stack auf Next.js + TailwindCSS setzt. Payload ist Next-nativ und Neobrutalism ist Tailwind-first, sodass beide bestens ineinandergreifen.
Was du bauen wirst
- Eine Payload-CMS-App mit einer Posts-Collection
- Ein Next.js-Frontend, das die Beiträge über Payloads REST-API abruft
- Eine
/blog-Seite, die eine responsive Liste mit Neobrutalism-Komponenten rendert (Cards, Badges, Buttons)
Voraussetzungen
- Node.js 20.9+ (Payload erfordert Node 20.9 oder neuer)
- Next.js 15+ für das Frontend (Payload ist Next-nativ)
- Jede kompatible DB: SQLite (Dev), Postgres oder MongoDB
Tipp: Für die lokale Entwicklung ist SQLite der schnellste Weg. Wechsle beim Deployen zu Postgres/Mongo.
Teil 1 — Payload CMS einrichten
Du kannst Payload zu einer bestehenden Next.js-App hinzufügen oder eine neue aufsetzen. Wir setzen ein eigenes Payload-Projekt auf, damit seine API sauber und portabel bleibt.
1) Eine neue Payload-App erstellen
Das startet Payload und seine Admin-UI (üblicherweise unter http://localhost:3000/admin).
2) Eine Posts-Collection definieren
Erstelle 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;Füge sie deiner Hauptkonfiguration src/payload.config.ts hinzu:
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
],
});Wenn du noch keine Media- oder Users-Collection hast, führe
create-payload-appmit einem Blog-Template aus oder füge später einfache Collections für Uploads und Auth hinzu.
3) Ein paar Beiträge anlegen
Öffne die Admin-UI → Posts → Create New und füge ein paar veröffentlichte Beiträge mit vergangenen publishedAt-Daten hinzu.
4) Prüfen, ob die REST-API funktioniert
Payload stellt standardmäßig eine REST-API unter /api/<collection> bereit.
Rufe auf:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Du solltest JSON mit deinen Beiträgen sehen.
Beim Deployen setzt du
PAYLOAD_PUBLIC_SERVER_URLund deine DB-Umgebungsvariablen. Wenn du Payload getrennt von Next.js hostest, aktiviere CORS für den Origin deines Frontends.
Teil 2 — Next.js + Neobrutalism-Frontend einrichten
Wir bauen eine Next.js-App, die Payloads REST-API konsumiert und eine Blog-Liste mit Neobrutalism-Komponenten rendert.
1) Die Next.js-App erstellen
2) TailwindCSS installieren (falls du nicht das Tailwind-Template gewählt hast)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pFüge ./components/**/*.{ts,tsx} und ./app/**/*.{ts,tsx} zum content-Feld von tailwind.config.ts hinzu. Nimm auch die Pfade von Neobrutalism mit auf, falls der Installer das verlangt.
3) Neobrutalism installieren
Nutze den offiziellen Installer (CLI) oder die manuelle Installation. Beispiel (pnpm):
pnpm add retrouiImportiere dann die Komponenten dort, wo du sie brauchst. (Falls Neobrutalism eine CLI zum Kopieren von Komponenten mitbringt, führe sie hier aus und folge den Anweisungen.)
4) Mit Payload verbinden (env + Fetch-Helper)
Erstelle .env.local:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Füge ein kleines Fetch-Utility lib/payload.ts hinzu:
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) Die Blog-Listen-Seite mit Neobrutalism bauen
Erstelle 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>
);
}Wenn dein Neobrutalism-Paket andere Import-Namen/-Pfade bereitstellt, passe die
import { Card } from 'neobrutalism/card'-Zeilen entsprechend an. Du kannst auch beliebige andere Neobrutalism-Komponenten einsetzen (Tabs, Inputs usw.).
6) Beitragsdetailseite (optional)
Erstelle 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>
);
}Für Rich Text nutze den Payload-Renderer deiner Wahl (z. B. @payloadcms/richtext-lexical) und rendere entsprechend.
Teil 3 — Cross-Origin- & Deployment-Hinweise
Wenn deine Next.js-App auf einem anderen Origin läuft als Payload:
- Aktiviere CORS in Payload: setze
cors: ["https://your-next-app.com", "http://localhost:3000"]inpayload.config.ts. - Setze
serverURL(undPAYLOAD_PUBLIC_SERVER_URL) korrekt, damit die Bild-URLs auflösen. - Schütze Entwürfe: oben haben wir öffentlichen
readerlaubt. Für private Inhalte ersetze das durch eineaccess.read-Funktion, die Auth/Rollen prüft.
Deployment-Optionen:
- Self-Hosting (Node-App auf einem VPS) oder Deployment auf Vercel/Cloudflare für moderne DX.
- Nutze SQLite für schnelle Demos, Postgres/Mongo für Prod.
Teil 4 — Erweiterungen
- Suche & Filter: Nutze Payloads Query-Parameter (z. B.
where[title][like]) und verdrahte ein Neobrutalism-Input + Tabs zum Filtern. - Pagination: Die REST-API liefert
totalDocs,limit,page; baue ein „Mehr laden“ mit einem Neobrutalism-Button. - Bilder: Verschiebe
coverImagein eine eigene Media-Collection und nutze Payloads Upload-Adapter. - Vorschaumodus: Richte in Next.js eine Entwurfsvorschau-Route ein, die mit Draft-Auth-Headern abruft.
- GraphQL: Payload stellt auch GraphQL bereit; nutze es, wenn du typisierte Queries bevorzugst.
Fehlerbehebung
- CORS-Fehler: Prüfe die
cors-Konfiguration und stelle sicher, dassserverURLgesetzt ist. - Bilder werden nicht angezeigt: Bestätige die für Uploads zurückgegebene
urlund dass dienext.config.jsvon Next.js die Payload-Domain unterimages.domainserlaubt. - Nichts wird gerendert: Untersuche die Fetch-URL in
lib/payload.ts. Stelle sicher, dass Datensätze existieren undstatuspublishedist.
Zusammenfassung
Du hast jetzt:
- Eine Payload-CMS-App, die Beiträge über REST bereitstellt
- Eine Next.js-Seite, gestylt mit Neobrutalism
- Eine
/blog-Seite, die Beiträge auflistet und auf einzelne verlinkt
Von hier aus füge Autoren, Kategorien und Tags hinzu; style mit weiteren Neobrutalism-Komponenten; und veröffentliche es.