Carregando anúncio...

Widget de Posts Recentes por Categoria para Blogger

 

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:

xml
<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>:

css
/* ========================================
   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>:

javascript
<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

  1. Acesse o Painel do BloggerLayout
  2. Clique em "Adicionar um gadget" na seção desejada (sidebar recomendado)
  3. Escolha HTML/JavaScript
  4. Cole o código da Seção 1 (XML do Widget)
  5. Salve

Passo 2: Adicionar o CSS

  1. Vá em TemaEditar HTML
  2. Procure por </b:skin> (quase no final do CSS)
  3. ANTES desta tag, cole o código da Seção 2 (CSS)
  4. Salve

Passo 3: Adicionar o JavaScript

  1. No mesmo editor de HTML, procure por </body>
  2. ANTES desta tag, cole o código da Seção 3 (JavaScript)
  3. 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:

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! 🚀