-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Plano: Script de Extração de Reactions History do dev.to
Context
Daniel quer coletar dados de "Reactions History" das páginas de stats dos seus artigos no dev.to para automação em massa. Atualmente, o dev.to não expõe esses dados via API pública — a única forma de acessá-los é pela página /stats de cada artigo, que requer autenticação. O script será executado manualmente no console do browser quando ele estiver logado.
Motivação: Coletar dados de reações de múltiplos posts para análise/dashboard.
Formato de saída: JSON estruturado.
Estrutura DOM Descoberta
div.crayons-card.p-4.overflow-auto
├── h2.crayons-subtitle.mb-2 → "Reactions History"
└── div.fs-sm.py-2.flex.items-center (× N entradas, ex: 222)
├── div.mr-3.relative
│ ├── img.crayons-avatar.h-8.w-8 → avatar do usuário
│ └── img.crayons-avatar.absolute... → ícone da reação
└── div.flex-1.flex.items-center.justify-between
├── div → "readinglist\n by Username" (tipo + link do user)
└── div.fs-xs → "Mar 19" ou "Dec 26 '25" (data)
Tipos de Reação Mapeados
| Tipo | Ícone SVG |
|---|---|
like |
sparkle-heart.svg |
unicorn |
multi-unicorn.svg |
readinglist |
save.svg |
fire |
fire.svg |
raised_hands |
raised-hands.svg |
exploding_head |
exploding-head.svg |
Formato de Datas
- Ano corrente:
"Mar 19" - Ano anterior:
"Dec 26 '25"
Implementação
Arquivo a criar
Nenhum arquivo no projeto. O deliverable é um snippet JavaScript para console do browser.
Script — Funcionalidades
- Localizar a seção "Reactions History" pelo
h2com texto correspondente - Iterar todas as entradas (
div.fs-sm.py-2.flex.items-center) - Extrair de cada entrada:
reactionType— texto antes do\nno primeiro div filhousername— texto do<a>no primeiro div filhouserProfileUrl—hrefdo<a>userAvatarUrl—srcdo primeiro<img>date— texto dodiv.fs-xs, normalizado para data completa
- Extrair metadados do artigo (título da página, URL atual)
- Montar JSON estruturado:
{
"articleUrl": "https://dev.to/danielhe4rt/...",
"articleSlug": "data-engineering-101-...",
"extractedAt": "2026-03-18T...",
"totalReactions": 222,
"reactions": [
{
"type": "readinglist",
"username": "Daniel Reis",
"userProfileUrl": "https://dev.to/danielhe4rt",
"userAvatarUrl": "https://media2.dev.to/...",
"date": "Mar 19"
}
],
"summary": {
"like": 45,
"unicorn": 30,
"readinglist": 80,
"fire": 20,
"raised_hands": 15,
"exploding_head": 32
}
}- Copiar automaticamente para o clipboard via
navigator.clipboard.writeText() - Log no console com resumo (total, contagem por tipo)
Fluxo de uso para automação em massa
┌─────────────────────────────────┐
│ Abrir: /meu-post/stats │
│ (logado no dev.to) │
├─────────────────────────────────┤
│ Colar script no Console │
│ → Extrai reactions │
│ → Copia JSON pro clipboard │
│ → Mostra resumo no console │
├─────────────────────────────────┤
│ Repetir para cada artigo │
│ ou salvar como snippet do │
│ Chrome DevTools │
└─────────────────────────────────┘
Expected Behavior
Happy Path
- Given o usuário está logado no dev.to e na página
/article-slug/stats - Then o script extrai todas as reações, monta JSON, copia pro clipboard e loga resumo
Edge Cases
-
Given a página não tem seção "Reactions History" (artigo sem reações)
-
Then o script loga mensagem de aviso e retorna JSON com
reactions: [] -
Given data no formato do ano anterior (ex:
"Dec 26 '25") -
Then o script mantém a data como string original (sem tentar parsear, para evitar bugs de timezone)
-
Given o usuário não está na página de stats
-
Then o script loga erro informativo: "Execute este script na página /stats de um artigo do dev.to"
Script
(() => {
// Validação: está na página certa?
if (!window.location.pathname.endsWith('/stats')) {
console.error('❌ Execute este script na página /stats de um artigo do dev.to');
return;
}
// Encontrar a seção "Reactions History"
const headers = document.querySelectorAll('h2');
let rhHeader = null;
headers.forEach(h => {
if (h.textContent.trim() === 'Reactions History') rhHeader = h;
});
if (!rhHeader) {
const result = {
articleUrl: window.location.href.replace('/stats', ''),
articleSlug: window.location.pathname.split('/').filter(Boolean).pop(),
extractedAt: new Date().toISOString(),
totalReactions: 0,
reactions: [],
summary: {}
};
console.warn('⚠️ Nenhuma seção "Reactions History" encontrada.');
console.log(JSON.stringify(result, null, 2));
return;
}
const container = rhHeader.parentElement;
const entries = container.querySelectorAll('.fs-sm.py-2.flex.items-center');
const reactions = [];
const summary = {};
entries.forEach(entry => {
const imgs = entry.querySelectorAll('img');
const rightSide = entry.querySelector('.flex-1');
if (!rightSide) return;
const infoDivs = rightSide.children;
// Tipo de reação
const rawText = infoDivs[0]?.textContent.trim() || '';
const reactionType = rawText.split('\n')[0].trim();
// Usuário
const link = infoDivs[0]?.querySelector('a');
const username = link?.textContent.trim() || 'unknown';
const userProfileUrl = link?.href || '';
// Avatar
const userAvatarUrl = imgs[0]?.src || '';
// Data
const date = infoDivs[1]?.textContent.trim() || '';
reactions.push({
type: reactionType,
username,
userProfileUrl,
userAvatarUrl,
date
});
// Contagem por tipo
summary[reactionType] = (summary[reactionType] || 0) + 1;
});
const result = {
articleUrl: window.location.href.replace('/stats', ''),
articleSlug: window.location.pathname.split('/').filter(Boolean).pop(),
extractedAt: new Date().toISOString(),
totalReactions: reactions.length,
reactions,
summary
};
// Copiar para clipboard
navigator.clipboard.writeText(JSON.stringify(result, null, 2)).then(() => {
console.log('✅ JSON copiado para o clipboard!');
}).catch(() => {
console.warn('⚠️ Não foi possível copiar para o clipboard. JSON abaixo:');
});
// Log no console
console.log(`📊 ${result.articleSlug}`);
console.log(` Total: ${result.totalReactions} reações`);
Object.entries(result.summary).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
console.log(JSON.stringify(result, null, 2));
return result;
})();Verificação
- Abrir a página
https://dev.to/danielhe4rt/data-engineering-101-a-real-beginners-approach-25a8/stats - Colar o script no console
- Verificar que o JSON foi copiado para o clipboard (Ctrl+V em um editor)
- Confirmar que
totalReactionsbate com o número exibido na página - Confirmar que o
summarysoma corretamente