Pular para o conteúdo
Kayro Gomes
CMS,  Frontend

Migrando para Payload CMS + Next.js 15

Author

Kayro Gomes

Date Published

Foto de capa ilustrativa com gradiente azul e roxo — usada no hero da home

Este site já foi WordPress, depois Next.js puro com MDX, depois um blog estático. Em 2026 migrei para Payload CMS 3 rodando em cima de Next.js 15 com Postgres no Neon e deploy na Vercel. A decisão foi menos sobre "qual CMS é melhor" e mais sobre "quero editar conteúdo pelo navegador, mas sem abrir mão do React quando precisar".

Por que Payload

Payload mora dentro do seu app Next.js como um pacote npm. Não é um servidor separado, não é uma API externa — você define as collections, registra os plugins, e ele aparece em /admin automaticamente. Para um portfólio pessoal, isso é ouro: posso customizar a UI, adicionar blocos novos, mudar a estrutura dos posts sem pedir permissão a ninguém.

TL;DR: Payload + Next.js 15 + Neon + Vercel = CMS moderno, deploy trivial, preview ao vivo, e código 100% aberto pra eu hackear quando quiser.

A migração em 4 passos

Não precisei reescrever o site. O processo foi:

1. Schema das collections

Defini Posts, Pages, Categories, Media, Forms e Users em arquivos TypeScript. Cada field do schema vira coluna no Postgres automaticamente — o adapter vercelPostgresAdapter cuida do DDL. Quando você roda pnpm dev pela primeira vez, o Payload compara o schema com o banco e faz push das diferenças (em prod, ele exige migrations explícitas via pnpm payload migrate).

2. Storage de mídia

Em vez de armazenar uploads no filesystem do Vercel (que é efêmero), usei o plugin @payloadcms/storage-vercel-blob. Cada upload vai pro Vercel Blob, e o Payload guarda só a URL no banco. As variantes (thumbnail, square, small, medium, large) são geradas via sharp no onUpload.

3. Renderização no front

As páginas de [slug] e /posts/[slug] usam getPayload({ config }) direto no Server Component. Sem API route, sem GraphQL, sem cache manual — o Next lida com tudo via React Server Components. A página é gerada estaticamente no build e revalidada por tag quando você publica uma página nova no admin.

Exemplo: query de página com draft mode

1const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
2 const { isEnabled: draft } = await draftMode()
3 const payload = await getPayload({ config: configPromise })
4
5 const result = await payload.find({
6 collection: 'pages',
7 draft,
8 limit: 1,
9 pagination: false,
10 overrideAccess: draft,
11 where: { slug: { equals: slug } },
12 })
13
14 return result.docs?.[0] || null
15})

Note o draft mode: quando você entra em preview pelo admin, a página renderiza a versão _draft=true antes de publicar. Em produção, só vê a versão publicada.

4. O que eu queria e o que consegui

Queria preview ao vivo sem dor, versionamento de posts, e a possibilidade de customizar o editor com blocos novos (tipo um "Callout" ou "Code Snippet" especializado). Tudo isso veio de fábrica. O que ainda dá trabalho: deploy com dev mode acidental poluindo a tabela payload_migrations — daí ter um branch dev separado no Neon e checagem automática no build.

Imagem ilustrativa de exemplo usada em posts do blog

Imagem ilustrativa de exemplo. Substitua no admin em Mídia.

Nos próximos posts vou detalhar o setup Neon + branch dev (isolando ambiente local sem tocar produção) e o que aprendi migrando a busca de Fuse.js para a busca nativa do Payload. Se tiver dúvida ou quer ver o código-fonte completo, o repo está aberto.