Pular para o conteúdo

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.

Um 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 /blog que 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

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

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-app com 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 → PostsCreate 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_URL e 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

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) Instalar o TailwindCSS (se você não escolheu o template do Tailwind)

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

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

Depois 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"] no payload.config.ts.
  • Configure o serverURL (e o PAYLOAD_PUBLIC_SERVER_URL) corretamente para que as URLs das imagens sejam resolvidas.
  • Proteja os rascunhos: acima permitimos um read público. Para conteúdo privado, substitua-o por uma função access.read que 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 coverImage para 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 cors e garanta que o serverURL esteja definido.
  • Imagens que não aparecem: confirme a url retornada para os uploads e verifique se o next.config.js do Next.js autoriza o domínio do Payload em images.domains.
  • Nada é renderizado: inspecione a URL de fetch em lib/payload.ts. Garanta que existam registros e que o status deles seja published.

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


← Voltar aos artigos