Voltar para todos os artigos
Nós Construímos uma Ferramenta de Auditoria de SEO Técnico. Então a Apontamos Para Nós Mesmos.

Nós Construímos uma Ferramenta de Auditoria de SEO Técnico. Então a Apontamos Para Nós Mesmos.

Um motor de SEO de 32 verificações com Node.js e SSE, as armadilhas de SSRF que ninguém te avisa, e o que aconteceu quando fizemos 'dogfooding' no nosso...

Pesquisa técnica projetada por humanos, sintetizada com assistência de personas de IA.
20 min de leitura

TL;DR / Sumário Executivo

Um motor de SEO de 32 verificações com Node.js e SSE, as armadilhas de SSRF que ninguém te avisa, e o que aconteceu quando fizemos 'dogfooding' no nosso...

💡 TL;DR (Resumo Executivo)

Principais pontos em 60 segundos:

  1. A Ferramenta: Construímos um auditor de SEO técnico com 32 verificações e 5 categorias, usando um executor TypeScript e um parser HTML baseado em regex. Zero dependências de parsing externas. A auditoria completa leva cerca de 600ms.
  2. O Stream: A versão web utiliza Server-Sent Events (SSE) para transmitir os resultados das verificações em tempo real — sem WebSocket, sem Socket.io, sem polling. A API nativa EventSource do navegador cuida de tudo.
  3. A Armadilha: Quando seu servidor busca URLs fornecidas pelo usuário, você construiu um proxy aberto. Gastamos mais tempo na proteção contra SSRF (6 camadas de validação, incluindo defesa contra DNS rebinding) do que em todo o motor de pontuação.
  4. O Dogfooding: A primeira execução contra a nossa própria página de ferramenta marcou 65 / 100 — REGULAR. A infraestrutura (hreflang, sitemap) estava perfeita. O SEO de nível de página (schema, estrutura) foi catastrófico — 0% em JSON-LD porque ele simplesmente não existia.
  5. A Tese: O SEO de infraestrutura é sem estado e declarativo — quando quebra, você percebe. O SEO de nível de página é embutido em componentes React e invisível por padrão. É por isso que você precisa de verificações automatizadas.

A Arquitetura

Existe um tipo específico de silêncio que acontece quando você executa sua própria ferramenta de diagnóstico contra seu próprio site de produção e o terminal imprime SCORE: 65 / 100 — REGULAR.

Não "bom". Não "excelente". Regular. O tipo de nota que significa "tecnicamente passando, mas ninguém está impressionado".

Esta é a história da criação dessa ferramenta — um auditor de SEO técnico com 32 verificações e 5 categorias — e o que ele encontrou quando o voltamos para dentro. As decisões de arquitetura, as armadilhas de segurança e o abismo entre "temos um sitemap" e "nosso SEO realmente funciona".

Por Que as Ferramentas Existentes Deixaram a Desejar

Se você já implantou um site Next.js com internacionalização e depois abriu o Google Search Console para descobrir seu locale em hindi indexado enquanto seu conteúdo real em português está na posição 42, você entende a frustração.

O Lighthouse mede performance e acessibilidade, mas não sabe o que é hreflang. O PageSpeed Insights se preocupa com Core Web Vitals, mas não vai te avisar que sua página de ferramenta está sem um <h1> porque o componente React renderiza o título como uma <div> estilizada. O Teste de Resultados Avançados do Google valida JSON-LD uma página por vez, manualmente. O Screaming Frog faz tudo, por £199/ano, e requer um app desktop que executa um rastreamento completo quando tudo o que você precisa é saber se "meu último deploy quebrou o hreflang?".

Verified SourcePreços Oficiais Screaming Frog

A licença do Screaming Frog SEO Spider custa £199/ano por usuário.

Precisávamos de algo específico: uma ferramenta que um desenvolvedor executa após o git push para verificar se o SEO técnico não regrediu. Não um crawler. Não uma plataforma de monitoramento. Um instantâneo de diagnóstico.

Então, construímos um.

O Stack: Quatro Arquivos Que Fazem Tudo

Todo o motor é composto por quatro arquivos TypeScript com clara separação de responsabilidades:

ArquivoLOCResponsabilidade
runner.ts563Motor central — 32 verificações em 5 categorias, callbacks de progresso
parser.ts240Parser baseado em Regex — extrai título, canonical, hreflang, OG, H1/H2, JSON-LD, imagens, links
fetcher.ts103Cliente HTTP com timeout (8s), limite de tamanho (2MB), cache
url-validator.ts111Proteção SSRF — 6 camadas de validação incluindo resolução de DNS

Total de ~1.000 linhas de TypeScript. Sem frameworks de teste. Sem bibliotecas de parsing externas. Sem binários de navegador.

Cada verificação é uma função tipada dentro do executor que recebe um fetcher e um parser, executa sua lógica e retorna um CheckResult:

typescript
// Dentro de runner.ts — simplificado da implementação real const canonical = parser.canonical(html); if (!canonical) return { pass: false, details: 'Nenhuma tag <link rel="canonical"> encontrada' }; return { pass: true, details: `Canonical: ${canonical}` };

Adicionar uma nova verificação significa escrever uma função e registrá-la no mapa de categorias do executor. Sem arquivos de configuração, sem camadas de indireção — apenas funções TypeScript chamando funções TypeScript.

As 32 Verificações

Cinco categorias, cada uma testando uma dimensão diferente do SEO técnico:

CategoriaVerificaçõesO que ela captura
hreflang6Canonical ausente, locales órfãos, tags hreflang duplicadas
sitemap7Sitemap quebrado, robots.txt ausente, falta de lastmod
metadata8Títulos longos, H1 ausente, sem imagem OG
schema5JSON-LD ausente, tipos inválidos, marcação incompleta
structure6Sem hierarquia H2, falta de links internos, conteúdo raso

Cada verificação tem um peso (3–10 pontos). Se passar, recebe o peso total; se falhar, zero. A pontuação da categoria é a porcentagem de pontos ganhos. A pontuação global é a média aritmética das cinco categorias.

Isso é deliberadamente simples. Sem crédito parcial, sem categorias ponderadas, sem pontuações de confiança de aprendizado de máquina. Quando um desenvolvedor vê MTA-006: FAIL — Nenhuma tag H1 encontrada, há exatamente uma coisa a fazer a respeito.

Por Que Regex, e Não um Parser DOM

O parser consiste em 240 linhas de extração por regex direcionada. Nada de Cheerio, jsdom ou Playwright. Apenas padrões que correspondem às estruturas HTML específicas que importam para o SEO:

typescript
// parser.ts — extrai exatamente o que as verificações de SEO precisam export function parseHtml(html: string) { const title = html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim(); const canonical = html.match( /<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']+)["']/i )?.[1]; const h1s = [...html.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/gi)] .map(m => m[1].replace(/<[^>]+>/g, '').trim()); // ... mais 15 extratores }

Isso funciona porque os sinais de SEO residem em padrões HTML previsíveis e bem estruturados. Uma tag canonical é sempre <link rel="canonical" href="...">. Uma tag hreflang é sempre <link rel="alternate" hreflang="..." href="...">. O JSON-LD está sempre dentro de <script type="application/ld+json">. Você não precisa de uma árvore DOM completa para encontrá-los — você precisa de correspondência de padrões (pattern matching).

O trade-off é real: o regex não consegue lidar com HTML malformado ou profundamente aninhado. Mas estamos parseando a saída SSR — o HTML que o servidor envia na primeira requisição. Isso é exatamente o que o Googlebot vê. Se um componente React renderiza tags hreflang no lado do cliente após a hidratação, o Googlebot pode não as ver, e nossa ferramenta também não. Isso é uma funcionalidade, não um bug.

A recompensa: zero dependências externas no parser, nenhum inchaço de node_modules e o parsing é concluído em poucos milissegundos.


O Fetcher: Concorrência Sem Complexidade

A camada HTTP lida com três coisas que importam para uma ferramenta de diagnóstico: cache, controle de concorrência e medição de TTFB.

javascript
class Fetcher { constructor({ concurrency = 5, timeoutMs = 10000 }) { this.cache = new Map(); this.activeRequests = 0; this.queue = []; } async fetch(url, opts = {}) { const cacheKey = `${opts.method || 'GET'}:${url}`; if (this.cache.has(cacheKey)) return this.cache.get(cacheKey); return this._enqueue(url, opts); } }

A fila é um semáforo manual — sem biblioteca externa. Quando activeRequests < concurrency, a tarefa é executada imediatamente. Caso contrário, ela é enviada para a fila e retirada quando uma vaga abre. Isso evita que o localhost seja sobrecarregado durante o desenvolvimento e impede que o Firebase Hosting de produção nos limite.

O cache é indexado por método + URL. A mesma página buscada para verificações de hreflang e verificações de metadados acessa a rede apenas uma vez. A auditoria completa é concluída em 606 milissegundos contra um site real. Não é um erro de digitação.


Levando para a Web: SSE em Vez de REST

A versão CLI imprime os resultados conforme eles acontecem. Para a versão web, precisávamos da mesma experiência: o usuário vê cada verificação sendo concluída em tempo real, em vez de um spinner seguido por uma parede de dados.

Server-Sent Events (SSE) são o ajuste natural. O protocolo é simples — uma resposta HTTP com Content-Type: text/event-stream que o servidor mantém aberta, enviando eventos nomeados:

event: progress
data: {"phase":"hreflang","check":3,"total":6,"id":"HRF-003","status":"pass"}

event: progress
data: {"phase":"metadata","check":6,"total":8,"id":"MTA-006","status":"fail",
       "details":"Tag H1 não encontrada"}

event: category_done
data: {"id":"metadata","score":69,"label":"REGULAR"}

O Route Handler do Next.js transmite esses eventos conforme cada verificação é concluída:

typescript
// app/api/tools/seo-health-check/route.ts export async function GET(req: NextRequest) { const url = req.nextUrl.searchParams.get('url'); // ... validação, rate limiting ... const stream = new ReadableStream({ async start(controller) { const send = (event, data) => { controller.enqueue( encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) ); }; send('connected', { message: 'Auditoria iniciada' }); const report = await audit(url, { onCheckComplete: (p) => send('progress', p), onCategoryDone: (c) => send('category_done', c), }); send('complete', report); controller.close(); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' }, }); }

No cliente, um hook React envolve a API nativa EventSource:

typescript
function useAudit() { const [progress, setProgress] = useState([]); const [report, setReport] = useState(null); const startAudit = useCallback((url) => { const es = new EventSource( `/api/tools/seo-health-check?url=${encodeURIComponent(url)}` ); es.addEventListener('progress', (e) => { setProgress((prev) => [...prev, JSON.parse(e.data)]); }); es.addEventListener('complete', (e) => { setReport(JSON.parse(e.data)); es.close(); }); }, []); return { progress, report, startAudit }; }

Sem WebSocket, sem Socket.io, sem polling. O EventSource integrado do navegador lida com a reconexão automaticamente, embora para uma auditoria sub-segundo nunca tenhamos precisado disso.

Verified SourceMDN Web Docs — Server-Sent Events

SSE é um padrão W3C que permite que servidores enviem dados para clientes web via HTTP.

Por Que Não WebSocket?

O SSE é unidirecional: servidor → cliente. Isso é tudo de que precisamos. O cliente envia a URL uma vez (na query string) e o servidor transmite os resultados de volta. O WebSocket adiciona uma capacidade bidirecional que nunca usaríamos, ao custo de um protocolo mais complexo, batimentos cardíacos (heartbeats) manuais e a necessidade de lidar com o handshake de upgrade.

O SSE também funciona através de proxies HTTP e CDNs sem configuração especial. Quando você faz o deploy no Firebase Hosting com Cloudflare na frente, isso importa.


Segurança: Seu Servidor Agora é um Proxy

Aqui está a parte que a maioria dos tutoriais ignora.

Quando seu servidor busca URLs fornecidas pelos usuários, você construiu um proxy aberto. Sem proteção, alguém pode enviar http://169.254.169.254/latest/meta-data/iam/security-credentials/ e seu servidor buscará alegremente os metadados da instância do seu provedor de nuvem, incluindo credenciais IAM temporárias.

Isso é Server-Side Request Forgery (SSRF) e é a razão pela qual gastamos mais tempo na validação de URL do que em todo o motor de pontuação.

Seis Camadas de Validação

typescript
export async function validateUrl(input: string) { // 1. Parse — rejeita URLs malformadas const url = new URL(input); // lança erro se for inválida // 2. Protocol — apenas HTTP(S) if (!['http:', 'https:'].includes(url.protocol)) return { valid: false, error: 'Apenas HTTP e HTTPS são permitidos' }; // 3. Port — apenas 80/443 if (url.port && !['80', '443', ''].includes(url.port)) return { valid: false, error: 'Apenas as portas 80 e 443 são permitidas' }; // 4. Sem IPs literais — força a resolução de DNS if (isIP(url.hostname)) return { valid: false, error: 'Endereços IP não são permitidos' }; // 5. Hosts bloqueados — endpoints de metadados de nuvem if (BLOCKED_HOSTS.includes(url.hostname.toLowerCase())) return { valid: false, error: 'Hostname bloqueado' }; // 6. Resolução de DNS — verifica se o IP resolvido é público const addresses = await dns.resolve4(url.hostname); for (const ip of addresses) { if (isPrivateIP(ip)) return { valid: false, error: 'Resolve para IP privado' }; } return { valid: true, url }; }

A Camada 6 é a crítica. Um invasor pode registrar um domínio que resolve para 169.254.169.254, ignorando as verificações de hostname. Ao revalidar após a resolução do DNS, capturamos ataques de DNS rebinding.

Verified SourceOWASP — Prevenção de Server-Side Request Forgery

A OWASP recomenda validar os endereços IP resolvidos após a busca no DNS, não apenas os hostnames, para defender contra DNS rebinding.

Também aplicamos:

  • Rate limiting: 3 auditorias por IP por hora, 20 globais por hora, no máximo 2 simultâneas.
  • Timeouts: 8s por busca, 45s no total.
  • Limites de corpo: 2MB por resposta (um sitemap não deve ser maior que isso).

O Dogfooding: Pontuação 65

O momento da verdade. Apontamos a ferramenta para a sua própria página — gsstk.gem98.com/pt-BR/tools/seo-tools/seo-health-check — e obtivemos isto:

  ▸ hreflang    100%  ██████████████████  EXCELENTE
  ▸ sitemap     100%  ██████████████████  EXCELENTE
  ▸ metadata     69%  ████████████░░░░░░  REGULAR
  ▸ schema        0%  ░░░░░░░░░░░░░░░░░░  CRÍTICO
  ▸ structure    55%  ██████████░░░░░░░░  REGULAR

  SCORE: 65 / 100  REGULAR
  21 passaram · 6 falharam · 5 puladas

Duas categorias perfeitas. Uma no zero. A história do porquê é mais instrutiva do que os números.

O Que Acertamos: A Camada de Infraestrutura

Hreflang e sitemap marcaram 100%. Mas chegar lá não foi o processo tranquilo que a pontuação sugere.

Oito meses atrás, tínhamos um sitemap que retornava dados binários porque o Firebase Hosting estava servindo o XML com o cabeçalho Content-Type errado. Antes disso, tínhamos um sitemap.xml que estava truncado — um limite de token em nosso script de geração cortou silenciosamente o arquivo em ~200 URLs, deixando centenas de páginas impossíveis de descobrir. E o AI Crawl Control da Cloudflare estava bloqueando bots que queríamos deixar passar, incluindo alguns crawlers de SEO.

O robots.txt passou por sua própria evolução. Uma versão anterior esqueceu de usar Disallow nos ghost locales — /hi/, /en-GB/, /fr/ — o que significava que o Googlebot estava rastreando diligentemente conteúdo traduzido por máquina que diluía nossa autoridade em 15 idiomas quando tínhamos conteúdo real apenas em dois.

Essas não foram falhas catastróficas. Foram o tipo de erosão lenta e invisível que acontece quando a infraestrutura é configurada uma vez e nunca mais é validada. O sitemap existia. O robots.txt existia. Eles apenas não funcionavam corretamente. Foi necessário construir um verificador automatizado para capturar as quebras sutis — e mesmo agora, a única razão pela qual estão em 100% é que corrigimos cada problema conforme a ferramenta os sinalizava ao longo de várias iterações.

A lição: ter um sitemap não é o mesmo que ter um sitemap correto. As verificações de infraestrutura passam hoje porque a automação capturou o que a revisão manual perdeu, repetidamente, ao longo de meses.

O Que Erramos: Todo o Resto

A categoria de metadados marcou 69%. As falhas foram pontuais, mas embaraçosas:

MTA-003: O título tem 61 caracteres (máximo recomendado: 60). Um caractere a mais. O título da página da ferramenta era "SEO Health Check — Verificador de SEO Técnico | gsstk" — 61 caracteres. Elaboramos cuidadosamente para densidade de palavras-chave e esquecemos de contar.

MTA-006: Nenhuma tag H1 encontrada. A página da ferramenta renderiza seu título dentro de um componente React que usa uma <div> estilizada em vez de um <h1> semântico. Visualmente idêntico. Semanticamente invisível para o Googlebot. Este é o tipo de bug que só existe no abismo entre "parece certo" e "está certo" — exatamente o abismo que um parser baseado em regex expõe porque vê a saída SSR, não a página estilizada.

MTA-008: Sem og:image. Construímos toda a infraestrutura de compartilhamento — título OG, descrição OG — e esquecemos a imagem. Todas as páginas de ferramentas do gsstk têm essa mesma lacuna. Uma correção global no componente de layout da ferramenta resolveria isso para todas as ferramentas de uma só vez.

A categoria schema marcou 0%. Zero. Não porque o JSON-LD estivesse errado — porque ele simplesmente não existia. Nenhum <script type="application/ld+json"> em lugar nenhum da página. Tínhamos planejado o esquema WebApplication no PRD, discutido os campos exatos (applicationCategory: "DeveloperApplication", offers.price: "0"), escrito os componentes — e nunca os implantamos. As verificações por JSON válido, contexto schema.org e validação de @type retornaram skip porque não havia nada para validar.

A categoria de estrutura (55%) revelou que a página da ferramenta era funcionalmente um app de página única sem conteúdo estático para o Googlebot indexar. Sem cabeçalhos H2. Sem links internos para outras ferramentas ou posts de blog. Sem nenhuma hierarquia de conteúdo. O componente React renderiza um formulário, espera pela entrada do usuário e mostra os resultados — nada disso existe na saída SSR. Para o Google, a página está praticamente vazia.


A Arquitetura da Cegueira

A pontuação de 65 conta uma história sobre dois tipos diferentes de dívida técnica.

A camada de infraestrutura (hreflang, sitemap, robots.txt) marcou 100% porque é sem estado e declarativa. Você escreve uma configuração, gera arquivos, faz o deploy. Quando quebra, quebra de forma visível — o Google Search Console te envia e-mails irritados sobre erros no sitemap. E quando você corrige, a correção é permanente.

O SEO de nível de página (metadados, schema, estrutura) marcou 41% em média porque está embutido em componentes e é invisível por padrão. Ninguém abre um componente React e verifica se o H1 é um <h1> real ou uma <div className="text-3xl font-bold">. Ninguém visualiza o código-fonte após cada deploy para verificar se o JSON-LD foi renderizado. Ninguém conta links internos na saída SSR.

É por isso que a ferramenta existe. Não para substituir o Google Search Console ou o Lighthouse, mas para capturar a classe de bugs que vivem no abismo entre o que o desenvolvedor vê no navegador e o que o motor de busca vê no HTML.


As Seis Correções

Cada falha da auditoria mapeia para uma mudança concreta e pequena:

VerificaçãoCorreçãoEsforço
MTA-003Encurtar o título em 1 caractere1 minuto
MTA-006Mudar de <div> para <h1> no componente de título1 minuto
MTA-008Adicionar og:image aos metadados de layout5 minutos
SCH-001Adicionar JSON-LD WebApplication às páginas30 minutos
STR-002Adicionar seções H2 estáticas abaixo do formulário1 hora
STR-004Adicionar links internos para ferramentas e blog1 hora

Esforço total estimado: ~3 horas. Pontuação projetada após: ≥ 90/100.

O ponto não é que esses sejam problemas difíceis. O ponto é que ninguém os notou por meses. A página funcionava perfeitamente. Os usuários podiam auditar URLs. Os componentes React estavam polidos. Mas a saída SSR — a versão que importa para o SEO — era um esqueleto.


O Que Vem a Seguir

O abismo entre o que a ferramenta verifica hoje e o que ela deveria verificar está principalmente na categoria schema. A versão atual verifica "o JSON-LD existe?", mas não "os campos obrigatórios estão presentes?" ou "o @type é um tipo válido do schema.org?". Quando alguém envia um esquema WebApplication sem um campo name, a ferramenta dá 100% no schema. Isso é enganoso. Expandir para a validação estrutural completa é a próxima prioridade.

Também faltam verificações entre páginas. Verificar se as tags hreflang são recíprocas (a página A aponta para a página B e a página B aponta de volta para a página A) requer a busca de páginas adicionais — o que significa mais latência e mais superfície de ataque para SSRF. Existe uma tensão de design entre a completude e a promessa de "uma URL em menos de um segundo".

Google Search Console: Cedo Demais para Dizer

Lançamos as correções de SEO em nível de página no mesmo dia em que publicamos este artigo. O ciclo de rastreamento do Google normalmente leva de dias a semanas para refletir mudanças estruturais como novo JSON-LD ou metadados reorganizados. Publicar números de "antes e depois" hoje significaria fabricar dados para se adequar a uma narrativa — e nós não fazemos isso aqui.

O que podemos dizer: a camada de infraestrutura (sitemap, hreflang, robots.txt) está estável e correta há semanas, depois de meses de correções iterativas capturadas pela ferramenta. As correções em nível de página (schema, estrutura, metadados) entraram no ar hoje.

Revisitaremos isso em 60–90 dias com números reais. Se os dados contarem uma história monótona, publicaremos a história monótona.


Experimente

A ferramenta está disponível em gsstk.gem98.com/tools/seo-tools/seo-health-check.

Cole uma URL. Obtenha uma pontuação em menos de um segundo.

32 verificações. 5 categorias. Sem necessidade de conta. Sem chave de API. Sem vendas casadas.

Se a sua pontuação for melhor que 65, você já está à frente de onde estávamos. Se for pior — bom, pelo menos agora você sabe exatamente o que corrigir.

Este artigo foi estruturado por humanos e sintetizado com o auxílio de IA sob a persona de Aether (IA).



Fontes Externas


Leituras Relacionadas no gsstk

Receba novos artigos

Cadastre-se para receber notificações sobre novos artigos direto no seu email

Não enviaremos spam. Você pode cancelar a inscrição a qualquer momento.