amostra 01/em produção
Visa Contabilidade
Um site institucional que saiu do WordPress sem perder o que já ranqueava.
- Next
- MDX
- Zod
- Resend
01
Contexto
A Visa Contabilidade atua em Cascavel, no Paraná, desde 1985. Ao encerrar o contrato com a agência anterior, o escritório precisava migrar o site sem perder a presença que já tinha nas buscas.
O pedido era manter a identidade visual e deixar o site leve, com SEO local forte e preparado para buscas feitas por inteligência artificial.
02
Ponto de partida
Antes de escrever qualquer linha, o site antigo foi inventariado pela API pública do WordPress.
12
páginas institucionais
73
posts no blog, de 2020 a 2026
79
categorias de blog
0
dados estruturados ou meta descriptions
03
Linha do tempo
38 commits no histórico do projeto. Estes são os marcos.
20 de jul. de 2026
Inventário do site antigo
Levantamento de páginas, posts, categorias, rastreamento e integrações, e criação do projeto.
25 de jul. de 2026
Base, formulário e conteúdo
Páginas reconstruídas, formulário com camadas de proteção, redirecionamentos 301, avaliações reais do Google e o blog migrado para MDX.
28 de jul. de 2026
Site no ar
Troca para a nova hospedagem sem tirar o site do ar e sem interromper o e-mail corporativo.
30 de jul. de 2026
Medição de conversões
E-mail transacional do formulário e eventos de conversão para formulário e WhatsApp.
01 de ago. de 2026
Proteção reforçada
Desafio anti-bot com Turnstile, limite de requisições persistente e coordenadas exatas no schema para SEO local.
18 de ago. de 2026
Rodada de SEO
Metadados e schema revisados, ícones sociais e um mapa do site navegável.
08 de set. de 2026
Autoridade e acessibilidade
Registro no conselho de contabilidade no schema e ajustes de acessibilidade também lidos por agentes de IA.
04
Mapa de decisões
Reconstruído a partir do histórico de commits, do README e da documentação do projeto.
01
O blog virou código
Os 73 posts foram convertidos para MDX dentro do repositório por um script. O conteúdo deixou de depender do banco do WordPress e ganhou histórico e revisão.
02
Uma fonte única de verdade
Telefone, endereço, redes e navegação moram num único arquivo. Cabeçalho, rodapé, schema e metadados leem dele, então uma mudança acontece num lugar só.
03
Formulário como pilha de defesa
Cada envio passa por origem, limite de requisições, tamanho, validação, anti-bot e proteção contra injeção antes de virar e-mail.
04
Nenhum link perdido
36 redirecionamentos 301, cruzados com os dados do Search Console: todas as páginas que recebiam cliques continuaram respondendo.
05
SEO construído do zero
Dados estruturados para o escritório, perguntas frequentes, trilhas de navegação, posts e avaliações, com sitemap e robots liberando os rastreadores de IA.
06
Categorias com sentido
79 categorias foram consolidadas em 9 por um mapa não destrutivo. O conteúdo original ficou intacto.
05
Arquitetura
As mesmas cinco camadas desta página, aplicadas ao projeto.
000 m/interface
- Páginas estáticas no App Router
- Biblioteca de seções reutilizáveis
- Banner de consentimento próprio
030 m/rota
- Rota do formulário de contato
- Redirecionamentos 301
- CSP e cabeçalhos de segurança
060 m/serviço
- Schema Zod compartilhado entre cliente e servidor
- Provedor de e-mail plugável
- Verificação do Turnstile
090 m/repositório
- Conteúdo em MDX validado no build
- Mapa de categorias consolidadas
- Configuração institucional única
120 m/persistência
- Arquivos versionados no repositório
- Limite de requisições no Upstash Redis
06
Código
Trechos reais do repositório público.
O formulário como pilha de defesa
A ordem importa: as verificações mais baratas vêm primeiro, e cada uma devolve um erro genérico que não revela a regra para quem está atacando.
export async function POST(request: Request): Promise<Response> {
// CSRF: só aceita requisições do próprio site.
if (!isSameOrigin(request)) {
return NextResponse.json({ ok: false, error: "forbidden" }, { status: 403 });
}
// Rate limit por IP (durável via Upstash se configurado, in-memory senão).
const ip = clientIp(request.headers);
const rl = await contactRateLimiter.check(`contato:${ip}`);
if (!rl.success) {
return NextResponse.json(
{ ok: false, error: "rate_limited" },
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } },
);
}
// ... parse com teto de tamanho (evita payload gigante)
// Validação autoritativa no servidor.
const parsed = contactSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ ok: false, error: "invalid" }, { status: 422 });
}
const data = parsed.data;
// Anti-spam: honeypot + timing. Bot recebe "sucesso" falso (não vaza a regra).
if (looksLikeBot({ honeypot: data.company, renderedAt: data.renderedAt })) {
return NextResponse.json({ ok: true, id: "ignored" }, { status: 200 });
}
// ... Turnstile, proteção contra injeção de cabeçalho e envio
}Anti-spam sem serviço de terceiros
Um campo invisível que só robô preenche e um tempo mínimo de preenchimento. Resolve boa parte do spam sem depender de nenhum serviço externo.
/**
* Anti-spam sem terceiros:
* - honeypot: campo invisível que só bot preenche.
* - timing: formulário enviado rápido demais (< limiar) = bot.
*/
export function looksLikeBot(input: {
honeypot?: unknown;
renderedAt?: unknown;
minMs?: number;
}): boolean {
if (typeof input.honeypot === "string" && input.honeypot.trim() !== "") return true;
const minMs = input.minMs ?? 2500;
const renderedAt = Number(input.renderedAt);
if (Number.isFinite(renderedAt) && renderedAt > 0) {
if (Date.now() - renderedAt < minMs) return true;
}
return false;
}CSRF barrado pela origem
Um POST forjado em outro domínio chega com outra origem e é recusado antes de qualquer processamento. Sem origem nenhuma, também é recusado.
/**
* CSRF básico: só aceita requisições cuja Origin/Referer bate com o próprio site.
* Bloqueia POST forjado a partir de outro domínio.
*/
export function isSameOrigin(request: Request): boolean {
const origin = request.headers.get("origin");
const referer = request.headers.get("referer");
const allowed = new Set<string>([siteConfig.url]);
// ... origens de desenvolvimento liberadas fora de produção
const candidate = origin ?? (referer ? safeOrigin(referer) : null);
if (!candidate) return false;
return allowed.has(candidate.replace(/\/$/, ""));
}07
Resultado
108
páginas geradas como HTML estático
36
redirecionamentos 301
100%
das páginas com cliques preservadas
9
avaliações reais do Google com schema
Estado atual
Em produção desde 28 de julho de 2026, com manutenção contínua de SEO.