SEO Specialist: do crawl ao relatório estruturado

Como construir um especialista de SEO com Hermes Agent que rastreia páginas, identifica problemas técnicos, extrai keywords, analisa concorrentes e entrega um relatório estruturado — tudo via terminal. O agente é stateless: cada execução parte do zero, e o relatório é salvo com timestamp para criar uma linha do tempo sem banco de dados.


00 · Core Web Vitals 2026

Antes de construir o crawler, você precisa saber o que medir. O Google atualizou as métricas em 2024 — INP substituiu FID como Core Web Vital obrigatório.

Métrica Threshold Descrição
LCP · Largest Contentful Paint ≤ 2.5s Tempo até o maior elemento visível carregar. Medido no CrUX (usuários reais).
INP · Interaction to Next Paint ≤ 200ms Novo desde mar/2024 — substituiu o FID. Mede latência de todas as interações da sessão.
CLS · Cumulative Layout Shift ≤ 0.1 Estabilidade visual. Layouts que pulam quando imagens/carregam são penalizados.
TTFB · Time to First Byte ≤ 0.8s Tempo até o primeiro byte da resposta. Afeta diretamente o LCP.
FCP · First Contentful Paint ≤ 1.8s Primeiro conteúdo visível na tela. Indicador de percepção de velocidade.
LoAF · Long Animation Frames Experimental Métrica experimental que mede frames de animação longos — indica main thread bloqueada.

O nosso crawler mede TTFB (via tempo de resposta do axios) e FCP/LCP estimado (via tamanho do HTML e contagem de recursos). Para métricas reais de usuários, integre com a CrUX API (gratuita) ou o PageSpeed Insights API.


01 · O Problema

SEO profissional exige olhar múltiplo ao mesmo tempo: técnico, editorial, competitivo e de resultados. Fazer isso manualmente é impraticável além de algumas páginas.

Um auditor de SEO típico precisa verificar: status HTTP de cada página, estrutura de headings (H1 único, hierarquia H2-H6), meta tags (title ~60 caracteres, description ~160), links quebrados, imagens sem alt text, canonical tags, schema markup, tempo de resposta (TTFB ≤0.8s), e estabilidade visual (CLS ≤0.1). Depois vem a análise editorial: tópicos semânticos, entidades no conteúdo, alinhamento com o título. E por fim a comparação com concorrentes.

Fazer isso para um site de 50 páginas leva ~4 horas manualmente. Com o agente, leva menos de 2 minutos — e o relatório é estruturado, comparável e versionado.


02 · Arquitetura

O agente coordena três módulos especializados. Cada um é independente — você pode rodar só o crawler, só o keyword analyzer, ou o fluxo completo.

CLI hermes run args SEO Agent coordena fluxo crawl keywords competitors Crawler cheerio + axios Keyword Analyzer NLP + densidade Competitor gap analysis result Relatório markdown

Sólido = chamada síncrona. Tracejado = resultado agregado no relatório final.

O fluxo é simples: o CLI recebe a URL e parâmetros, passa para o agente, que orquestra os três módulos em sequência. Cada módulo retorna JSON estruturado. O agente consolida tudo em um relatório markdown e envia para o Telegram (ou salva em arquivo).


03 · Setup

Configure o agente no config.yaml. Sem dependências externas — tudo que o agente precisa já está disponível via tools do Hermes.

Passo 1: Configurar agente

Arquivo: ~/.config/hermes/config.yaml

Adicione o agente seo-specialist com system prompt e tools habilitadas. O prompt define o comportamento: crawler técnico, keyword analysis, competitor gap, e formatação do relatório.

# ~/.config/hermes/config.yaml
agents:
  seo-specialist:
    system_prompt: |
      Você é um especialista de SEO técnico.
      Suas tarefas:
      1. Crawlar a URL fornecida e extrair métricas técnicas
      2. Analisar tópicos semânticos do conteúdo (frequência, entidades, alinhamento)
      3. Comparar com concorrentes se fornecidos
      4. Gerar relatório markdown estruturado
      
      Regras (thresholds 2026):
      - Title: ~60 caracteres (Google reescreve com frequência; foque em relevância)
      - Meta description: ~160 caracteres
      - H1 deve ser único por página
      - TTFB ≤ 800ms, loadTime ≤ 3000ms
      - Keyword density NÃO é métrica relevante desde 2012 — foque em tópicos semânticos
      - Use cheerio para crawl estático, puppeteer/playwright para JS-heavy
    tools:
      - terminal
      - web
      - file
      - send_message
    model: claude-sonnet-4

Passo 2: Instalar dependências

Instale as dependências do crawler. O agente usa axios + cheerio para páginas estáticas, e puppeteer/playwright como fallback para SPAs.

# Dependências do crawler
npm install axios cheerio

# Opcional: para páginas com JavaScript
npm install puppeteer

# Alternativa moderna (multi-browser)
npm install playwright
npx playwright install chromium

# Para crawl em escala
npm install crawlee

# Teste rápido
node -e "require('axios'); require('cheerio'); console.log('OK')"

Passo 3: Testar conexão

Teste a conexão rodando o agente em uma URL simples. Isso valida que o config, as tools e as dependências estão corretos.

# Primeira execução — crawl apenas
hermes run seo-specialist   --url https://example.com   --mode crawl-only   --output terminal

04 · Módulo Crawler

O coração da auditoria. Cada página rastreada gera um snapshot estruturado com todas as métricas SEO relevantes. Crawl completo de página média em <2s com cheerio.

Fetch

O crawler usa axios para buscar a página e cheerio para parsear o HTML. O timeout é 10s — páginas mais lentas que isso já são um problema de SEO.

// seo-crawler.js
const axios = require('axios');
const cheerio = require('cheerio');

async function crawl(url) {
  const start = Date.now();
  const { data, status, headers } = await axios.get(url, {
    timeout: 10000,
    maxRedirects: 5,
    headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO-Bot/1.0)' }
  });
  const $ = cheerio.load(data);
  const loadTime = Date.now() - start;
  
  return { url, status, loadTime, $ };
}

Extract

Extração das métricas principais: title, meta description, headings, imagens, links, canonical. Cada métrica é validada contra regras de SEO.

// seo-crawler.js — extração de métricas
function extractMetrics($, url) {
  const title = $('title').text().trim();
  const metaDesc = $('meta[name="description"]').attr('content') || '';
  const h1s = $('h1').map((i, el) => $(el).text().trim()).get();
  const h2s = $('h2').map((i, el) => $(el).text().trim()).get();
  const images = $('img').map((i, el) => ({
    src: $(el).attr('src'),
    alt: $(el).attr('alt') || '',
    hasAlt: !!$(el).attr('alt')
  })).get();
  const links = $('a[href]').map((i, el) => ({
    href: $(el).attr('href'),
    text: $(el).text().trim(),
    isExternal: $(el).attr('href')?.startsWith('http')
  })).get();
  const canonical = $('link[rel="canonical"]').attr('href') || '';

  return { title, metaDesc, h1s, h2s, images, links, canonical };
}

Validate

Validação das regras de SEO. Cada problema é classificado como crítico, moderado ou oportunidade. O score geral é calculado a partir dessas regras.

// seo-crawler.js — validação
function validate(metrics, loadTime) {
  const issues = [];
  
  // Title
  if (!metrics.title) issues.push({
    severity: 'critical', rule: 'title-missing',
    message: 'Title tag ausente'
  });
  else if (metrics.title.length < 30 || metrics.title.length > 65) issues.push({
    severity: 'warning', rule: 'title-length',
    message: `Title tem ${metrics.title.length} chars (ideal: ~60)`
  });
  
  // H1
  if (metrics.h1s.length === 0) issues.push({
    severity: 'critical', rule: 'h1-missing',
    message: 'Nenhum H1 encontrado'
  });
  else if (metrics.h1s.length > 1) issues.push({
    severity: 'warning', rule: 'h1-multiple',
    message: `${metrics.h1s.length} H1s encontrados (deve ser 1)`
  });
  
  // Imagens sem alt
  const noAlt = metrics.images.filter(i => !i.hasAlt).length;
  if (noAlt > 0) issues.push({
    severity: 'warning', rule: 'img-no-alt',
    message: `${noAlt} imagens sem alt text`
  });
  
  // Load time (TTFB proxy)
  if (loadTime > 3000) issues.push({
    severity: 'warning', rule: 'slow',
    message: `Tempo de resposta: ${loadTime}ms (ideal: <1s)`
  });
  
  // Score: 100 - penalidades
  const penalties = issues.reduce((s, i) =>
    s + (i.severity === 'critical' ? 15 : i.severity === 'warning' ? 8 : 3), 0);
  const score = Math.max(0, 100 - penalties);
  
  return { score, issues };
}

04b · Engines de Crawl

Nem toda página é HTML estático. SPAs, frameworks modernos e conteúdo dinâmico exigem engines diferentes. Escolha a ferramenta certa para cada caso.

Cheerio · estático · <100ms

Use quando: a página entrega HTML completo no primeiro request (blogs, sites estáticos, landing pages). É a opção mais rápida e leve.

// Crawler com cheerio — modo padrão
const axios = require('axios');
const cheerio = require('cheerio');

async function crawlStatic(url) {
  const { data } = await axios.get(url, { timeout: 10000 });
  const $ = cheerio.load(data);
  return { title: $('title').text(), html: data.length };
}

// Fallback automático: se HTML < 1000 chars, tenta headless

Puppeteer · headless chromium · ~2-5s

Use quando: a página é SPA (React, Vue, Angular) ou carrega conteúdo via JavaScript após o load. Renderiza o DOM completo antes de extrair.

// Crawler com puppeteer — para SPAs
const puppeteer = require('puppeteer');

async function crawlSPA(url) {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });
  const html = await page.content();
  await browser.close();
  return { html: html.length, title: 'extraído do DOM' };
}

// Instalação: npm install puppeteer

Playwright · multi-browser · ~2-5s

Use quando: precisa testar em múltiplos browsers (Chromium, Firefox, WebKit) ou quer uma API mais moderna e estável que o Puppeteer. Recomendado para projetos novos.

// Crawler com playwright — multi-browser
const { chromium } = require('playwright');

async function crawlPlaywright(url) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });
  const html = await page.content();
  await browser.close();
  return { html: html.length };
}

// Instalação: npm install playwright
// Instala browsers: npx playwright install chromium

Crawlee · Apify SDK · profissional

Use quando: precisa de crawl em larga escala com filas, proxy rotation, storage e retry automático. Sucessor do Apify SDK, open-source.

// Crawler com Crawlee — profissional
const { CheerioCrawler } = require('crawlee');

const crawler = new CheerioCrawler({
  async requestHandler({ $, request }) {
    const title = $('title').text();
    console.log(`Título: ${title}`);
  },
});

await crawler.run(['https://example.com']);

// Instalação: npm install crawlee

Recomendação prática: Comece com axios + cheerio. Se o HTML retornado tiver menos de 1000 caracteres, faça fallback automático para puppeteer ou playwright. Para crawl em escala (1000+ páginas), migre para Crawlee.


05 · Módulo Keywords

Depois do crawl, o agente entende para quê a página quer rankear. Análise semântica feita automaticamente, baseada no conteúdo real — sem APIs externas. Keyword density é mito desde 2012; o que importa são tópicos, entidades e intenção de busca.

Limpeza

Remove tags HTML, stopwords em português, pontuação e palavras com menos de 3 caracteres. O resultado é um array de tokens limpos.

// keyword-analyzer.js
const STOPWORDS = new Set([
  'a','o','as','os','de','da','do','em','para','com',
  'um','uma','que','e','ou','se','na','no','por','mais',
  'como','mas','dos','das','ao','aos','sua','seu'
]);

function tokenize(text) {
  return text
    .toLowerCase()
    .replace(/[^\w\s]/g, ' ')
    .split(/\s+/)
    .filter(w => w.length >= 3 && !STOPWORDS.has(w));
}

Frequência e Tópicos

Conta a frequência de cada token e identifica os top 15 tópicos do conteúdo. Cada tópico é avaliado por: frequência absoluta, presença no title, e presença nos headings H2.

// keyword-analyzer.js — frequência e tópicos semânticos
function analyzeTopics(text, title, h2s) {
  const tokens = tokenize(text);
  const totalWords = tokens.length;
  
  // Frequência
  const freq = {};
  tokens.forEach(t => { freq[t] = (freq[t] || 0) + 1; });
  
  // Top 15 tópicos com contexto semântico
  const topics = Object.entries(freq)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 15)
    .map(([word, count]) => ({
      word, count,
      frequency: ((count / totalWords) * 100).toFixed(2),
      inTitle: title.toLowerCase().includes(word),
      inHeadings: h2s.some(h => h.toLowerCase().includes(word))
    }));
  
  return { totalWords, topics };
}

Alinhamento Semântico

Score de alinhamento entre tópicos do conteúdo e o title. Se o tópico principal não está no título nem nos H2, é um sinal de desalinhamento entre intenção do conteúdo e intenção do título.

// keyword-analyzer.js — score de alinhamento semântico
function alignmentScore(topics, title, h2s) {
  const titleWords = tokenize(title);
  const headingText = h2s.join(' ').toLowerCase();
  
  // Tópicos presentes no title OU nos headings
  const aligned = topics.filter(k =>
    titleWords.some(tw => tw.includes(k.word) || k.word.includes(tw)) ||
    headingText.includes(k.word)
  ).length;
  
  // Tópicos órfãos (só no body, não no title nem headings)
  const orphans = topics
    .filter(k => !titleWords.some(tw => tw.includes(k.word) || k.word.includes(tw)))
    .filter(k => !headingText.includes(k.word))
    .map(k => k.word);
  
  return {
    score: Math.round((aligned / Math.min(topics.length, 5)) * 100),
    orphans: orphans.slice(0, 5)
  };
}

Nota sobre keyword density: O Google não usa densidade de keyword como fator de ranking direto desde a era pré-Panda (2012). Em 2026, o foco é em tópicos semânticos, entidades (NLP/NER) e intenção de busca. A frequência de 1-2% pode ser um indicador de naturalidade, mas não é regra. O que importa é: o conteúdo responde à intenção da busca? Os tópicos principais estão no title e nos headings?


06 · Análise de Concorrentes

Para saber onde você está, precisa comparar com quem já está na frente. O agente levanta dados de 2-3 concorrentes e gera um relatório de gap lado a lado — sem APIs pagas.

A análise competitiva não usa APIs pagas. O agente faz crawl das páginas dos concorrentes (homepage + 2-3 páginas de produto/conteúdo) e compara métricas técnicas diretamente. Isso é limitado — não dá para ver backlinks reais ou autoridade de domínio — mas é suficiente para identificar gaps técnicos e de conteúdo.

Ferramentas gratuitas para análise competitiva

Antes de construir o crawler próprio, conheça o arsenal gratuito disponível:

# Google Search Console (dados reais do seu site)
# → Consultas, CTR, posições médias, páginas indexadas

# Google Sheets + IMPORTXML (dados de qualquer site)
=IMPORTXML("https://concorrente.com/pagina", "//title")
=IMPORTXML("https://concorrente.com/pagina", "//meta[@name='description']/@content")
=IMPORTXML("https://concorrente.com/pagina", "//h1")

# Screaming Frog (500 URLs free)
# → Crawl completo, análise técnica, export CSV

# PageSpeed Insights API (gratuito)
# → Core Web Vitals de qualquer URL
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=URL&key=***

# CrUX API (dados reais de usuários)
# → LCP, INP, CLS por origem/URL

# AlsoAsked / AnswerThePublic (limitado)
# → Perguntas "People Also Ask" para criar conteúdo

Relatório técnico comparativo

Métrica Seu site concorrente-a.com concorrente-b.com
Score Técnico 78/100 85/100 62/100
Tempo de Resposta 890ms 720ms 3400ms
H1 Único 2 H1s

Gap: -7 pts vs líder do grupo

Keywords gap

Keyword Você Conc. A Conc. B
marketing digital 2.1% 1.8% 0.3%
seo técnico 1.5% 2.2% 1.1%
automação 0.4% 1.9% 0.0%
agentes ia 3.2% 0.5% 0.6%
relatório seo 1.8% 2.0% 1.5%

Gap identificado: "automação" — você perde para A

O gap analysis é simples mas efetivo: para cada keyword que você e o concorrente compartilham, compara densidade. Keywords que o concorrente tem e você não são gaps de oportunidade. Keywords que você tem e o concorrente não são vantagens defensivas.


07 · Relatório Final

O agente compila tudo — crawl, keywords, concorrência — em um relatório markdown estruturado. O formato é projetado para ser lido rapidamente e comparado semanalmente.

Estrutura do relatório

Seção Conteúdo
1. Header URL, data, score
2. Score Breakdown 100 → penalidades
3. Issues crítico / warning / info
4. Keywords top 15 + densidade
5. Competitor Gap tabela comparativa
6. Action Items priorizados
7. Raw Data JSON completo

Formatos de saída: markdown · json · html

Preview do relatório

Item Valor
Score geral 78/100
Problemas Críticos 2
Title muito longo 72 chars
3 imagens sem alt seo-crawler.js:89
Warnings 3
Meta description curta 98 chars
Tempo resposta 2.1s
H2 sem hierarquia 3 orphans
Top Keywords
marketing digital 2.1% ✓
automação seo 1.8% ✓
hermes agent 3.5% ⚠ excesso

O relatório é salvo com timestamp no nome do arquivo (seo-report-2026-06-08T14-30-00.md), criando uma linha do tempo automática. Para comparar semanas, basta diff dos arquivos.


08 · Fluxo Completo

Do setup à execução completa em uma linha de comando — e como agendar para auditorias semanais automáticas.

Timeline

Tempo Etapa Detalhes
Min 0-5 Setup Configurar agente no config.yaml, instalar dependências (axios, cheerio), testar conexão com URL simples.
Min 5-10 Primeiro crawl Rodar o agente na URL principal. Verificar se o crawler extrai todas as métricas e se o score faz sentido.
Min 10-15 Adicionar concorrentes Incluir 2-3 URLs de concorrentes no comando. Verificar se o gap analysis identifica diferenças reais.
Min 15-20 Automatizar Criar cron job para auditoria semanal. Configurar entrega via Telegram para notificação automática.

Comando completo

Execução manual em uma linha. O modo full roda crawler + keywords + competitors + relatório.

# Execução completa
hermes run seo-specialist   --url https://seu-site.com   --competitors concorrente1.com,concorrente2.com   --mode full   --output markdown   --deliver telegram

# Cron job — auditoria toda segunda 9h
hermes cron create seo-weekly   --schedule "0 9 * * 1"   --agent seo-specialist   --args "--url https://seu-site.com --mode full --deliver telegram"

09 · Riscos & Mitigações

Risco Severidade Mitigação
Rate limiting: crawl agressivo pode bloquear seu IP no servidor alvo. ALTA Delay de 500ms entre requests, respeitar robots.txt, usar User-Agent identificável. Limitar a 10 páginas por execução.
Cheerio não executa JavaScript: SPAs e conteúdo dinâmico não são rastreados. MÉDIA Fallback para Puppeteer quando o HTML inicial tem <1000 chars. Flag --engine puppeteer força o modo headless.
Análise de concorrentes é limitada: sem dados reais de backlinks ou autoridade de domínio. MÉDIA Usar como indicador direcional, não absoluto. Complementar com dados do Search Console e ferramentas pagas (Ahrefs, SEMrush).
Keyword stuffing falso-positivo: páginas técnicas com muitas repetições de termos válidos. BAIXA Ignorar palavras dentro de <code> e <pre> na contagem. Ajustar threshold por tipo de página.

10 · Open Questions

Crawl recursivo ou apenas página única?

Para v1, crawl de página única é suficiente. Recursivo adiciona complexidade (gestão de fila, deduplicação, profundidade) que pode ser overkill para a maioria dos sites pequenos.

Decidir antes de: slice 3

Integrar com Google Search Console API?

A API gratuita do GSC fornece dados reais de impressões, CTR e posições médias. Isso elevaria o relatório de "indicador técnico" para "dados de performance real". Requer OAuth2 e autorização do proprietário do site.

Decidir com: SEO team, antes de slice 4


Key Files

  • ~/.config/hermes/config.yaml — Configuração do agente SEO Specialist com thresholds 2026.
  • seo-crawler.js — Crawler com axios + cheerio. Extrai métricas técnicas e valida CWV.
  • keyword-analyzer.js — Tokenização, frequência, tópicos semânticos e alinhamento (não density).
  • competitor-gap.js — Comparação lado a lado de métricas técnicas sem APIs pagas.
  • report-generator.js — Consolida JSON em markdown estruturado com timestamp.
  • crawler-engines/ — Cheerio, Puppeteer, Playwright, Crawlee — escolha por caso.

Gotchas

  • O alignmentScore usa tokenização simples. Palavras compostas ("marketing digital") podem não alinhar se o title tiver apenas "marketing". Considere n-grams para v2.
  • O score de 0-100 é linear: cada crítico = -15, warning = -8. Isso pode ser muito agressivo para sites grandes. Ajuste os pesos no validate().
  • Cron jobs do Hermes rodam em sessão limpa — o node_modules precisa estar no PATH ou usar caminho absoluto para os scripts.
  • Keyword density é mito: o Google não usa desde 2012. Foque em tópicos semânticos, entidades e intenção de busca.
  • INP substituiu FID em mar/2024. Seu crawler mede TTFB via axios; para INP/CLS reais, use CrUX API ou PSI API.

Publicado em andrecosta.dev.