Integração Next.js
Publique os artigos no seu site Next.js, no seu domínio e no seu layout, sem deploy por artigo.
Com a integração, cada artigo aprovado e agendado no Postline vai ao ar no seu site Next.js. O site lê os artigos publicados pela API de leitura, guarda em cache, e o Postline avisa por webhook quando algo muda. Depois do primeiro deploy, publicar não pede mudança de código.
Requisitos
- Next.js 13.4 ou mais novo, com o App Router. O Pages Router não é suportado.
- Um site que roda em Node.js: Vercel, Netlify ou o seu servidor. Cloudflare via OpenNext ainda não é suportado.
- Um projeto no Postline.
Instalar
Rode o init na pasta do site
npx @postline/cli initO CLI mostra um endereço e um código. Abra o endereço, entre no Postline e autorize (o código vale 15 minutos). O projeto autorizado é o que o site vai mostrar.
Confira os arquivos
O CLI detecta a versão do Next.js, o gerenciador de pacotes, se o projeto usa TypeScript e o modo de cache, lista os arquivos que vai criar e pergunta antes. Nenhum arquivo existente é alterado: um arquivo que já existe é mantido como seu.
| Arquivo | O que faz |
|---|---|
lib/postline.ts | O cliente do site: lê os artigos com as tags de cache. |
lib/postline-blocks.tsx | Desenha os blocos do artigo. Troque pelos seus componentes. |
lib/postline-feed.ts | Todos os artigos, página a página, para o sitemap e o feed. |
app/blog/page.tsx | A lista do blog. |
app/blog/[slug]/page.tsx | Cada artigo, com título e descrição de SEO. |
app/blog/sitemap.xml/route.ts | O sitemap do blog. Adicione ao seu índice ou ao robots.txt. |
app/blog/rss.xml/route.ts | O feed RSS. |
app/api/postline/webhook/route.ts | Recebe os eventos e expira o cache. |
app/api/postline/health/route.ts | A verificação da conexão. |
Com src/app, os arquivos vão para src/. Sem tsconfig.json, saem em JavaScript. O CLI anota o que criou em .postline/manifest.json.
Instale os pacotes
O CLI escreve as chaves em .env.local e termina com o comando para o seu gerenciador:
npm install @postline/next @postline/sdk server-onlyConfigure a hospedagem e publique
Copie as variáveis abaixo para a hospedagem, faça o deploy e conclua a conexão no Postline. A verificação chama /api/postline/health com um desafio, que o site assina com a chave do webhook: assim o Postline confere que o domínio roda o site instalado.
Variáveis de ambiente
| Variável | Obrigatória | O que é |
|---|---|---|
POSTLINE_PROJECT_ID | Sim | O projeto do site. |
POSTLINE_READ_TOKEN | Sim | O token de leitura (pl_read_…). Só no servidor. |
POSTLINE_WEBHOOK_SECRET | Sim | A chave que confere a assinatura dos eventos (whsec_…). |
POSTLINE_PREVIOUS_WEBHOOK_SECRET | Não | A chave anterior, aceita enquanto uma rotação termina. |
POSTLINE_API_URL | Não | Padrão: https://app.postline.com.br. |
Nunca com NEXT_PUBLIC_
As chaves ficam só no servidor. lib/postline.ts importa server-only, então um import acidental num componente de cliente quebra o build em vez de vazar o token.
Na Vercel
Em Settings › Environment Variables, adicione cada variável para Production (e Preview, se quiser o blog nas prévias), ou pela linha de comando:
vercel env add POSTLINE_PROJECT_ID production
vercel env add POSTLINE_READ_TOKEN production
vercel env add POSTLINE_WEBHOOK_SECRET productionDepois, publique de novo (vercel --prod ou um push) para o deploy ler as variáveis.
Como o cache funciona
O site lê os artigos com tags de cache e o webhook expira essas tags quando algo muda. Um artigo publicado depois do build aparece na primeira visita, sem novo deploy.
- Cache Components (Next.js 16 com
cacheComponents: true):lib/postline.tsusa"use cache",cacheTag(...)ecacheLife({ revalidate: 300 }). - Cache do fetch (os outros casos): cada leitura leva
next: { tags, revalidate: 300 }.
Os 300 segundos são uma rede de segurança para um webhook que não chegue. As tags:
| Tag | Expira |
|---|---|
postline:<projectId> | Tudo do projeto. |
postline:<projectId>:<locale> | Um idioma. |
postline:<projectId>:article:<articleId> | Um artigo. |
A cada evento, o webhook expira as tags do artigo e os caminhos do blog: a lista, o artigo (e o slug anterior, quando mudou), rss.xml e sitemap.xml. Ele nunca aplica o conteúdo do evento: a próxima visita lê a origem, então um evento atrasado não traz de volta uma revisão antiga.
Eventos do site
O Postline manda ao /api/postline/webhook do site:
| Evento | Quando | Referência |
|---|---|---|
article.published | Um artigo vai ao ar. | Payload |
article.updated | Uma nova revisão de um artigo publicado vai ao ar. | Payload |
article.unpublished | Um artigo sai do ar. | Payload |
article.deleted | Um artigo é apagado. | Payload |
article.slug_changed | O slug muda; previousSlug traz o antigo. | Payload |
Cada evento vem no envelope de todos os eventos, com os cabeçalhos webhook-id, webhook-timestamp e webhook-signature do Standard Webhooks. O data traz o artigo:
{
"id": "evt_9a7c…",
"type": "article.published",
"version": 1,
"timestamp": "2026-10-17T12:00:00.000Z",
"organizationId": "Zx9…",
"projectId": "c3a8f2d0-…",
"data": {
"articleId": "0b9c4e7a-…",
"revision": 3,
"locale": "pt-BR",
"slug": "onboarding-que-segura-clientes",
"previousSlug": null,
"sequence": 42
}
}createWebhookHandler confere a assinatura sozinho. Uma resposta que não seja 2xx é repetida, com o mesmo webhook-id.
Sites instalados antes do 0.2.0
O @postline/next 0.1 não entende o envelope, nem a API v1 (/articles, pl_read_). Atualize @postline/next e @postline/sdk para o 0.2.0 e rode npx @postline/cli init de novo: ele emite um token pl_read_ e uma chave whsec_ para as variáveis de ambiente. Como o init não altera arquivos que já existem, troque nos seus posts() por articles(), post( por article( e .items por .data (ou apague os arquivos gerados antes de rodar o init, para ele escrever de novo).
Rotacionar as chaves
A chave do webhook pode ser trocada sem perder entregas: depois de uma rotação, a chave anterior continua assinando por 24 horas.
- No Postline: na conexão do site, Rotacionar segredo mostra a chave nova uma vez.
- Pelo CLI: rodar
npx @postline/cli initde novo emite um token de leitura e uma chave de webhook novos. O CLI move a chave atual paraPOSTLINE_PREVIOUS_WEBHOOK_SECRETno.env.local.
Depois, na hospedagem:
POSTLINE_WEBHOOK_SECRETrecebe a chave nova;POSTLINE_PREVIOUS_WEBHOOK_SECRET, a anterior (ePOSTLINE_READ_TOKENo token novo, se rodou oinit).- Publique.
- Depois de 24 horas, apague
POSTLINE_PREVIOUS_WEBHOOK_SECRETe publique de novo.
Referência do @postline/next
Para servidor (importa server-only). Os arquivos que o init escreve já usam tudo isto.
createPostline(config)
As leituras do site, com as tags que o webhook expira.
import { createPostline } from "@postline/next"
const site = createPostline({
projectId: process.env.POSTLINE_PROJECT_ID!,
token: process.env.POSTLINE_READ_TOKEN!,
apiUrl: "https://app.postline.com.br",
locale: "pt-BR",
basePath: "/blog",
cacheMode: "fetch-cache", // ou "cache-components"
})
const { data, nextCursor } = await site.articles() // uma página da lista
const article = await site.article("onboarding-que-segura-clientes") // ou null
site.tags // ["postline:<projectId>", "postline:<projectId>:pt-BR"]createWebhookHandler(config)
O Route Handler de /api/postline/webhook. Confere a assinatura do Standard Webhooks (até 5 minutos de diferença, corpo até 64 KB), valida o evento e expira tags e caminhos.
import { createWebhookHandler } from "@postline/next"
import { revalidatePath, revalidateTag } from "next/cache"
export async function POST(request: Request) {
return createWebhookHandler({
projectId: process.env.POSTLINE_PROJECT_ID!,
secret: process.env.POSTLINE_WEBHOOK_SECRET!,
previousSecret: process.env.POSTLINE_PREVIOUS_WEBHOOK_SECRET,
basePath: "/blog",
revalidateTag,
revalidatePath,
// Opcional: o seu tratamento, depois de expirar o cache.
onEvent: async (event) => {
// event é o envelope; um erro aqui faz o Postline tentar de novo
},
})(request)
}onEvent recebe o envelope (WebhookEvent).
| Resposta | Quando |
|---|---|
200 {"received": "<id>"} | Tudo expirado. |
400 invalid_event | O corpo não é um evento válido deste projeto. |
401 invalid_signature | A assinatura não bate ou o momento está fora da janela de 5 minutos. |
413 too_large | Corpo acima de 64 KB. |
500 revalidation_failed | O Next.js não conseguiu expirar o cache; o Postline tenta de novo. |
500 handler_failed | O seu onEvent deu erro; o Postline tenta de novo. |
createHealthHandler(config)
O Route Handler de /api/postline/health, lido pela verificação da conexão. Responde { ok, projectId, nextVersion, cacheMode } e, com ?challenge=…, também proof: o HMAC-SHA256 de health:<challenge> com a chave do webhook.
import { createHealthHandler } from "@postline/next"
export async function GET(request: Request) {
return createHealthHandler({
projectId: process.env.POSTLINE_PROJECT_ID!,
secret: process.env.POSTLINE_WEBHOOK_SECRET!,
nextVersion: "16.0.0",
cacheMode: "fetch-cache",
})(request)
}tagsFor(projectId, event) e WebhookEvent
tagsFor devolve as três tags de um artigo ({ articleId, locale }, como em event.data), para expirar à mão. WebhookEvent é o tipo do envelope de um evento do site.