Zum Inhalt springen

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.

Eine 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

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

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-app mit 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 → PostsCreate 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_URL und 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

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 installieren (falls du nicht das Tailwind-Template gewählt hast)

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

Fü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 retroui

Importiere 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"] in payload.config.ts.
  • Setze serverURL (und PAYLOAD_PUBLIC_SERVER_URL) korrekt, damit die Bild-URLs auflösen.
  • Schütze Entwürfe: oben haben wir öffentlichen read erlaubt. Für private Inhalte ersetze das durch eine access.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 coverImage in 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, dass serverURL gesetzt ist.
  • Bilder werden nicht angezeigt: Bestätige die für Uploads zurückgegebene url und dass die next.config.js von Next.js die Payload-Domain unter images.domains erlaubt.
  • Nichts wird gerendert: Untersuche die Fetch-URL in lib/payload.ts. Stelle sicher, dass Datensätze existieren und status published ist.

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.


← Zurück zu den Artikeln