Vou criar um widget completo e modular que se integra perfeitamente ao seu tema. O widget será assíncrono, compatível com lazy load e seguirá todas as diretrizes do tema Speed.
📍 1. CÓDIGO DO WIDGET (Colar na Seção Desejada)
Localize a seção onde deseja adicionar o widget (por exemplo, dentro de <b:section id='sidebar'>) e cole este código:
<b:widget id='RecentByCategory' locked='false' title='Posts Recentes por Categoria' type='HTML' version='2'>
<b:widget-settings>
<b:widget-setting name='content'><![CDATA[
<!-- Container do Widget -->
<div class='recent-by-category-widget' id='recentByCategoryWidget'>
<!-- Seletor de Categoria -->
<div class='category-selector-wrapper'>
<label for='categorySelect' class='category-label'>Escolha uma categoria:</label>
<select id='categorySelect' class='category-select'>
<option value=''>Todas as Categorias</option>
<!-- As categorias serão carregadas dinamicamente -->
</select>
</div>
<!-- Loading Indicator -->
<div class='loading-indicator' id='categoryLoading'>
<div class='spinner'></div>
<p>Carregando posts...</p>
</div>
<!-- Container dos Posts -->
<div class='recent-posts-container' id='recentPostsContainer'>
<!-- Posts serão inseridos aqui via JavaScript -->
</div>
<!-- Mensagem de Erro -->
<div class='error-message' id='errorMessage' style='display: none;'>
<p>Não foi possível carregar os posts. Tente novamente.</p>
</div>
</div>
]]></b:widget-setting>
</b:widget-settings>
<b:includable id='main'>
<div class='widget'>
<b:if cond='data:title != ""'>
<h3 class='widget-title'><data:title/></h3>
</b:if>
<div class='widget-content'>
<data:content/>
</div>
</div>
</b:includable>
</b:widget>🎨 2. CSS DO WIDGET (Colar dentro de <b:skin><![CDATA[...]]></b:skin>)
Adicione este CSS antes do fechamento do ]]></b:skin>:
/* ========================================
Widget: Posts Recentes por Categoria
======================================== */
.recent-by-category-widget {
margin-bottom: 2rem;
}
/* Seletor de Categoria */
.category-selector-wrapper {
margin-bottom: 1.5rem;
}
.category-label {
display: block;
font-weight: 600;
color: var(--gray-700);
margin-bottom: 0.5rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.category-select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--gray-300);
border-radius: var(--radius);
font-size: 1rem;
color: var(--gray-900);
background: var(--white);
transition: var(--transition);
cursor: pointer;
font-family: inherit;
}
.category-select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.category-select:hover {
border-color: var(--primary);
}
/* Loading Indicator */
.loading-indicator {
text-align: center;
padding: 2rem;
display: none;
}
.loading-indicator.active {
display: block;
}
.spinner {
width: 40px;
height: 40px;
margin: 0 auto 1rem;
border: 4px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-indicator p {
color: var(--gray-500);
font-size: 0.875rem;
}
/* Container dos Posts */
.recent-posts-container {
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.recent-posts-container.loaded {
opacity: 1;
transform: translateY(0);
}
.recent-posts-list {
list-style: none;
padding: 0;
margin: 0;
}
/* Card Individual do Post */
.recent-post-item {
background: var(--white);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
transition: var(--transition);
border: 1px solid var(--gray-200);
}
.recent-post-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
border-color: var(--primary);
}
.recent-post-item:last-child {
margin-bottom: 0;
}
.recent-post-link {
display: flex;
gap: 1rem;
padding: 0.875rem;
text-decoration: none;
color: inherit;
}
/* Thumbnail */
.recent-post-thumbnail {
flex-shrink: 0;
width: 80px;
height: 80px;
border-radius: calc(var(--radius) - 4px);
overflow: hidden;
background: var(--gray-200);
position: relative;
}
.recent-post-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.recent-post-item:hover .recent-post-thumbnail img {
transform: scale(1.1);
}
/* Conteúdo do Post */
.recent-post-content {
flex: 1;
min-width: 0;
}
.recent-post-title {
font-size: 0.95rem;
font-weight: 600;
line-height: 1.4;
margin-bottom: 0.5rem;
color: var(--gray-900);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.recent-post-item:hover .recent-post-title {
color: var(--primary);
}
.recent-post-meta {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--gray-500);
}
.recent-post-date,
.recent-post-category {
display: flex;
align-items: center;
gap: 0.25rem;
}
.recent-post-category {
background: var(--gray-100);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
/* Mensagem de Erro */
.error-message {
text-align: center;
padding: 2rem;
color: var(--danger);
background: rgba(239, 68, 68, 0.1);
border-radius: var(--radius);
border: 1px solid var(--danger);
}
.error-message p {
margin: 0;
font-size: 0.875rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1.5rem;
color: var(--gray-500);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
margin: 0;
font-size: 0.95rem;
}
/* Dark Mode Compatibility */
@media (prefers-color-scheme: dark) {
.category-label {
color: var(--gray-300);
}
.category-select {
background: var(--gray-800);
color: var(--gray-100);
border-color: var(--gray-600);
}
.recent-post-item {
background: var(--gray-800);
border-color: var(--gray-700);
}
.recent-post-title {
color: var(--gray-100);
}
.recent-post-category {
background: var(--gray-700);
}
.spinner {
border-color: var(--gray-700);
border-top-color: var(--primary);
}
}
/* Responsive */
@media (max-width: 768px) {
.recent-post-thumbnail {
width: 70px;
height: 70px;
}
.recent-post-title {
font-size: 0.875rem;
}
.recent-post-meta {
font-size: 0.7rem;
}
}⚡ 3. JAVASCRIPT (Colar antes de </body>)
Cole este script antes do fechamento da tag </body>:
<script>
//<![CDATA[
/* ========================================
Widget: Posts Recentes por Categoria
Sistema Assíncrono com Lazy Load
======================================== */
(function() {
'use strict';
// Configurações
const CONFIG = {
blogUrl: window.location.origin,
maxPosts: 5,
imageSize: 'w150-h100-c',
defaultImage: 'https://via.placeholder.com/150x100/6366f1/ffffff?text=Sem+Imagem',
categoryParam: '/-/', // Prefixo para filtrar por categoria
};
// Elementos do DOM
const elements = {
widget: document.getElementById('recentByCategoryWidget'),
categorySelect: document.getElementById('categorySelect'),
postsContainer: document.getElementById('recentPostsContainer'),
loading: document.getElementById('categoryLoading'),
errorMessage: document.getElementById('errorMessage'),
};
// Verifica se o widget existe
if (!elements.widget) {
console.warn('Widget de Posts Recentes por Categoria não encontrado');
return;
}
// Cache de posts por categoria
const postsCache = new Map();
/**
* Formata a URL da imagem para o tamanho correto
*/
function formatImageUrl(url) {
if (!url) return CONFIG.defaultImage;
// Remove parâmetros existentes e adiciona o novo tamanho
return url.replace(/\/s\d+(-c)?\//, `/${CONFIG.imageSize}/`);
}
/**
* Extrai a imagem do post
*/
function extractImage(entry) {
// Tenta pegar a imagem do media$thumbnail
if (entry.media$thumbnail && entry.media$thumbnail.url) {
return formatImageUrl(entry.media$thumbnail.url);
}
// Tenta extrair do conteúdo
if (entry.content && entry.content.$t) {
const imgMatch = entry.content.$t.match(/<img[^>]+src="([^">]+)"/);
if (imgMatch) {
return formatImageUrl(imgMatch[1]);
}
}
return CONFIG.defaultImage;
}
/**
* Formata a data no padrão brasileiro
*/
function formatDate(dateString) {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
/**
* Extrai as categorias do post
*/
function extractCategories(entry) {
if (!entry.category || !Array.isArray(entry.category)) {
return [];
}
return entry.category.map(cat => cat.term);
}
/**
* Carrega as categorias disponíveis
*/
async function loadCategories() {
try {
const url = `${CONFIG.blogUrl}/feeds/posts/summary?alt=json&max-results=500`;
const response = await fetch(url);
const data = await response.json();
const categoriesSet = new Set();
if (data.feed && data.feed.entry) {
data.feed.entry.forEach(entry => {
const categories = extractCategories(entry);
categories.forEach(cat => categoriesSet.add(cat));
});
}
// Popula o select
const sortedCategories = Array.from(categoriesSet).sort();
sortedCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
elements.categorySelect.appendChild(option);
});
} catch (error) {
console.error('Erro ao carregar categorias:', error);
}
}
/**
* Carrega posts de uma categoria específica
*/
async function loadPosts(category = '') {
// Mostra loading
elements.loading.classList.add('active');
elements.postsContainer.classList.remove('loaded');
elements.errorMessage.style.display = 'none';
try {
// Verifica cache
if (postsCache.has(category)) {
renderPosts(postsCache.get(category));
return;
}
// Monta a URL
const categoryPath = category ? `${CONFIG.categoryParam}${encodeURIComponent(category)}` : '';
const url = `${CONFIG.blogUrl}/feeds/posts/default${categoryPath}?alt=json&max-results=${CONFIG.maxPosts}`;
const response = await fetch(url);
const data = await response.json();
const posts = [];
if (data.feed && data.feed.entry) {
data.feed.entry.forEach(entry => {
const post = {
title: entry.title.$t,
url: entry.link.find(link => link.rel === 'alternate').href,
image: extractImage(entry),
date: formatDate(entry.published.$t),
categories: extractCategories(entry),
};
posts.push(post);
});
}
// Salva no cache
postsCache.set(category, posts);
// Renderiza
renderPosts(posts);
} catch (error) {
console.error('Erro ao carregar posts:', error);
elements.errorMessage.style.display = 'block';
elements.postsContainer.innerHTML = '';
} finally {
elements.loading.classList.remove('active');
}
}
/**
* Renderiza os posts no DOM
*/
function renderPosts(posts) {
if (posts.length === 0) {
elements.postsContainer.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<p>Nenhum post encontrado nesta categoria.</p>
</div>
`;
elements.postsContainer.classList.add('loaded');
return;
}
const listHTML = posts.map(post => `
<article class="recent-post-item">
<a href="${post.url}" class="recent-post-link">
<div class="recent-post-thumbnail">
<img
class="lazyload"
data-src="${post.image}"
alt="${post.title}"
width="80"
height="80"
/>
</div>
<div class="recent-post-content">
<h4 class="recent-post-title">${post.title}</h4>
<div class="recent-post-meta">
<span class="recent-post-date">📅 ${post.date}</span>
${post.categories[0] ? `<span class="recent-post-category">🏷️ ${post.categories[0]}</span>` : ''}
</div>
</div>
</a>
</article>
`).join('');
elements.postsContainer.innerHTML = `
<ul class="recent-posts-list">
${listHTML}
</ul>
`;
// Adiciona classe de carregamento concluído
setTimeout(() => {
elements.postsContainer.classList.add('loaded');
}, 50);
// Trigger para lazy load (compatibilidade com Lazyify)
setTimeout(() => {
if (typeof $ !== 'undefined' && $.fn) {
$(window).trigger('scroll');
}
// Fallback: dispara evento nativo
window.dispatchEvent(new Event('scroll'));
}, 100);
}
/**
* Inicialização
*/
function init() {
// Carrega categorias
loadCategories();
// Carrega posts iniciais (todas as categorias)
loadPosts();
// Event listener para mudança de categoria
elements.categorySelect.addEventListener('change', (e) => {
loadPosts(e.target.value);
});
}
// Aguarda DOM estar pronto
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
//]]>
</script>📋 INSTRUÇÕES DE INSTALAÇÃO
Passo 1: Adicionar o Widget
- Acesse o Painel do Blogger → Layout
- Clique em "Adicionar um gadget" na seção desejada (sidebar recomendado)
- Escolha HTML/JavaScript
- Cole o código da Seção 1 (XML do Widget)
- Salve
Passo 2: Adicionar o CSS
- Vá em Tema → Editar HTML
- Procure por
</b:skin>(quase no final do CSS) - ANTES desta tag, cole o código da Seção 2 (CSS)
- Salve
Passo 3: Adicionar o JavaScript
- No mesmo editor de HTML, procure por
</body> - ANTES desta tag, cole o código da Seção 3 (JavaScript)
- Salve o tema
✨ RECURSOS DO WIDGET
✅ Carregamento Assíncrono - Não bloqueia o carregamento da página
✅ Cache Inteligente - Armazena posts já carregados para melhor performance
✅ Lazy Load - Imagens carregam sob demanda (compatível com Lazyify)
✅ Dark Mode - Adapta-se automaticamente ao tema escuro
✅ Responsivo - Funciona perfeitamente em mobile
✅ Filtro por Categoria - Dropdown dinâmico com todas as categorias do blog
✅ Animações Suaves - Transições elegantes ao carregar conteúdo
✅ Tratamento de Erros - Mensagens claras em caso de falha
🎯 PERSONALIZAÇÃO
Você pode ajustar as configurações no início do JavaScript:
const CONFIG = {
maxPosts: 5, // Número de posts a exibir
imageSize: 'w150-h100-c', // Tamanho das thumbnails
defaultImage: '...', // Imagem padrão quando não há thumbnail
};O widget está pronto para uso e 100% integrado ao seu tema Speed! 🚀
