Aller au contenu

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.

Un 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 /blog qui 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

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

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 Posts

Ajoutez-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-app avec 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 → PostsCreate 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_URL ainsi 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

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) Installer TailwindCSS (si vous n’avez pas choisi le template Tailwind)

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

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

Importez 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"] dans payload.config.ts.
  • Définissez correctement serverURL (et PAYLOAD_PUBLIC_SERVER_URL) pour que les URL d’images se résolvent.
  • Protégez les brouillons : plus haut, nous avons autorisé un read public. Pour du contenu privé, remplacez-le par une fonction access.read qui 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 coverImage vers 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 cors et assurez-vous que serverURL est bien défini.
  • Images qui n’apparaissent pas : confirmez l’url renvoyée pour les uploads et vérifiez que le next.config.js de Next.js autorise le domaine Payload sous images.domains.
  • Rien ne s’affiche : inspectez l’URL de fetch dans lib/payload.ts. Assurez-vous que des enregistrements existent et que leur status vaut published.

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 /blog qui 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.


← Retour aux articles