/bek.no.lo.ˈʒi.a/
Blog headless multilíngue para quem escreve e edita, em Next.js 16 e Strapi 5.
Projeto pessoal · Blog headless · Node.js
Imagens
Contexto
Cada post, categoria e tag tem versão por locale no Strapi, com pt-BR como origem. Front e CMS formam um monorepo Turborepo, os dois em Node.js, com tipos em @blog/types. O front usa App Router e Tailwind.
Problema
Quem escreve precisa de um fluxo só para publicar, ver o que foi lido e definir quem acessa o texto inteiro. Deploy, cache e a conta do visitante ficam fora desse fluxo. Rascunho, SEO e mídia entram nele.
O que fiz e por quê
Arquitetura e conteúdo
- Strapi 5 com PostgreSQL: Post, Author, Category e Tag com rascunho/publicação. O i18n é por campo: título, slug, resumo, texto e SEO mudam por locale; imagem, autor, categoria e tags são os mesmos em todas as traduções do post. O tempo de leitura é calculado no middleware do Strapi a partir do conteúdo.
- Theme Settings, single type sem i18n, guarda duas paletas em hex. O Next injeta como custom properties e o escuro é o padrão. Sem o registro salvo, vale a paleta deste portfólio.
- SSG e ISR. Todo path leva o locale, inclusive o padrão: / responde 307 para /pt-BR. Na publicação, um webhook invalida a página por tag, na hora (revalidateTag com expire 0). Os TTLs (300 s na API, 1 h no ISR) ficam só como rede de segurança. Post, categoria, tag e autor não usam generateStaticParams. O middleware se chama proxy.ts.
- Páginas: home paginada, post, categoria, tag, autor, busca, 404, conta, login, cadastro e área de admin. Paginação, não scroll infinito. Uploads no Cloudinary. Permissão pública de leitura criada no bootstrap do Strapi, não à mão no painel.
Cache em Redis de ponta a ponta
- Cache handler próprio do Next.js em Redis, sem o pacote @neshca/cache-handler. O índice por tag é um SET, para uma revalidação não apagar uma chave gravada ao mesmo tempo. O cache de ISR sobrevive a deploys e restarts. Testei derrubando o processo do Next.js e pausando o Strapi: um processo novo serviu a página a partir do cache, com a origem fora do ar.
- Rodando o cenário de falha de verdade, achei dois bugs: a promise de conexão rejeitada ficava memorizada e desligava o cache até o fim do processo, e a reconexão padrão do cliente Redis travava toda renderização quando o Redis caía. Corrigi resetando a promise e usando reconnectStrategy: false com timeout de 2 s: com o Redis fora, a página responde em cerca de 0,5 s sem cache e volta a gravar sozinha quando ele sobe.
- Cache da API REST do Strapi em Redis com o plugin da comunidade compatível com Strapi 5, porque o plugin previsto na especificação só suporta a v4.
Autenticação e SEO
- Firebase Auth (e-mail/senha e Google) com papéis cumulativos leitor, assinante, editor e admin em Custom Claims, definidos só no servidor. O papel padrão entra na sessão. Session cookies, rotas protegidas no runtime Node.js e Firebase Admin SDK inicializado sob demanda. O login do painel do Strapi continua separado.
- Post premium: o texto completo só para assinante ou acima; visitante vê um trecho. Draft Mode só para editor e admin.
- SEO por idioma: slug real de cada locale no hreflang, no sitemap e no seletor; JSON-LD (Article, BreadcrumbList, WebSite), Open Graph gerado na hora, RSS, robots e imagem em AVIF/WebP.
- GA4 só depois do consentimento (LGPD), com eventos de leitura e Web Vitals.
CI
- CI no GitHub Actions com install, lint, type-check e build em todo PR e push para main. O build do Next e o do Strapi passam sem Postgres, Redis ou CMS no ar.
Resultado
- A página pública acompanha a publicação, sem rebuild do Next.
- Smoke test contra produção. 15 ADRs registram os trade-offs.
Próximos passos
- Testes automatizados.
- Domínio próprio e CDN da Cloudflare, adiados até o domínio existir.