14 novembre 2025
Integrare Neobrutalism con Payload CMS
Le librerie UI possono accelerare notevolmente il tuo sviluppo. In questo articolo passiamo in rassegna le 5 migliori per il tuo prossimo progetto React.
Dov Azencot
@DovAzencotUna guida pratica, dall'inizio alla fine, per collegare Payload CMS come backend dei contenuti e Neobrutalism come kit di componenti del front-end, e poi rilasciare un elenco di articoli neobrutalista e pulito in Next.js.
Il risultato è ottimo se il tuo stack è Next.js + TailwindCSS. Payload è nativo per Next e Neobrutalism è pensato con Tailwind in primo piano, così i due si incastrano alla perfezione.
Cosa costruirai
- Un'app Payload CMS con una collection Posts
- Un front-end Next.js che recupera gli articoli dalla REST API di Payload
- Una pagina
/blogche renderizza un elenco responsive usando i componenti di Neobrutalism (Cards, Badges, Buttons)
Prerequisiti
- Node.js 20.9+ (Payload richiede Node 20.9 o più recente)
- Next.js 15+ per il front-end (Payload è nativo per Next)
- Qualsiasi database compatibile: SQLite (dev), Postgres o MongoDB
Suggerimento: per lo sviluppo locale, SQLite è la via più rapida. Passa a Postgres/Mongo al momento del deploy.
Parte 1 — Configurare Payload CMS
Puoi aggiungere Payload a un'app Next.js esistente o generarne una nuova. Qui generiamo un progetto Payload dedicato, così la sua API resta pulita e portabile.
1) Creare una nuova app Payload
Questo avvia Payload e la sua Admin UI (di solito su http://localhost:3000/admin).
2) Definire una collection Posts
Crea 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;Aggiungila alla configurazione principale 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
],
});Se non hai ancora una collection Media o Users, esegui
create-payload-appcon un template blog oppure aggiungi più avanti semplici collection per gli upload e l'autenticazione.
3) Popolare qualche articolo
Apri l'Admin UI → Posts → Create New e aggiungi un paio di articoli pubblicati con date publishedAt passate.
4) Verificare che la REST API funzioni
Payload espone una REST API di default su /api/<collection>.
Vai su:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Dovresti vedere un JSON con i tuoi articoli.
Al momento del deploy, imposta
PAYLOAD_PUBLIC_SERVER_URLe le variabili d'ambiente del tuo database. Se ospiti Payload separatamente da Next.js, abilita il CORS per l'origine del tuo front-end.
Parte 2 — Configurare il front-end Next.js + Neobrutalism
Costruiamo un'app Next.js che consuma la REST API di Payload e renderizza un elenco di articoli usando i componenti di Neobrutalism.
1) Creare l'app Next.js
2) Installare TailwindCSS (se non hai scelto il template Tailwind)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pAggiungi ./components/**/*.{ts,tsx} e ./app/**/*.{ts,tsx} al campo content di tailwind.config.ts. Includi anche i path di Neobrutalism se l'installer lo richiede.
3) Installare Neobrutalism
Usa l'installer ufficiale (CLI) o l'installazione manuale. Esempio (pnpm):
pnpm add retrouiPoi importa i componenti dove ti servono. (Se Neobrutalism include una CLI per copiare i componenti, eseguila qui e segui le indicazioni.)
4) Collegarsi a Payload (env + helper di fetch)
Crea .env.local:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Aggiungi una piccola utility di fetch 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) Costruire la pagina dell'elenco articoli con Neobrutalism
Crea 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>
);
}Se il tuo pacchetto Neobrutalism espone nomi o path di import diversi, adatta di conseguenza le righe
import { Card } from 'neobrutalism/card'. Puoi anche sostituirli con qualsiasi altro componente di Neobrutalism (Tabs, Inputs, ecc.).
6) Pagina di dettaglio dell'articolo (opzionale)
Crea 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>
);
}Per il rich text, usa il renderer di Payload che preferisci (ad esempio @payloadcms/richtext-lexical) e renderizzalo di conseguenza.
Parte 3 — Note su cross-origin e deploy
Se la tua app Next.js gira su un'origine diversa da quella di Payload:
- Abilita il CORS in Payload: imposta
cors: ["https://your-next-app.com", "http://localhost:3000"]inpayload.config.ts. - Configura correttamente
serverURL(ePAYLOAD_PUBLIC_SERVER_URL) affinché gli URL delle immagini si risolvano. - Proteggi le bozze: sopra abbiamo consentito un
readpubblico. Per i contenuti privati, sostituiscilo con una funzioneaccess.readche verifichi autenticazione e ruoli.
Opzioni di deploy:
- Self-hosting (app Node su un VPS) oppure deploy su Vercel/Cloudflare per una DX moderna.
- Usa SQLite per le demo rapide, Postgres/Mongo per la produzione.
Parte 4 — Miglioramenti
- Ricerca e filtri: usa i parametri di query di Payload (ad esempio
where[title][like]) e collega un Input + dei Tabs di Neobrutalism per filtrare. - Paginazione: la REST API restituisce
totalDocs,limit,page; costruisci un «Carica altro» con un Button di Neobrutalism. - Immagini: sposta
coverImagein una collection Media dedicata e usa l'adapter di upload di Payload. - Modalità anteprima: predisponi in Next.js una route di anteprima delle bozze che recupera i dati con gli header di autenticazione delle bozze.
- GraphQL: Payload espone anche GraphQL; usalo se preferisci query tipizzate.
Risoluzione dei problemi
- Errori CORS: controlla la configurazione
corse assicurati cheserverURLsia impostato. - Immagini che non compaiono: verifica l'
urlrestituito per gli upload e che ilnext.config.jsdi Next.js autorizzi il dominio di Payload sottoimages.domains. - Non viene renderizzato nulla: ispeziona l'URL di fetch in
lib/payload.ts. Assicurati che i record esistano e che il lorostatussiapublished.
Riepilogo
Ora hai:
- Un'app Payload CMS che espone gli articoli via REST
- Un sito Next.js con lo stile di Neobrutalism
- Una pagina
/blogche elenca gli articoli e rimanda a ciascuno di essi
Da qui, aggiungi autori, categorie e tag, dai stile con altri componenti di Neobrutalism e mandalo in produzione.