Executando verificação de segurança...
25

Crie seu próprio Blog Pessoal usando API do TabNews! (HTML, CSS e JS Puros)

8 meses atrás <---
Github: https://github.com/Jeiel0rbit/jeiel-blog


E aí, comunidade TabNews! 👋

Já pensou em ter um blog pessoal exibindo seus próprios posts publicados aqui no TabNews, mas hospedado onde você quiser e com a sua cara? Hoje vou mostrar como fazer exatamente isso usando apenas HTML, CSS e JavaScript puro, consumindo a API pública do TabNews.

Recentemente, atualizei meu próprio blog (que você pode ver aqui usando essa abordagem, e o resultado é um site leve, rápido e que se atualiza automaticamente sempre que publico algo novo no TabNews.

Vamos ver como fazer? Baseei este tutorial no código que foi desenvolvido a 8 meses atrás! Dessa vez com sugestão do Rafael.

A Mágica por Trás: A API do TabNews

O TabNews oferece uma API REST pública bem simples e poderosa. Para o nosso blog, vamos usar principalmente dois endpoints:

  1. Listar posts de um usuário: https://www.tabnews.com.br/api/v1/contents/{username}
    • Este endpoint retorna uma lista de conteúdos (posts e comentários) de um usuário específico. Usaremos ele para exibir a lista inicial de posts no nosso blog. Adicionamos ?with_children=false para não trazer todos os comentários juntos e tornar a resposta mais leve inicialmente.
  2. Obter o conteúdo de um post específico: https://www.tabnews.com.br/api/v1/contents/{username}/{slug}
    • Quando o usuário clica em um título na lista, usamos este endpoint para buscar o conteúdo completo daquele post específico, incluindo o corpo em Markdown.

Estrutura Básica (HTML)

Nosso HTML será simples. Usaremos o framework CSS Bulma para facilitar a estilização e Font Awesome para ícones, mas você pode adaptar para o seu framework preferido ou usar CSS puro.

Sei, não é CSS tão puro assim...

Precisamos de:

  • Um <header> com o título do blog e talvez uma imagem de perfil.
  • Um <main> onde o conteúdo dinâmico será carregado.
    • Uma div para a lista de posts (#posts-list).
    • Uma div para exibir o detalhe de um post (#post-detail), inicialmente escondida.
    • Um campo de busca (#search-input).
    • Um botão "Carregar mais" (#load-more).
  • Um <footer> com informações de copyright e links.
  • Incluir a biblioteca marked.js para converter o Markdown do TabNews em HTML: <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Seu Blog com TabNews</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <style>
        /* Adicione os estilos do CSS fornecido ou seus próprios aqui */
        pre { position: relative; /* ... outros estilos ... */ }
        .copy-button { /* ... estilos ... */ }
        img { max-width: 100%; height: auto; }
        /* ... mais estilos (dark mode, etc) ... */
    </style>
</head>
<body>
    <header class="hero is-link is-small mb-5">
        <div class="hero-body">
            <div class="container">
                 <div class="level is-mobile">
                     <div class="level-left">
                         <div class="level-item">
                             <figure class="image is-48x48">
                                 <img src="sua-foto.png" alt="Foto de Perfil" class="is-rounded">
                             </figure>
                         </div>
                         <div class="level-item">
                              <h1 class="title is-2 has-text-white">Meu Blog</h1>
                         </div>
                     </div>
                     <div class="level-right">
                        <div class="level-item">
                           <button id="dark-mode-toggle" aria-label="Alternar modo escuro">🌙</button>
                        </div>
                     </div>
                 </div>
            </div>
        </div>
    </header>

    <main class="section pt-0">
        <div class="container">
            <div class="field search-wrapper mb-6">
                 <div class="control has-icons-left">
                     <input class="input is-medium" type="text" id="search-input" placeholder="Pesquisar por título...">
                     <span class="icon is-left"><i class="fas fa-search"></i></span>
                 </div>
            </div>

            <div id="posts-list" class="mb-5"></div>

            <div class="has-text-centered">
                 <button id="load-more" class="button is-info is-medium mt-4 is-hidden">
                     <span class="icon"><i class="fas fa-plus"></i></span>
                     <span>Carregar mais</span>
                 </button>
            </div>

            <div id="post-detail" class="is-hidden">
                 <button id="back-button" class="button is-link is-light mb-5">
                     <span class="icon"><i class="fas fa-arrow-left"></i></span>
                     <span>Voltar para a lista</span>
                 </button>
                 <div id="post-content"></div>
            </div>
        </div>
    </main>

    <footer class="footer has-background-grey-darker has-text-light mt-6">
       <div class="container">
         <p class="has-text-centered">&copy; 2025 Seu Nome</p>
         </div>
    </footer>

    <script>
        // Nosso JavaScript virá aqui
    </script>
</body>
</html>

Buscando e Exibindo Posts (JavaScript)

Agora, a parte principal. Dentro da tag <script> (ou em um arquivo .js separado):

  1. Variáveis e Inicialização: Pegamos referências aos elementos HTML e definimos algumas variáveis de estado.

    document.addEventListener('DOMContentLoaded', () => {
        const postsList = document.getElementById('posts-list');
        const postDetail = document.getElementById('post-detail');
        const postContent = document.getElementById('post-content');
        const backButton = document.getElementById('back-button');
        const loadMoreButton = document.getElementById('load-more');
        const searchInput = document.getElementById('search-input');
        const searchWrapper = document.querySelector('.search-wrapper');
        // Adicione as referências do dark mode se implementado
        let allFetchedPosts = []; // Guarda todos os posts buscados
        let posts = []; // Guarda os posts a serem exibidos (pode ser filtrado)
        let visiblePosts = 5; // Quantos posts exibir inicialmente/por página
        const POSTS_PER_PAGE = 5;
        const username = 'JeielMiranda'; // MUDE PARA SEU USUÁRIO TABNEWS!
    
        // ... funções virão aqui ...
    
        fetchPosts(); // Busca os posts ao carregar a página
    });
    
  2. Função WorkspacePosts: Busca a lista de posts do seu usuário.

    async function fetchPosts() {
        try {
            const response = await fetch(`https://www.tabnews.com.br/api/v1/contents/${username}?with_children=false`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const fetchedPosts = await response.json();
    
            allFetchedPosts = fetchedPosts
                .filter(post => post.status === 'published' && post.title && post.published_at && !post.parent_id) // Apenas posts publicados, com título, data e que são raiz (não comentários)
                .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); // Ordena do mais recente para o mais antigo
    
            posts = allFetchedPosts;
            renderPosts();
        } catch (error) {
            console.error('Erro ao buscar posts:', error);
            postsList.innerHTML = '<div class="notification is-danger has-text-centered">Erro ao carregar conteúdo.</div>';
        }
    }
    
    • Importante: Filtramos por status === 'published', title, published_at e !post.parent_id para garantir que estamos pegando apenas os posts principais que foram publicados e têm as informações necessárias.
  3. Função renderPosts: Exibe a lista de posts na tela.

    function renderPosts(filteredPosts = null) {
        postsList.innerHTML = ''; // Limpa a lista atual
        const postsToRender = (filteredPosts !== null ? filteredPosts : posts).slice(0, visiblePosts);
    
        postsToRender.forEach(post => {
            const postElement = document.createElement('article');
            postElement.className = 'box post-box mb-5'; // Usando classe 'box' do Bulma
    
            postElement.innerHTML = `
                <h2 class="title is-4 mb-2">
                    <a href="#" class="has-text-link post-link" data-slug="${post.slug}">${post.title}</a>
                </h2>
                <div class="post-meta level is-mobile mt-3">
                    <div class="level-left">
                        <div class="level-item">
                            <span class="icon-text">
                                <span class="icon"><i class="fas fa-calendar-alt"></i></span>
                                <span>${new Date(post.published_at).toLocaleDateString()}</span>
                            </span>
                        </div>
                    </div>
                    <div class="level-right">
                        <div class="level-item mr-3">
                            <span class="icon-text">
                                <span class="icon"><i class="fas fa-star"></i></span>
                                <span>${post.tabcoins ?? 0}</span>
                            </span>
                        </div>
                        <div class="level-item">
                            <span class="icon-text">
                                <span class="icon"><i class="fas fa-comments"></i></span>
                                <span>${post.children_deep_count ?? 0}</span>
                            </span>
                        </div>
                    </div>
                </div>
            `;
            postsList.appendChild(postElement);
        });
    
        // Lógica do botão "Carregar Mais"
        const sourceArray = filteredPosts !== null ? filteredPosts : posts;
        if (sourceArray.length > visiblePosts) {
            loadMoreButton.classList.remove('is-hidden');
        } else {
            loadMoreButton.classList.add('is-hidden');
        }
    
        // Adiciona evento de clique aos links dos posts
        document.querySelectorAll('.post-link').forEach(link => {
            link.addEventListener('click', (e) => {
                e.preventDefault();
                const slug = e.target.getAttribute('data-slug');
                showPostDetail(slug);
            });
        });
    }
    
  4. Função showPostDetail: Busca e exibe o conteúdo de um post específico.

    async function showPostDetail(slug) {
        try {
            const response = await fetch(`https://www.tabnews.com.br/api/v1/contents/${username}/${slug}`);
             if (!response.ok) {
                 throw new Error(`HTTP error! status: ${response.status}`);
             }
            const post = await response.json();
    
            if (!post || !post.body) {
                 postContent.innerHTML = '<div class="notification is-warning">Conteúdo não disponível.</div>';
                 return;
            }
    
            // Usa marked.js para converter Markdown em HTML
            const contentHtml = marked.parse(post.body);
    
            postContent.innerHTML = `
                <div class="box content">
                    <h1 class="title is-2 mb-3">${post.title}</h1>
                    <div class="post-meta level is-mobile is-size-6 has-text-grey mb-5">
                       <div class="level-left">
                         <div class="level-item">
                           <span class="icon-text">
                             <span class="icon"><i class="fas fa-calendar-alt"></i></span>
                             <span>${new Date(post.published_at).toLocaleDateString()}</span>
                           </span>
                         </div>
                       </div>
                       <div class="level-right">
                         <div class="level-item mr-3">
                            <span class="icon-text">
                              <span class="icon"><i class="fas fa-star"></i></span>
                              <span>${post.tabcoins ?? 0} TabCoins</span>
                            </span>
                         </div>
                         <div class="level-item">
                           <span class="icon-text">
                              <span class="icon"><i class="fas fa-comments"></i></span>
                              <span>${post.children_deep_count ?? 0} Comentários</span>
                            </span>
                         </div>
                       </div>
                    </div>
                    <hr class="mb-5">
                    <div>${contentHtml}</div>
                    <hr class="mt-5">
                    <a href="https://www.tabnews.com.br/${username}/${post.slug}" class="button is-link is-outlined mt-4" target="_blank" rel="noopener noreferrer">
                         <span class="icon"><i class="fas fa-external-link-alt"></i></span>
                         <span>Ver no TabNews</span>
                    </a>
                </div>
            `;
    
            // Adiciona botão de copiar aos blocos de código (opcional, mas útil)
            addCopyButtons();
    
            // Esconde a lista e mostra o detalhe
            postsList.classList.add('is-hidden');
            loadMoreButton.parentElement.classList.add('is-hidden');
            searchWrapper.classList.add('is-hidden');
            postDetail.classList.remove('is-hidden');
            window.scrollTo(0, 0); // Rola para o topo
    
        } catch (error) {
            console.error('Erro ao buscar post:', error);
            postContent.innerHTML = '<div class="notification is-danger">Erro ao carregar este post.</div>';
        }
    }
    
  5. Funcionalidades Extras:

    • Botão Voltar: Simplesmente esconde a div de detalhe e mostra a lista novamente.
    • Carregar Mais: Incrementa visiblePosts e chama renderPosts novamente.
    • Busca: Filtra o array allFetchedPosts pelo título e chama renderPosts com o array filtrado.
    • Dark Mode: Adiciona/remove uma classe no <html> e salva a preferência no localStorage.
    • Botão Copiar Código: Uma função addCopyButtons que encontra pre code e adiciona um botão para copiar o conteúdo usando navigator.clipboard.writeText.
    // Exemplo Botão Voltar
    backButton.addEventListener('click', () => {
        postDetail.classList.add('is-hidden');
        postsList.classList.remove('is-hidden');
        loadMoreButton.parentElement.classList.remove('is-hidden');
        searchWrapper.classList.remove('is-hidden');
        // Re-renderiza a lista com base na busca atual ou todos os posts
        const currentQuery = searchInput.value.toLowerCase();
         if (currentQuery) {
             const filtered = allFetchedPosts.filter(p => p.title && p.title.toLowerCase().includes(currentQuery));
             renderPosts(filtered);
         } else {
             renderPosts(allFetchedPosts);
         }
    });
    
    // Exemplo Carregar Mais
    loadMoreButton.addEventListener('click', () => {
        visiblePosts += POSTS_PER_PAGE;
        // Re-renderiza considerando a busca atual
        const currentQuery = searchInput.value.toLowerCase();
         if (currentQuery) {
             const filtered = allFetchedPosts.filter(p => p.title && p.title.toLowerCase().includes(currentQuery));
             renderPosts(filtered);
         } else {
             renderPosts(allFetchedPosts);
         }
    });
    
    // Exemplo Busca
    searchInput.addEventListener('input', () => {
        const query = searchInput.value.toLowerCase();
        visiblePosts = POSTS_PER_PAGE; // Reseta paginação na busca
        const filteredPosts = allFetchedPosts.filter(post =>
            post.title && post.title.toLowerCase().includes(query)
        );
        posts = filteredPosts; // Atualiza 'posts' para o filtro atual
        renderPosts(filteredPosts);
        if (!query) {
            posts = allFetchedPosts; // Reseta se a busca for limpa
            renderPosts(allFetchedPosts);
        }
    });
    
    // Exemplo Botão Copiar (simplificado)
    function addCopyButtons() {
       document.querySelectorAll('#post-content pre code').forEach((block) => {
         const preElement = block.parentElement;
         if (preElement && preElement.tagName === 'PRE') {
            preElement.style.position = 'relative';
            const button = document.createElement('button');
            button.className = 'copy-button'; // Estilize esta classe
            button.textContent = 'Copiar';
            button.onclick = () => { /* ... lógica de cópia ... */ };
            preElement.appendChild(button);
         }
       });
    }
    

Hospedagem

A beleza disso é que, por ser apenas HTML, CSS e JS estáticos, você pode hospedar gratuitamente em serviços como:

  • Cloudflare Pages
  • GitHub Pages - Onde o meu está hospedado
  • Vercel
  • Netlify


Conclusão

Com um pouco de código front-end e a API do TabNews, você pode criar um blog pessoal totalmente funcional e integrado com seus conteúdos aqui da plataforma. É uma ótima maneira de ter seu portfólio de posts centralizado e com a sua identidade visual.

O código que usei como base está mais completo (com dark mode e mais detalhes de estilo), mas a lógica central é essa que expliquei. Sinta-se à vontade para adaptar, melhorar e compartilhar suas próprias versões!

Espero que tenham gostado e que seja útil para alguém! Se tiverem dúvidas ou sugestões, comentem aí embaixo!

Até a próxima! 🚀


Promoção: Tem um blog? Queira se ajuntar com devjonatas e sua comunidade:

https://github.com/jonatasoli/awesome-brazilian-tech-blogs

Carregando publicação patrocinada...
0
0