← Voltar ao Blog
· 20 min leitura ·

SEO Case Study: Do zero ao topo do Google — Parte 2: Execução, ferramentas e primeiros resultados

Documentação da fase de execução: ferramentas de monitoramento automatizado (Puppeteer + MongoDB), expansão de keywords de 10 para 55, otimizações on-page e estratégia de cibersegurança — com código real e resultados medidos.

#seo#google#astro#puppeteer#mongodb#cibersegurança#case-study#posicionamento
Compartilhar

Este é o segundo artigo da série “Do zero ao topo do Google”. Na Parte 1, documentamos o diagnóstico inicial: zero presença no Google, baseline completo com screenshots, stack técnica, e 8 correções de SEO técnico que subiram o PageSpeed mobile de 58 para 87.

Agora vamos documentar a fase de execução — o que foi construído, otimizado e medido na primeira semana pós-diagnóstico.

O que mudou: Na Parte 1 tínhamos 16 páginas e zero ferramentas de monitoramento. Agora temos 78 páginas, 55 keywords monitoradas, um sistema automatizado de ranking com dashboard em tempo real, e uma estratégia de cibersegurança implementada.


O que foi construído: seo-tools

A primeira coisa que fiz após o diagnóstico foi criar ferramentas para medir automaticamente o que o Google retorna para nossas keywords-alvo. Sem medição, SEO é achismo.

O sistema tem 4 componentes:

seo-tools/
├── check-ranking.mjs    # Puppeteer scraper — simula buscas humanas no Google
├── dashboard.html        # Dashboard visual: gráficos, tabelas, screenshots
├── dashboard.mjs         # Express API (porta 3333) para servir dados
├── db.mjs                # Camada MongoDB — persistência e agregação
└── keywords.config.json  # 55 keywords configuráveis

1. Scraper de ranking: check-ranking.mjs

O coração do sistema. Um script Node.js que usa Puppeteer para abrir o Google em um browser real, digitar cada keyword como um humano faria, e extrair os resultados orgânicos.

Arquitetura anti-detecção:

// Rotação de User-Agent a cada sessão
const USER_AGENTS = [
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/134.0.0.0",
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/133.0.0.0",
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Firefox/135.0",
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Edg/134.0.0.0",
];

// Stealth patches — esconder que é automação
await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, "webdriver", { get: () => false });
  Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
  window.chrome = { runtime: {}, loadTimes: function(){}, csi: function(){} };
});

// Browser reinicia a cada 15 keywords para evitar fingerprinting
const RESTART_EVERY = 15;
if (i > 0 && i % RESTART_EVERY === 0) {
  await browser.close();
  ({ browser, page } = await launchBrowser());
}

Simulação humana:

O script não simplesmente carrega uma URL com ?q=keyword. Ele:

  1. Abre google.com e encontra a caixa de busca
  2. Digita a keyword caractere por caractere com delays aleatórios (25-85ms por letra)
  3. Espera 600-1000ms antes de pressionar Enter
  4. Rola a página em múltiplos passos aleatórios (200-500px cada)
  5. Às vezes rola para cima (60% de chance) para parecer navegação natural
  6. Screenshot full-page de cada SERP
  7. Delays aleatórios de 9.6-19.2s entre buscas (base 12s, ±40%)
  8. Pausa extra a cada 5 keywords (~5-10s adicional)
// Digitação humana — caractere por caractere
for (const char of keyword) {
  await page.keyboard.type(char, { delay: 0 });
  await sleep(25 + Math.random() * 60);
}
await sleep(600 + Math.random() * 400);
await page.keyboard.press('Enter');

// Google usa AJAX — não podemos esperar navegação tradicional
await page.waitForSelector('#search, #rso, #botstuff', { timeout: 12000 });

Detecção e recuperação de CAPTCHA:

// Quando o Google detecta automação e mostra CAPTCHA
if (block.captcha) {
  console.error("🚫 CAPTCHA detectado! Aguardando resolução manual...");
  // 4 tentativas com 30s de espera cada (2 min total)
  for (let wait = 0; wait < 4; wait++) {
    await sleep(30000);
    const retryBlock = await checkForBlock(page);
    if (!retryBlock.captcha) {
      console.log("✅ CAPTCHA resolvido!");
      break;
    }
  }
  // Se persistir, reinicia browser com novo UA e continua
  await browser.close();
  ({ browser, page } = await launchBrowser());
}

Extração de resultados:

O Google muda os seletores do HTML frequentemente. O scraper usa múltiplos seletores com fallbacks para funcionar mesmo quando o Google atualiza o layout:

// Seletores primários (2025-2026)
const containers = document.querySelectorAll(
  '#search .g, #rso .g, #rso [data-sokoban-container], ' +
  '#rso .tF2Cxc, #rso .MjjYud .g, #rso .N54PNb, #rso .kb0PBd'
);

// Fallback 1: qualquer link com <h3>
document.querySelectorAll('#rso a[href^="http"]').forEach(a => {
  const h3 = a.querySelector('h3');
  if (h3) results.push({ url: a.href, title: h3.textContent });
});

// Fallback 2: busca mais ampla
document.querySelectorAll('a[href^="http"]').forEach(a => {
  const h3 = a.querySelector('h3');
  if (h3 || a.closest('[data-sokoban-container]')) { /* ... */ }
});

2. Camada MongoDB: db.mjs

Cada scan gera dados que precisam ser persistidos para comparação ao longo do tempo. O MongoDB armazena:

// 3 coleções com índices otimizados
await db.collection("scans").createIndex({ date: -1 });
await db.collection("rankings").createIndex({ keyword: 1, date: -1 });
await db.collection("screenshots").createIndex({ scanId: 1, keyword: 1 });
ColeçãoConteúdoTamanho típico
scansMetadados de cada execução (data, status, contadores)~1 KB/scan
rankingsPosição de cada keyword em cada scan~55 docs/scan
screenshotsScreenshots PNG binários das SERPs~200 KB/screenshot

Agregação para histórico:

O pipeline de agregação mais útil é o que mostra a evolução de cada keyword ao longo dos scans:

export async function getAllKeywordsLatest() {
  return db.collection("rankings").aggregate([
    { $sort: { date: -1 } },
    {
      $group: {
        _id: "$keyword",
        latestPosition: { $first: "$position" },
        latestDate: { $first: "$date" },
        history: {
          $push: {
            position: "$position",
            date: "$date",
            scanId: "$scanId",
          },
        },
      },
    },
    { $sort: { _id: 1 } },
  ]).toArray();
}

E a comparação entre dois scans:

export async function compareScans(scanId1, scanId2) {
  const [rankings1, rankings2] = await Promise.all([
    getRankingsByScan(scanId1),
    getRankingsByScan(scanId2),
  ]);

  const map1 = new Map(rankings1.map(r => [r.keyword, r]));
  const map2 = new Map(rankings2.map(r => [r.keyword, r]));

  for (const keyword of allKeywords) {
    // Calcula: up, down, stable, new, lost
    const change = prevPos - currPos; // positivo = melhorou
    const status = change > 0 ? 'up' : change < 0 ? 'down' : 'stable';
  }
}

3. Dashboard: dashboard.html + dashboard.mjs

O dashboard é uma SPA com Canvas para gráficos e Express como API backend.

5 abas funcionais:

AbaFunção
🏆 RankingsTabela com posição, página, URL e link para screenshot
📈 EvoluçãoCanvas chart com linhas por keyword + sparklines na tabela
📸 ScreenshotsGrid de screenshots das SERPs com modal fullscreen
⚖️ CompararSeleciona dois scans e mostra diferença por keyword (↑↓🆕💀)
🔧 MelhoriasLista priorizada de otimizações SEO pendentes

O gráfico Canvas renderiza com devicePixelRatio para ficar nítido em telas Retina:

const dpr = window.devicePixelRatio || 1;
canvas.width = displayW * dpr;
canvas.height = displayH * dpr;
canvas.style.width = displayW + 'px';
canvas.style.height = displayH + 'px';
ctx.scale(dpr, dpr);

API endpoints:

GET /api/scans                          # Lista todos os scans
GET /api/scans/:id/rankings             # Rankings de um scan específico
GET /api/scans/:id/screenshots          # Lista screenshots de um scan
GET /api/screenshots/:scanId/:keyword   # Imagem PNG de um screenshot
GET /api/overview                       # Agregação de todas as keywords
GET /api/compare/:id1/:id2              # Comparação entre dois scans

Expansão de keywords: 10 → 42 → 55

A estratégia de keywords evoluiu em 3 fases:

Fase 1: Keywords iniciais (10)

Na Parte 1, definimos 10 keywords focadas em marca pessoal:

rafael cavalcanti da silva, rafael cavalcanti, rafaelroot,
rafael root, rafael cavalcanti desenvolvedor, rafael cavalcanti fullstack,
rafael cavalcanti segurança, rafael cavalcanti devops,
rafael cavalcanti security specialist, rafael cavalcanti python

Fase 2: Expansão para 42

Análise dos artigos do blog revelou que tínhamos conteúdo para ranquear em nichos técnicos específicos, não só marca pessoal. Adicionamos keywords de:

  • Hardening Linux (7): hardening linux, hardening linux checklist, linux server hardening guide, etc.
  • Nginx (3): nginx reverse proxy go, nginx reverse proxy go production, etc.
  • Engenharia reversa (6): engenharia reversa android frida, frida android hooking, etc.
  • Profissional (4): desenvolvedor fullstack brasilia, especialista segurança informação brasil, etc.
  • Produtos (6): xpusher push ads, wsocket websocket sdk, cnab 240 python, etc.
  • SEO (2): seo case study google ranking, caso de estudo seo google posicionamento

Fase 3: Expansão para 55 (+13 cibersegurança)

Auditoria revelou que as palavras “cyber security” e “cibersegurança” apareciam zero vezes em todo o site — apesar de segurança ser uma competência central. Adicionamos:

"cyber security",
"cybersecurity specialist",
"cibersegurança",
"cibersegurança brasil",
"especialista cibersegurança",
"especialista cibersegurança brasil",
"cyber security specialist brazil",
"offensive security pentesting",
"segurança ofensiva pentesting",
"rafael cavalcanti cyber security",
"rafael cavalcanti go golang",
"rafael cavalcanti reverse engineering",
"rafael cavalcanti brasilia"

Estratégia de cibersegurança: implementação

O diagnóstico “zero sinais de cibersegurança” levou a uma implementação em 4 camadas:

1. Meta keywords (todos os 5 idiomas)

// src/i18n/pt-br.ts
meta: {
  keywords: "desenvolvedor fullstack, cibersegurança, pentesting,
    segurança ofensiva, segurança defensiva, hardening, ..."
}

// src/i18n/en.ts
meta: {
  keywords: "fullstack developer, cyber security, pentesting,
    offensive security, defensive security, hardening, ..."
}

Sim, o Google ignora <meta name="keywords">. Mas Bing, Yandex e Baidu ainda usam. Em um site multilíngue com 5 idiomas, vale a pena.

2. Tags dos artigos do blog

Cada artigo recebeu tags de segurança relevantes:

ArtigoTags adicionadas
Engenharia Reversa Android + Fridasegurança, cibersegurança, pentesting
Hardening Linux Segurança Defensivacibersegurança, cyber-security
Nginx Reverse Proxy Go (PT)segurança, hardening
Reverse Engineering Android + Frida (EN)security, cyber-security, pentesting
Linux Server Hardening Checklist (EN)cyber-security
Nginx Reverse Proxy Go (EN)security, hardening

3. JSON-LD knowsAbout (ambos layouts)

"knowsAbout": [
  "Web Development", "Fullstack Development",
  "Cyber Security", "Penetration Testing",
  "Offensive Security", "Defensive Security",
  "Linux Hardening", "Reverse Engineering",
  "..."
]

4. Sitemap com prioridades segmentadas

serialize(item) {
  const url = item.url;
  if (url === 'https://rafaelroot.com/') {
    item.priority = 1.0;          // Homepage
  } else if (url.match(/\/(en|pt|es|ru)\/$/)) {
    item.priority = 0.9;          // Locale homes
  } else if (url.match(/\/blog\/.+\//)) {
    item.priority = 0.9;          // Artigos (conteúdo principal)
    item.changefreq = 'monthly';
  } else if (url.match(/\/habilidades\/.+|\/skills\/.+/)) {
    item.priority = 0.85;         // Skill subpages
  } else if (url.match(/\/blog\/$/)) {
    item.priority = 0.85;         // Blog index
  } else {
    item.priority = 0.8;          // Seções
  }
  return item;
}

Expansão do site: 16 → 78 páginas

Na Parte 1 o site tinha 16 páginas. Agora tem 78. O que foi adicionado:

i18n completo com slugs internacionalizados

Cada seção do site agora tem URLs traduzidas:

Seçãopt-brenesru
Sobre/sobre/en/about/es/acerca/ru/обо-мне
Habilidades/habilidades/en/skills/es/habilidades/ru/навыки
Experiência/experiencia/en/experience/es/experiencia/ru/опыт
Blog/blog/en/blog/es/blog/ru/блог
Projetos/projetos/en/projects/es/proyectos/ru/проекты
Contato/contato/en/contact/es/contacto/ru/контакт

Páginas de skills individuais

30 novas páginas criadas dinamicamente — cada skill tem sua própria página com descrição detalhada:

/habilidades/python    →  /en/skills/python
/habilidades/go        →  /en/skills/go
/habilidades/docker    →  /en/skills/docker
/habilidades/linux     →  /en/skills/linux
... (30 skill subpages)

Impacto no SEO

╔══════════════════════════════════════════════════════════╗
║            EXPANSÃO DO SITE — Parte 1 → Parte 2         ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  Páginas indexáveis:     16 → 78  (+387%)                ║
║  Keywords monitoradas:   10 → 55  (+450%)                ║
║  Idiomas com URLs:        5        (mantido)             ║
║  Artigos do blog:         6        (mantido)             ║
║  Seções com URLs:         0 → 6 × 5 idiomas = 30        ║
║  Skill subpages:          0 → 30                         ║
║  Sitemap URLs:           16 → 78                         ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝

Mais páginas = mais URLs no sitemap = mais oportunidades de indexação = mais pontos de entrada para o tráfego orgânico. Cada skill subpage é otimizada para long-tail keywords como “rafael cavalcanti python” ou “rafael cavalcanti docker”.


Primeiro scan: baseline automatizado (9 de março de 2026)

O primeiro scan com o check-ranking.mjs foi executado no dia seguinte ao diagnóstico:

╔══════════════════════════════════════════════════════════════╗
║   SEO Ranking Checker v2 — Browser Visível + MongoDB        ║
╚══════════════════════════════════════════════════════════════╝

🎯 Domain: rafaelroot.com
🔑 Keywords: 55
📄 Max pages/keyword: 3 (top 30)
⏱  Delay: 12000ms

📋 Scan ID: [MongoDB ObjectId]

Resultados do primeiro scan

MétricaValor
Total de keywords55
Encontradas (domínio no top 30)0
Top 100
Top 300
Não encontradas55
CAPTCHAsSim — resolvidos com restart de browser

Todas as 55 keywords: não encontrado.

Isso era completamente esperado. O site tem 2 dias de vida. O Google ainda não indexou nenhuma página. O ciclo normal é:

  1. Dia 1-3: Sitemap submetido, Google começa a rastrear
  2. Dia 3-7: Primeiras páginas aparecem no índice
  3. Semana 2-4: Indexação estabiliza, primeiras impressões no Search Console
  4. Mês 1-3: Posições começam a se formar para keywords de baixa competição

O valor deste scan é ser o ponto zero automatizado. Quando rodemos o próximo scan, qualquer keyword que aparecer no top 30 representará progresso real e mensurável.

O que os competidores dominam

O scan também captura os top 10 resultados de cada keyword. Análise dos competidores:

KeywordTop 3 resultadosOportunidade
”rafael cavalcanti”LinkedIn, Instagram, EscavadorMédia — plataformas fortes mas conteúdo genérico
”rafael cavalcanti da silva”Jusbrasil, LinkedIn, G1Boa — conteúdo jurídico/negativo, não técnico
”rafaelroot”Nenhum resultado relevanteExcelente — keyword virgem
”hardening linux checklist”DigitalOcean, Red Hat, blogs BRDifícil — concorrência forte
”engenharia reversa android frida”Poucos resultados em PT-BRBoa — nicho com pouco conteúdo em português
”cibersegurança brasil”Portais de notícias, empresasDifícil — concorrência corporativa

Insight: As melhores oportunidades estão em:

  1. Keywords de marca (rafaelroot, rafael root) — sem competição
  2. Long-tail técnico em PT-BR (engenharia reversa android frida) — nicho com pouco conteúdo de qualidade
  3. Nome completo (rafael cavalcanti da silva) — competidores são conteúdo jurídico, que o Google pode considerar menos relevante que um portfólio profissional

Bugs encontrados e corrigidos no seo-tools

O primeiro scan revelou 5 bugs que foram corrigidos:

Bug 1: Illegal break statement (SyntaxError)

Uma chave } extra dentro do checkKeyword fechava o loop for prematuramente. O break ficava fora do loop — SyntaxError fatal.

Bug 2: waitForNavigation timeout

O script usava page.waitForNavigation() depois de pressionar Enter. Mas o Google usa AJAX para carregar resultados — a página não navega, ela atualiza via JavaScript. O waitForNavigation ficava travado até o timeout.

Correção: Mudamos para page.waitForSelector('#search, #rso, #botstuff') — espera os containers de resultado aparecerem no DOM, independente de navegação.

Bug 3: networkidle2 falhando

O waitUntil: 'networkidle2' espera ZERO requisições de rede por 500ms. Mas o Google faz requisições em background constantemente (telemetria, ads, suggestions). Nunca atinge idle.

Correção: Mudamos para domcontentloaded — espera só o DOM estar pronto, sem depender de rede.

Bug 4: Sessão de browser acumulando detecção

Usar o mesmo browser para 55 keywords seguidas acumula sinais de automação no fingerprint do browser. Resultado: CAPTCHAs a partir da keyword ~20.

Correção: Browser reinicia a cada 15 keywords com novo User-Agent:

async function launchBrowser() {
  const browser = await puppeteer.launch({
    args: [
      "--disable-blink-features=AutomationControlled",
      "--disable-infobars",
      "--disable-extensions",
    ],
    ignoreDefaultArgs: ['--enable-automation'],
  });
  const page = await browser.newPage();
  await page.setUserAgent(getRandomUA()); // UA diferente a cada sessão
  return { browser, page };
}

Bug 5: Dashboard chart vazio

O gráfico Canvas não renderizava porque todas as posições eram “não encontrado” (string, não número). O código tentava calcular Math.max() de um array vazio, resultando em NaN.

Correção: Estado vazio explícito no Canvas:

if (allHistory.length === 0) {
  ctx.fillStyle = '#8b949e';
  ctx.font = '16px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('Sem dados numéricos de posição para exibir.', W / 2, H / 2);
  ctx.fillText('Execute mais scans para ver a evolução.', W / 2, H / 2 + 25);
  return;
}

Otimizações on-page realizadas

Além da estratégia de cibersegurança, diversas otimizações on-page foram implementadas:

Remoção de nomes de organizações dos i18n

Todos os 5 arquivos de internacionalização continham nomes reais de empresas/organizações nos textos de experiência e descrições. Substituímos por referências genéricas:

// ANTES
"Atuou na Norte Energia S.A. como..."

// DEPOIS
"Atuou em empresa do setor de energia como..."

Motivo: evitar que o tráfego orgânico para essas organizações concorra com nossas páginas, e manter a privacidade profissional.

Logo e branding simplificados

  • Logo atualizado de rafael4root para Rafael Cavalcanti
  • Footer simplificado — removidas referências desnecessárias
  • Links de GitHub adicionados em cada projeto

Blog CSS corrigido na homepage

O arquivo blog.css era importado apenas na rota /blog, mas a homepage também mostra previews de artigos. Resultado: estilos de blog faltando na homepage. Corrigido adicionando import condicional.

Traduções de navegação corrigidas

Os links de skills na navegação mostravam texto em inglês mesmo em idiomas que não eram inglês. Corrigido com termos localizados em cada arquivo i18n.


Checklist da Parte 2

✅ Concluído

  • Ferramenta de ranking automatizada (check-ranking.mjs) com Puppeteer + anti-detecção
  • Dashboard visual com 5 abas (Rankings, Evolução, Screenshots, Comparação, Melhorias)
  • MongoDB como backend de persistência com agregação e comparação de scans
  • Expansão de keywords: 10 → 42 → 55
  • Estratégia de cibersegurança: meta keywords, blog tags, JSON-LD em 4 camadas
  • Sitemap com prioridades segmentadas (1.0 → 0.8)
  • i18n com slugs internacionalizados: 16 → 78 páginas
  • 30 skill subpages geradas dinamicamente
  • Primeiro scan executado — baseline de 55 keywords registrado
  • 5 bugs corrigidos no seo-tools (SyntaxError, timeout, CAPTCHA, chart, rede)
  • Remoção de nomes de organizações dos i18n
  • Logo e branding atualizados
  • Blog CSS e nav translations corrigidos
  • Google Search Console configurado + sitemap submetido

⬜ Pendente para a Parte 3

  • Segundo scan com dados reais (esperar indexação)
  • Configurar GA4 com PUBLIC_GA_ID
  • Otimizar Nginx (brotli, cache headers)
  • Internal linking entre posts do blog
  • Open Graph images únicas por artigo
  • Criar /about com schema.org Person completo
  • Link building técnico (GitHub, dev.to, Stack Overflow)
  • FAQ Schema nos artigos técnicos
  • Analisar dados reais do Search Console

Métricas atualizadas (9 de março de 2026)

╔══════════════════════════════════════════════════════════════╗
║              ESTADO ATUAL — 09/03/2026                        ║
╠══════════════════════════════════════════════════════════════╣
║                                                              ║
║  Total de páginas:           78 (+387% vs Parte 1)           ║
║  Keywords monitoradas:       55 (+450% vs Parte 1)           ║
║  Keywords no top 30:         0  (esperado — site tem 2 dias) ║
║  Scans executados:           1                               ║
║  PageSpeed Mobile:           87/100                          ║
║  PageSpeed Desktop:          90/100                          ║
║  SEO Score:                  100/100                         ║
║  Páginas indexadas (GSC):    Aguardando (~3-7 dias)          ║
║  Impressões orgânicas:       0 (aguardando indexação)        ║
║                                                              ║
║  Próxima ação: esperar indexação, executar scan #2           ║
║                                                              ║
╚══════════════════════════════════════════════════════════════╝

O que vem na Parte 3

No próximo artigo (previsto para semana 8), vamos documentar:

  1. Comparação scan #1 vs scan #N — quais keywords saíram do “não encontrado” para posições reais?
  2. Dados do Search Console — impressões, cliques, CTR, posição média por keyword
  3. Análise de indexação — quantas das 78 páginas o Google indexou? Quais foram priorizadas?
  4. Link building — backlinks construídos e seu impacto medido
  5. ROI — horas investidas vs resultados obtidos
  6. Checklist replicável — versão simplificada de tudo que fizemos para outros profissionais aplicarem

Este artigo faz parte da série “Do zero ao topo do Google”. Veja a Parte 1 (diagnóstico) e acompanhe a Parte 3 (em breve) para ver os resultados em tempo real.

Última atualização: 9 de março de 2026