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 init

O 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.

ArquivoO que faz
lib/postline.tsO cliente do site: lê os artigos com as tags de cache.
lib/postline-blocks.tsxDesenha os blocos do artigo. Troque pelos seus componentes.
lib/postline-feed.tsTodos os artigos, página a página, para o sitemap e o feed.
app/blog/page.tsxA lista do blog.
app/blog/[slug]/page.tsxCada artigo, com título e descrição de SEO.
app/blog/sitemap.xml/route.tsO sitemap do blog. Adicione ao seu índice ou ao robots.txt.
app/blog/rss.xml/route.tsO feed RSS.
app/api/postline/webhook/route.tsRecebe os eventos e expira o cache.
app/api/postline/health/route.tsA 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-only

Configure 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ávelObrigatóriaO que é
POSTLINE_PROJECT_IDSimO projeto do site.
POSTLINE_READ_TOKENSimO token de leitura (pl_read_…). Só no servidor.
POSTLINE_WEBHOOK_SECRETSimA chave que confere a assinatura dos eventos (whsec_…).
POSTLINE_PREVIOUS_WEBHOOK_SECRETNãoA chave anterior, aceita enquanto uma rotação termina.
POSTLINE_API_URLNãoPadrã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 production

Depois, 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.ts usa "use cache", cacheTag(...) e cacheLife({ 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:

TagExpira
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:

EventoQuandoReferência
article.publishedUm artigo vai ao ar.Payload
article.updatedUma nova revisão de um artigo publicado vai ao ar.Payload
article.unpublishedUm artigo sai do ar.Payload
article.deletedUm artigo é apagado.Payload
article.slug_changedO 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 init de novo emite um token de leitura e uma chave de webhook novos. O CLI move a chave atual para POSTLINE_PREVIOUS_WEBHOOK_SECRET no .env.local.

Depois, na hospedagem:

  1. POSTLINE_WEBHOOK_SECRET recebe a chave nova; POSTLINE_PREVIOUS_WEBHOOK_SECRET, a anterior (e POSTLINE_READ_TOKEN o token novo, se rodou o init).
  2. Publique.
  3. Depois de 24 horas, apague POSTLINE_PREVIOUS_WEBHOOK_SECRET e 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.

app/api/postline/webhook/route.ts
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).

RespostaQuando
200 {"received": "<id>"}Tudo expirado.
400 invalid_eventO corpo não é um evento válido deste projeto.
401 invalid_signatureA assinatura não bate ou o momento está fora da janela de 5 minutos.
413 too_largeCorpo acima de 64 KB.
500 revalidation_failedO Next.js não conseguiu expirar o cache; o Postline tenta de novo.
500 handler_failedO 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.

app/api/postline/health/route.ts
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.

Nesta página