Todo pipeline que gera conteúdo com apoio de IA em escala corre um risco específico: o Google Search trata conteúdo em escala sem valor humano agregado como spam, independente de ter sido produzido por automação, por humano, ou pelos dois. É a política oficial de Spam Policies for Google Web Search. Não é usar IA que é punido. É publicar sem revisão que agregue valor real ao leitor.

Foi essa a linha que guiou a arquitetura deste próprio blog: um pipeline no N8N que gera rascunho via LLM, mas nunca publica sozinho. Cada post passa por um gate humano (eu) antes de ir ao ar. Este post é literalmente esse pipeline descrevendo a si mesmo.

Por que um gate humano obrigatório

A decisão foi deliberada: cadência de 2 a 4 posts por semana, não publicação diária full-automática. Isso reduz o volume que um único revisor consegue realmente avaliar com atenção, e é exatamente esse atrito que separa “IA-assistido com revisão” de “conteúdo em escala”.

O gate não é burocracia. É a peça que decide se o domínio inteiro fica de pé no ranqueamento ou não.

Na prática, o gate vive como Pull Request no GitHub. O N8N escreve o markdown, abre o PR, o Cloudflare Pages gera um preview deploy, e eu recebo o aviso no Telegram. Aprovar é simplesmente dar merge; rejeitar é fechar o PR ou pedir ajuste.

A arquitetura em 4 passos

  1. O N8N solicita o rascunho para a LLM API a partir de um tema/prompt que eu disparo.
  2. O N8N cria uma branch, escreve o markdown em src/content/blog/pt/<slug>.md e abre o Pull Request.
  3. O GitHub dispara um preview deploy no Cloudflare Pages, publicado como check run no próprio PR; o Telegram me avisa com o link.
  4. Eu reviso o preview e decido: merge (publica) ou fecho o PR (descarto ou peço ajuste).

O contrato de schema que protege a produção

O acoplamento entre o N8N e o Astro é exatamente um schema Zod validado no build. Um markdown malformado nunca chega à produção: o build do preview falha antes.

// Schema mínimo validado no build: contrato N8N -> Astro
const blogSchema = z.object({
  title: z.string().min(10).max(70),
  description: z.string().min(50).max(160),
  pubDate: z.coerce.date(),
  category: z.enum(['tech', 'ia', 'automacao']),
  draft: z.boolean().default(false),
});

O schema real, com todos os campos (tags validadas contra uma lista fechada, redirectFrom para redirecionar slugs antigos, translationKey para o par PT/EN futuro), vive em src/content/post-schema.mjs deste próprio repositório.

Um detalhe que vale registrar: essa mesma regra existe em dois lugares (o Zod do Astro e uma réplica em JavaScript dentro do N8N, que valida o frontmatter antes de sequer abrir o PR). Duas fontes de verdade para a mesma regra são um risco real de divergência silenciosa, então o build também roda um teste de contrato que passa o mesmo conjunto de payloads pelos dois validadores e falha se eles discordarem em qualquer caso.

O que eu mudaria hoje

Duas coisas ficaram claras montando o pipeline. Primeiro, um botão de aprovação inline no Telegram (que chamaria um webhook e faria o merge via API, sem abrir o GitHub) economizaria tempo real, mas ainda é etapa avançada, não o padrão inicial: o merge manual pelo GitHub mobile já cumpre o gate com uma fricção aceitável. Segundo, o preview deploy é o único ponto em que eu de fato leio o post inteiro antes de publicar; é ali que o gate humano realmente acontece, não no clique de merge em si.

Se você está montando algo parecido, o ponto que eu não pularia é o schema validado no build. É a diferença entre um erro de formatação virar um post publicado quebrado, ou nunca sair do preview.