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.
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
alignmentScoreusa 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_modulesprecisa 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.