Executando verificação de segurança...
1
Klee
9 min de leitura ·

Seu Feed do TabNews em Casa: Lua + SolVM na Parada!

Quem já deu uma olhada no SolVM: Lua Turbinado com Go, nosso canivete suíço pra rodar Lua com a força do Go, já pegou a ideia: a gente quer facilitar a vida e botar mais poder na mão de quem curte Lua. E por que não usar essa ferramenta pra construir algo que a gente vê todo dia? Que tal um agregadorzinho de notícias do TabNews, feito por você?

Sabe aquela coceirinha de querer um feed do seu jeito, ou só de sacar como um app busca infos numa API e joga na tela? É exatamente isso que vamos fazer! Um TabNews simplão: você vê os posts quentinhos e, se curtir, clica pra ler mais. Tudo com Lua, servido pelo SolVM, e com um código que é quase um "oi, mundo" de tão direto.

O que a gente vai cozinhar aqui?

Basicamente, um servidor web em Lua que vai ter duas caras:

  1. Na entrada (/), ele mostra a lista dos posts mais recentes direto da API do TabNews.
  2. Se você clicar num post, ele te leva pra uma página (/post/<usuario>/<slug>) com o conteúdo completinho daquele post.

O que você precisa ter na mochila?

  • O SolVM, claro! Se ainda não tem ele instalado e pronto pra ação, corre lá no GitHub do SolVM que o pessoal explica como faz.
  • Aquela vontade boa de brincar com Lua.

Bora pro Código: A Receita do Bolo

Então, prepare seu editor de texto favorito, crie um arquivo chamado meu_tabnews_simplao.lua (ou o nome que achar mais bacana) e vamos começar a dar vida a ele, parte por parte.

Primeiro, vamos definir em qual porta nosso servidor vai rodar e avisar no console que estamos começando os trabalhos.

local port = 8080
print("Starting SolVM web server on port " .. port)

Com a porta definida, o próximo passo é pedir pro SolVM criar o nosso servidor web. A gente usa a função create_server. Ela precisa de um nome pro servidor (vamos chamar de "main_server"), a porta que definimos antes, e um valor false pra dizer que, por enquanto, não vamos nos preocupar com HTTPS (TLS), pra manter tudo simples.

create_server("main_server", port, false) -- nome, porta, usa_tls?

Agora que o servidor está "no ar" (ou quase!), precisamos dizer a ele o que fazer quando alguém acessar a página principal, o famoso / (a raiz do nosso site). Pra isso, usamos handle_http. Essa função associa uma rota (no caso, /) no nosso main_server com uma função Lua que vai ser executada toda vez que essa rota for chamada.
Essa função que a gente passa pro handle_http vai receber um argumento req, que conteria detalhes da requisição do usuário. Por agora, não vamos precisar muito dele pra nossa página inicial. O mais importante é que ela vai montar e devolver o HTML da nossa página.

handle_http("main_server", "/", function(req)
    -- Aqui dentro vai a mágica pra mostrar o feed de posts
    local html_content = [[
<!DOCTYPE html>
<html>
<head>
    <title>TabNews Feed</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f0f2f5; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .post { border-bottom: 1px solid #eee; padding: 15px 0; cursor: pointer; transition: background-color 0.2s; }
        .post:hover { background-color: #f8f9fa; }
        .post:last-child { border-bottom: none; }
        .post-title { color: #1a73e8; margin: 0 0 10px 0; }
        .post-meta { color: #666; font-size: 0.9em; }
        .post-content { white-space: pre-wrap; line-height: 1.6; } /* Para o conteúdo do post individual */
        .back-button { display: inline-block; margin-bottom: 20px; color: #1a73e8; text-decoration: none; } /* Para voltar */
        .back-button:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <h1>TabNews Feed</h1>
        <div id="posts"></div> <!-- Aqui os posts vão aparecer -->
    </div>

    <script>
        // JavaScript para buscar e mostrar os posts e para navegação
        function loadPosts() {
            fetch('https://www.tabnews.com.br/api/v1/contents')
                .then(response => response.json())
                .then(posts => {
                    const postsDiv = document.getElementById('posts');
                    const container = document.querySelector('.container');
                    // Garante que o título seja do feed e limpa posts antigos
                    if(container.querySelector('h1')) container.querySelector('h1').textContent = 'TabNews Feed';
                    if(container.querySelector('.back-button')) container.querySelector('.back-button').remove(); // Remove botão de voltar se existir
                    postsDiv.innerHTML = ''; 

                    posts.forEach(post => {
                        const postElement = document.createElement('div');
                        postElement.className = 'post';
                        // Ao clicar, mudamos a URL do navegador para a página do post
                        postElement.onclick = () => window.location.href = '/post/' + post.owner_username + '/' + post.slug;
                        postElement.innerHTML = `
                            <h2 class="post-title">${post.title}</h2>
                            <div class="post-meta">
                                Por ${post.owner_username} • ${new Date(post.created_at).toLocaleDateString('pt-BR')}
                            </div>
                        `;
                        postsDiv.appendChild(postElement);
                    });
                })
                .catch(error => {
                     console.error('Erro ao carregar posts:', error);
                     document.getElementById('posts').innerHTML = '<p>Falha ao carregar posts. Tente novamente.</p>';
                });
        }

        function loadPost(username, slug) {
            fetch(`https://www.tabnews.com.br/api/v1/contents/${username}/${slug}`)
                .then(response => response.json())
                .then(post => {
                    const container = document.querySelector('.container');
                    // Limpa o container e adiciona o conteúdo do post
                    container.innerHTML = `
                        <a href="/" class="back-button">← Voltar para o feed</a>
                        <h1>${post.title}</h1>
                        <div class="post-meta">
                            Por ${post.owner_username} • ${new Date(post.created_at).toLocaleDateString('pt-BR')}
                        </div>
                        <div class="post-content">${post.body ? post.body.replace(/</g, "&lt;").replace(/>/g, "&gt;") : 'Conteúdo não disponível.'}</div>
                    `; // Um escape simples pra tags HTML no corpo do post
                })
                .catch(error => {
                    console.error('Erro ao carregar post:', error);
                    const container = document.querySelector('.container');
                    container.innerHTML = '<a href="/" class="back-button">← Voltar para o feed</a><h1>Post não encontrado</h1><p>Ocorreu um erro ao carregar este post.</p>';
                });
        }

        // Roteamento no lado do cliente (navegador)
        const path = window.location.pathname;
        if (path.startsWith('/post/')) {
            const parts = path.split('/'); // Ex: ["", "post", "username", "slug"]
            const username = parts[2];
            const slug = parts[3];
            if (username && slug) {
                loadPost(username, slug);
            } else {
                // Se a URL for tipo /post/ mas sem username/slug, melhor voltar pro feed
                window.location.href = '/';
            }
        } else {
            loadPosts(); // Se não for um post individual, carrega o feed
        }
    </script>
</body>
</html>
    ]]
    
    -- E aqui a gente devolve o HTML pro navegador
    return {
        status = 200, -- Dizendo que deu tudo certo
        headers = {
            ["Content-Type"] = "text/html" -- Avisando que é um HTML
        },
        body = html_content -- E o conteúdo em si
    }
end)

Reparou na local html_content = [[ ... ]]? Essa é uma string multi-linhas do Lua, e a gente colocou nosso HTML, CSS e JavaScript todo aí dentro. Simples assim!
O JavaScript é quem faz o trabalho pesado no navegador:

  • A função loadPosts() busca os posts mais recentes na API do TabNews (fetch('https://www.tabnews.com.br/api/v1/contents')), depois pega cada post e cria um elementozinho div pra ele, com título e autor. O mais legal é que, quando você clica nesse elemento (postElement.onclick), ele muda a URL do navegador (window.location.href) para algo como /post/usuario/slug-do-post.
  • A função loadPost(username, slug) é chamada quando a URL é de um post específico. Ela busca os detalhes daquele post na API (fetch(\https://...`)`) e atualiza o conteúdo da página para mostrar o título, autor e o corpo do post. Ela também adiciona um botão "← Voltar para o feed".
  • A última parte do script verifica qual é a URL atual (window.location.pathname). Se começar com /post/, ela pega o usuário e o slug e chama loadPost. Senão, chama loadPosts pra mostrar o feed.

Agora, precisamos de uma rota para quando o usuário clicar num post e for direcionado para /post/usuario/slug-do-post. O SolVM nos permite usar um asterisco * como um curinga na rota. Então, "/post/*" vai pegar qualquer URL que comece com /post/.
O interessante aqui é que vamos servir exatamente o mesmo HTML que servimos para a rota /. Por quê? Porque o JavaScript que já está embutido naquele HTML é inteligente o suficiente para olhar a URL do navegador (window.location.pathname) e decidir se deve carregar a lista de posts ou um post específico. Isso simplifica nosso lado do servidor em Lua!

handle_http("main_server", "/post/*", function(req)
    -- A gente entrega o mesmo HTMLzão da página inicial.
    -- O JavaScript lá dentro é quem vai se virar pra mostrar o post certo
    -- baseado na URL que o navegador acessou.
    local html_content = [[
<!DOCTYPE html>
<html>
<head>
    <title>TabNews Post</title> <!-- Poderia ser dinâmico com mais JS, mas vamos simplificar -->
    <style>
        body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f0f2f5; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .post { border-bottom: 1px solid #eee; padding: 15px 0; cursor: pointer; transition: background-color 0.2s; }
        .post:hover { background-color: #f8f9fa; }
        .post:last-child { border-bottom: none; }
        .post-title { color: #1a73e8; margin: 0 0 10px 0; }
        .post-meta { color: #666; font-size: 0.9em; }
        .post-content { white-space: pre-wrap; line-height: 1.6; }
        .back-button { display: inline-block; margin-bottom: 20px; color: #1a73e8; text-decoration: none; }
        .back-button:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <!-- O JavaScript vai preencher isso aqui com o post ou o feed -->
        <h1>Carregando...</h1> 
        <div id="posts"></div> <!-- Usado pelo feed, mas o JS limpa se for post individual -->
    </div>

    <script>
        // EXATAMENTE O MESMO SCRIPT DA ROTA "/"
        function loadPosts() {
            fetch('https://www.tabnews.com.br/api/v1/contents')
                .then(response => response.json())
                .then(posts => {
                    const postsDiv = document.getElementById('posts');
                    const container = document.querySelector('.container');
                    if(container.querySelector('h1')) container.querySelector('h1').textContent = 'TabNews Feed';
                    if(container.querySelector('.back-button')) container.querySelector('.back-button').remove();
                    postsDiv.innerHTML = '';
                    posts.forEach(post => {
                        const postElement = document.createElement('div');
                        postElement.className = 'post';
                        postElement.onclick = () => window.location.href = '/post/' + post.owner_username + '/' + post.slug;
                        postElement.innerHTML = `
                            <h2 class="post-title">${post.title}</h2>
                            <div class="post-meta">
                                Por ${post.owner_username} • ${new Date(post.created_at).toLocaleDateString('pt-BR')}
                            </div>
                        `;
                        postsDiv.appendChild(postElement);
                    });
                })
                .catch(error => {
                     console.error('Erro ao carregar posts:', error);
                     document.getElementById('posts').innerHTML = '<p>Falha ao carregar posts. Tente novamente.</p>';
                });
        }

        function loadPost(username, slug) {
            fetch(`https://www.tabnews.com.br/api/v1/contents/${username}/${slug}`)
                .then(response => response.json())
                .then(post => {
                    const container = document.querySelector('.container');
                    container.innerHTML = `
                        <a href="/" class="back-button">← Voltar para o feed</a>
                        <h1>${post.title}</h1>
                        <div class="post-meta">
                            Por ${post.owner_username} • ${new Date(post.created_at).toLocaleDateString('pt-BR')}
                        </div>
                        <div class="post-content">${post.body ? post.body.replace(/</g, "&lt;").replace(/>/g, "&gt;") : 'Conteúdo não disponível.'}</div>
                    `;
                })
                .catch(error => {
                    console.error('Erro ao carregar post:', error);
                    const container = document.querySelector('.container');
                    container.innerHTML = '<a href="/" class="back-button">← Voltar para o feed</a><h1>Post não encontrado</h1><p>Ocorreu um erro ao carregar este post.</p>';
                });
        }

        const path = window.location.pathname;
        if (path.startsWith('/post/')) {
            const parts = path.split('/');
            const username = parts[2];
            const slug = parts[3];
            if (username && slug) {
                loadPost(username, slug); // AGORA ESTA FUNÇÃO SERÁ CHAMADA
            } else {
                window.location.href = '/'; 
            }
        } else {
            loadPosts();
        }
    </script>
</body>
</html>
    ]]
    
    return {
        status = 200,
        headers = { ["Content-Type"] = "text/html" },
        body = html_content
    }
end)

Então, quando o navegador pedir /post/fulano/meu-post-incrivel, o SolVM vai entregar esse HTML. O JavaScript dentro dele vai ver a URL, extrair "fulano" e "meu-post-incrivel", e chamar loadPost("fulano", "meu-post-incrivel"). Mágico, né?

Com nossos "manipuladores de rota" (os handle_http) prontos, só falta mandar o SolVM de fato iniciar o servidor. É pra isso que serve o start_server.

start_server("main_server")
print("Servidor rodando! Acesse em http://localhost:" .. port)

E, por último, como o servidor do SolVM roda em "segundo plano" (em goroutines, se você gosta dos termos técnicos do Go), nosso script Lua principal terminaria e levaria o servidor junto. Para evitar isso e manter nosso TabNews Simplão no ar, a gente adiciona um loop infinito com uma pequena pausa (sleep(1)), só pra segurar as pontas.

while true do
    sleep(1) -- Dá uma respirada pra não fritar a CPU
end

E é isso! Juntando todas essas partes, você tem seu meu_tabnews_simplao.lua completo.

Para Rodar a Brincadeira:

  1. Salve o código acima como meu_tabnews_simplao.lua.
  2. Abra seu terminal ou prompt de comando.
  3. Navegue até a pasta onde você salvou o arquivo.
  4. Execute com o SolVM: solvm meu_tabnews_simplao.lua
  5. Abra seu navegador e acesse http://localhost:8080.

Você deverá ver a lista de posts recentes do TabNews. Clique em um deles e veja a mágica acontecer: a página do post individual será carregada!

Desbravando o TabNews com Lua e SolVM

Viu só? Com poucas linhas de Lua e a ajuda do SolVM, a gente montou um leitor funcional do TabNews. A beleza aqui é que o Lua cuida de servir a "casca" da aplicação (o HTML), e o JavaScript embutido nessa casca faz as chamadas para a API do TabNews e monta a visualização dinamicamente no navegador.

Isso é só um gostinho do que dá pra fazer. O SolVM tá aí pra te dar as ferramentas pra construir desde scripts rapidinhos até serviços web mais encorpados, tudo com a leveza e a simplicidade do Lua, mas com a robustez do Go por baixo do capô.

Espero que tenha curtido essa aventura! Agora é com você: que tal adicionar mais funcionalidades? Um campo de busca? Paginação? O céu é o limite!

Obrigada pela companhia nessa jornada!
Código final

Carregando publicação patrocinada...