14 de novembro de 2025
Integrar o Neobrutalism com o Payload CMS
As bibliotecas de UI podem acelerar bastante o seu desenvolvimento. Neste artigo, repassamos as 5 melhores para o seu próximo projeto React.
Dov Azencot
@DovAzencotUm guia prático, de ponta a ponta, para conectar o Payload CMS como backend de conteúdo e o Neobrutalism como kit de componentes de front-end, e depois despachar uma lista de artigos neobrutalista e caprichada em Next.js.
O resultado é excelente se a sua stack se apoia em Next.js + TailwindCSS. O Payload é nativo do Next e o Neobrutalism é pensado com o Tailwind em primeiro lugar, então os dois se encaixam de maravilha.
O que você vai construir
- Uma app de Payload CMS com uma coleção Posts
- Um front-end em Next.js que busca os artigos a partir da API REST do Payload
- Uma página
/blogque renderiza uma lista responsiva usando componentes do Neobrutalism (Cards, Badges, Buttons)
Pré-requisitos
- Node.js 20.9+ (o Payload exige o Node 20.9 ou uma versão mais recente)
- Next.js 15+ para o front-end (o Payload é nativo do Next)
- Qualquer banco de dados compatível: SQLite (dev), Postgres ou MongoDB
Dica: para o desenvolvimento local, o SQLite é o caminho mais rápido. Mude para Postgres/Mongo ao fazer o deploy.
Parte 1 — Configurar o Payload CMS
Você pode adicionar o Payload a uma app de Next.js existente ou gerar uma nova. Aqui vamos gerar um projeto Payload dedicado para manter a sua API limpa e portável.
1) Criar uma nova app de Payload
Isso inicia o Payload e a sua interface de administração (em geral em http://localhost:3000/admin).
2) Definir uma coleção Posts
Crie 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;Adicione-a à sua configuração principal 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 você ainda não tem uma coleção Media ou Users, rode o
create-payload-appcom um template de blog ou adicione mais tarde coleções simples para os uploads e a autenticação.
3) Semear alguns artigos
Abra a interface de administração → Posts → Create New e adicione alguns artigos publicados com datas publishedAt no passado.
4) Confirmar que a API REST funciona
O Payload expõe uma API REST por padrão em /api/<collection>.
Acesse:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Você deve ver um JSON com os seus artigos.
Ao fazer o deploy, defina o
PAYLOAD_PUBLIC_SERVER_URLe as variáveis de ambiente do seu banco de dados. Se você hospedar o Payload separado do Next.js, habilite o CORS para a origem do seu front-end.
Parte 2 — Configurar o front-end Next.js + Neobrutalism
Vamos construir uma app de Next.js que consome a API REST do Payload e renderiza uma lista de artigos usando componentes do Neobrutalism.
1) Criar a app de Next.js
2) Instalar o TailwindCSS (se você não escolheu o template do Tailwind)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pAdicione ./components/**/*.{ts,tsx} e ./app/**/*.{ts,tsx} ao campo content do tailwind.config.ts. Inclua também os caminhos do Neobrutalism, caso o instalador exija.
3) Instalar o Neobrutalism
Use o instalador oficial (CLI) ou a instalação manual. Exemplo (pnpm):
pnpm add retrouiDepois importe os componentes onde precisar. (Se o Neobrutalism incluir uma CLI para copiar componentes, execute-a aqui e siga as instruções.)
4) Conectar ao Payload (env + utilitário de fetch)
Crie .env.local:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Adicione um pequeno utilitário 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) Construir a página de lista de artigos com o Neobrutalism
Crie 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 o seu pacote do Neobrutalism expuser outros nomes ou caminhos de import, ajuste as linhas
import { Card } from 'neobrutalism/card'de acordo. Você também pode trocar por qualquer outro componente do Neobrutalism (Tabs, Inputs, etc.).
6) Página de detalhe de um artigo (opcional)
Crie 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>
);
}Para o rich text, use o renderer do Payload que preferir (por exemplo, @payloadcms/richtext-lexical) e renderize-o de acordo.
Parte 3 — Notas sobre cross-origin e deploy
Se a sua app de Next.js roda em uma origem diferente da do Payload:
- Habilite o CORS no Payload: defina
cors: ["https://your-next-app.com", "http://localhost:3000"]nopayload.config.ts. - Configure o
serverURL(e oPAYLOAD_PUBLIC_SERVER_URL) corretamente para que as URLs das imagens sejam resolvidas. - Proteja os rascunhos: acima permitimos um
readpúblico. Para conteúdo privado, substitua-o por uma funçãoaccess.readque verifique a autenticação e os papéis.
Opções de deploy:
- Autohospedagem (app de Node em um VPS) ou deploy na Vercel/Cloudflare para uma DX moderna.
- Use SQLite para demos rápidas, Postgres/Mongo para produção.
Parte 4 — Aprimoramentos
- Busca e filtros: use os parâmetros de consulta do Payload (por exemplo,
where[title][like]) e conecte um Input + umas Tabs do Neobrutalism para filtrar. - Paginação: a API REST retorna
totalDocs,limit,page; construa um "Carregar mais" com um Button do Neobrutalism. - Imagens: mova o
coverImagepara uma coleção Media dedicada e use o adaptador de uploads do Payload. - Modo de preview: monte uma rota de preview de rascunhos no Next.js que busque os dados com os cabeçalhos de autenticação de rascunho.
- GraphQL: o Payload também expõe GraphQL; use-o se preferir consultas tipadas.
Resolução de problemas
- Erros de CORS: revise a configuração
corse garanta que oserverURLesteja definido. - Imagens que não aparecem: confirme a
urlretornada para os uploads e verifique se onext.config.jsdo Next.js autoriza o domínio do Payload emimages.domains. - Nada é renderizado: inspecione a URL de fetch em
lib/payload.ts. Garanta que existam registros e que ostatusdeles sejapublished.
Recapitulando
Agora você tem:
- Uma app de Payload CMS que expõe artigos via REST
- Um site de Next.js estilizado com o Neobrutalism
- Uma página
/blogque lista os artigos e leva a cada um
Daqui em diante, adicione autores, categorias e tags; dê estilo com mais componentes do Neobrutalism; e coloque no ar.