14 novembre 2025
Intégrer Neobrutalism avec Payload CMS
Les bibliothèques d’interface peuvent accélérer nettement votre développement. Dans cet article, nous passons en revue le top 5 pour votre prochain projet React.
Dov Azencot
@DovAzencotUn guide concret, de bout en bout, pour brancher Payload CMS comme backend de contenu et Neobrutalism comme kit de composants front-end, puis livrer une liste d’articles néo-brutaliste et soignée dans Next.js.
Le résultat est excellent si votre stack repose sur Next.js + TailwindCSS. Payload est nativement Next et Neobrutalism est pensé Tailwind d’abord : les deux s’emboîtent naturellement.
Ce que vous allez construire
- Une application Payload CMS avec une collection Posts
- Un front-end Next.js qui récupère les articles depuis l’API REST de Payload
- Une page
/blogqui affiche une liste responsive à l’aide des composants Neobrutalism (Cards, Badges, Buttons)
Prérequis
- Node.js 20.9+ (Payload exige Node 20.9 ou une version plus récente)
- Next.js 15+ pour le front-end (Payload est nativement Next)
- N’importe quelle base compatible : SQLite (dev), Postgres ou MongoDB
Astuce : en développement local, SQLite est le chemin le plus rapide. Passez à Postgres/Mongo au moment du déploiement.
Partie 1 — Mettre en place Payload CMS
Vous pouvez ajouter Payload à une application Next.js existante ou en générer une nouvelle. Ici, nous générons un projet Payload dédié pour garder son API propre et portable.
1) Créer une nouvelle application Payload
Cette commande démarre Payload et son interface d’administration (généralement sur http://localhost:3000/admin).
2) Définir une collection Posts
Créez 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 PostsAjoutez-la à votre configuration 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
],
})Si vous n’avez pas encore de collection Media ou Users, lancez
create-payload-appavec un template de blog, ou ajoutez plus tard des collections simples pour les uploads et l’authentification.
3) Insérer quelques articles
Ouvrez l’interface d’administration → Posts → Create New et ajoutez deux ou trois articles publiés avec des dates publishedAt passées.
4) Vérifier que l’API REST fonctionne
Payload expose une API REST par défaut sur /api/<collection>.
Rendez-vous à l’adresse :
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Vous devriez voir un JSON contenant vos articles.
Au moment du déploiement, renseignez
PAYLOAD_PUBLIC_SERVER_URLainsi que les variables d’environnement de votre base. Si vous hébergez Payload à part de Next.js, activez le CORS pour l’origine de votre front-end.
Partie 2 — Mettre en place le front-end Next.js + Neobrutalism
Nous allons construire une application Next.js qui consomme l’API REST de Payload et affiche une liste d’articles avec les composants Neobrutalism.
1) Créer l’application Next.js
2) Installer TailwindCSS (si vous n’avez pas choisi le template Tailwind)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pAjoutez ./components/**/*.{ts,tsx} et ./app/**/*.{ts,tsx} au champ content de tailwind.config.ts. Incluez également les chemins de Neobrutalism si l’installeur l’exige.
3) Installer Neobrutalism
Utilisez l’installeur officiel (CLI) ou l’installation manuelle. Exemple (pnpm) :
pnpm add retrouiImportez ensuite les composants là où vous en avez besoin. (Si Neobrutalism fournit une CLI pour copier les composants, lancez-la ici et suivez les indications.)
4) Se connecter à Payload (env + utilitaire de fetch)
Créez .env.local :
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Ajoutez un petit utilitaire de 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) Construire la page de liste d’articles avec Neobrutalism
Créez app/blog/page.tsx (App Router) :
import Image from "next/image"
import Link from "next/link"
import { Badge } from "neobrutalism/badge"
import { Button } from "neobrutalism/button"
// Example Neobrutalism components — adjust paths/names per your install
import { Card } from "neobrutalism/card"
import { getPublishedPosts, type Post } from "@/lib/payload"
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="mb-6 text-4xl font-extrabold">Blog</h1>
<p className="mb-10 text-muted-foreground">
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="overflow-hidden p-0">
{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="space-y-3 p-5">
<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 leading-tight font-bold">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
{post.excerpt ? (
<p className="line-clamp-3 text-sm text-muted-foreground">
{post.excerpt}
</p>
) : null}
<div className="pt-2">
<Button asChild>
<Link href={`/blog/${post.slug}`}>Read more</Link>
</Button>
</div>
</div>
</Card>
))}
</section>
</main>
)
}Si votre paquet Neobrutalism expose d’autres noms ou chemins d’import, adaptez les lignes
import { Card } from 'neobrutalism/card'en conséquence. Vous pouvez aussi remplacer par n’importe quel autre composant Neobrutalism (Tabs, Inputs, etc.).
6) Page de détail d’un article (facultatif)
Créez 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 max-w-3xl px-4 py-10">
<Link href="/blog" className="underline">
← Back to blog
</Link>
<h1 className="mt-3 text-4xl font-extrabold">{post.title}</h1>
{post.coverImage?.url ? (
<div className="relative my-6 h-80 w-full">
<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>
)
}Pour le rich text, utilisez le renderer Payload de votre choix (par exemple @payloadcms/richtext-lexical) et affichez-le en conséquence.
Partie 3 — Notes sur le cross-origin et le déploiement
Si votre application Next.js tourne sur une origine différente de celle de Payload :
- Activez le CORS dans Payload : renseignez
cors: ["https://your-next-app.com", "http://localhost:3000"]danspayload.config.ts. - Définissez correctement
serverURL(etPAYLOAD_PUBLIC_SERVER_URL) pour que les URL d’images se résolvent. - Protégez les brouillons : plus haut, nous avons autorisé un
readpublic. Pour du contenu privé, remplacez-le par une fonctionaccess.readqui vérifie l’authentification et les rôles.
Options de déploiement :
- Auto-hébergement (application Node sur un VPS) ou déploiement sur Vercel/Cloudflare pour une DX moderne.
- Utilisez SQLite pour les démos rapides, Postgres/Mongo pour la production.
Partie 4 — Pour aller plus loin
- Recherche et filtres : exploitez les paramètres de requête de Payload (par exemple
where[title][like]) et branchez un Input + des Tabs Neobrutalism pour filtrer. - Pagination : l’API REST renvoie
totalDocs,limit,page; construisez un « Charger plus » avec un Button Neobrutalism. - Images : déplacez
coverImagevers une collection Media dédiée et servez-vous de l’adaptateur d’upload de Payload. - Mode aperçu : mettez en place une route d’aperçu de brouillon dans Next.js qui récupère les données avec les en-têtes d’authentification de brouillon.
- GraphQL : Payload expose aussi une API GraphQL ; adoptez-la si vous préférez des requêtes typées.
Dépannage
- Erreurs CORS : vérifiez la configuration
corset assurez-vous queserverURLest bien défini. - Images qui n’apparaissent pas : confirmez l’
urlrenvoyée pour les uploads et vérifiez que lenext.config.jsde Next.js autorise le domaine Payload sousimages.domains. - Rien ne s’affiche : inspectez l’URL de fetch dans
lib/payload.ts. Assurez-vous que des enregistrements existent et que leurstatusvautpublished.
Récapitulatif
Vous disposez désormais de :
- Une application Payload CMS qui expose des articles via REST
- Un site Next.js habillé avec Neobrutalism
- Une page
/blogqui liste les articles et renvoie vers chacun d’eux
À partir de là, ajoutez des auteurs, des catégories et des tags ; peaufinez le tout avec d’autres composants Neobrutalism ; et mettez-le en ligne.