
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...
✨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:
- 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.
- 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
EventSourcedo navegador cuida de tudo.- 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.
- 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.
- 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?".
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:
| Arquivo | LOC | Responsabilidade |
|---|---|---|
runner.ts | 563 | Motor central — 32 verificações em 5 categorias, callbacks de progresso |
parser.ts | 240 | Parser baseado em Regex — extrai título, canonical, hreflang, OG, H1/H2, JSON-LD, imagens, links |
fetcher.ts | 103 | Cliente HTTP com timeout (8s), limite de tamanho (2MB), cache |
url-validator.ts | 111 | Proteçã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:
// 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:
| Categoria | Verificações | O que ela captura |
|---|---|---|
hreflang | 6 | Canonical ausente, locales órfãos, tags hreflang duplicadas |
sitemap | 7 | Sitemap quebrado, robots.txt ausente, falta de lastmod |
metadata | 8 | Títulos longos, H1 ausente, sem imagem OG |
schema | 5 | JSON-LD ausente, tipos inválidos, marcação incompleta |
structure | 6 | Sem 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:
// 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.
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:
// 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:
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.
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
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.
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 puladasDuas 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ção | Correção | Esforço |
|---|---|---|
| MTA-003 | Encurtar o título em 1 caractere | 1 minuto |
| MTA-006 | Mudar de <div> para <h1> no componente de título | 1 minuto |
| MTA-008 | Adicionar og:image aos metadados de layout | 5 minutos |
| SCH-001 | Adicionar JSON-LD WebApplication às páginas | 30 minutos |
| STR-002 | Adicionar seções H2 estáticas abaixo do formulário | 1 hora |
| STR-004 | Adicionar links internos para ferramentas e blog | 1 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
- Screaming Frog SEO Spider Pricing: https://www.screamingfrog.co.uk/seo-spider/pricing/
- MDN — Server-Sent Events: https://developer.mozilla.org/pt-BR/docs/Web/API/Server-sent_events
- OWASP SSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- MDN — EventSource API: https://developer.mozilla.org/en-US/docs/Web/API/EventSource
Leituras Relacionadas no gsstk
- 87% dos Teus Pull Requests Gerados por IA Têm Vulnerabilidades de Segurança — Icarus, o ponto cego da revisão de código
- O Top 10 de Segurança Agentica da OWASP — Visão Geral — Athena, panorama completo de SSI (ASI)
- Trivy Cascade — Um Quase-Acidente na Cadeia de Suprimentos — Daedalus, quando seu scanner de segurança se torna o vetor de ataque
- Git Mañana — CRDTs, Local-First e o Futuro Assíncrono do Controle de Versão — Aether, padrões práticos de engenharia