Como Construí Este Blog com Go

✨ Você já pensou em criar seu próprio blog pessoal, totalmente personalizado? Neste post, vou mostrar como construí este blog utilizando Go (Golang) e como você pode fazer o mesmo.

🔍 Por Que Escolhi Go?

Quando decidi criar meu próprio blog, tinha várias opções: WordPress, Ghost, ou geradores de sites estáticos como Hugo. Porém, escolhi desenvolver meu próprio sistema em Go por algumas razões fundamentais:

  1. Familiaridade com a linguagem: Tenho um relacionamento próximo com Go e queria aproveitar seus pontos fortes: simplicidade, performance e eficiência.

  2. Independência de sistemas prontos: Não queria depender de um CMS ou sistema de gerenciamento de blogs preexistente, que muitas vezes traz complexidades e limitações.

  3. Personalização completa: Queria algo mais pessoal, que pudesse expandir conforme minhas necessidades específicas, sem limitações impostas por plataformas de terceiros.

  4. Aprendizado contínuo: Desenvolver meu próprio blog foi uma excelente oportunidade para aprofundar meus conhecimentos em desenvolvimento web com Go.

  5. Compilação para um único binário: Go compila para um único executável, facilitando imensamente o processo de implantação e distribuição.

  6. Performance excepcional: Blogs construídos em Go têm tempos de carregamento extremamente rápidos, o que melhora a experiência do usuário e o SEO.

🏗️ Arquitetura e Estrutura do Projeto

Este blog segue uma arquitetura minimalista, mas eficiente, sem depender de frameworks complexos. Todos os componentes foram projetados pensando em simplicidade e facilidade de manutenção.

Estrutura de Diretórios

A estrutura do projeto é organizada da seguinte forma:

├── cmd/
│   └── blog/
├── content/
│   └── posts/
├── internal/
│   ├── models/
│   ├── renderer/
│   └── storage/
├── static/
│   ├── css/
│   ├── js/
│   └── wasm/
└── templates/

Esta organização segue as convenções de estrutura de projetos Go, tornando o código fácil de navegar e manter. A separação entre cmd, internal e content permite uma clara divisão de responsabilidades.

O Núcleo da Aplicação: Conversão Markdown para HTML

O coração do sistema está na conversão de arquivos Markdown em posts de blog. Todo o processo é executado em memória, sem necessidade de um banco de dados.

Estrutura dos Posts em Markdown

Cada post é um arquivo .md na pasta content/posts/, com metadados YAML no topo:

---
title: "Título do Post"
date: 2024-04-12
tags: ["tag1", "tag2"]
background: "script-do-background.js"
background_name: "Nome do Background"
background_instructions:
- icon:mouse-pointer Descrição da interação
- icon:hand-pointer Mais instruções de interação
---

# Conteúdo do post...

Carregamento e Processamento de Posts

O sistema carrega todos os arquivos Markdown usando um parser que gerencia tanto os metadados quanto o conteúdo:

// LoadPosts carrega todos os posts do diretório de conteúdo
func LoadPosts(contentDir string) ([]*Post, error) {
    var posts []*Post

    // Verificar se diretório existe
    if _, err := os.Stat(contentDir); os.IsNotExist(err) {
        return nil, fmt.Errorf("diretório de conteúdo não existe: %s", contentDir)
    }

    // Configurar o processador de markdown
    md := goldmark.New(
        goldmark.WithExtensions(extension.GFM),
        goldmark.WithParserOptions(
            parser.WithAutoHeadingID(),
        ),
        goldmark.WithRendererOptions(
            html.WithHardWraps(),
            html.WithXHTML(),
        ),
    )

    // Listar os arquivos markdown no diretório
    err := filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // Pular se não for arquivo .md
        if info.IsDir() || !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
            return nil
        }

        // Carregar e processar o markdown
        data, err := ioutil.ReadFile(path)
        if err != nil {
            return err
        }

        // Parse do conteúdo do arquivo
        post, err := ParsePost(data, md)
        if err != nil {
            return err
        }

        // O slug é o nome do arquivo sem a extensão
        post.Slug = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
        
        posts = append(posts, post)
        return nil
    })

    if err != nil {
        return nil, err
    }

    // Ordenar posts por data de publicação (mais recentes primeiro)
    sort.Slice(posts, func(i, j int) bool {
        return posts[i].PublishedAt.After(posts[j].PublishedAt)
    })

    return posts, nil
}

A função ParsePost processa tanto os metadados YAML quanto o conteúdo Markdown:

// ParsePost analisa o conteúdo markdown e extrai os metadados do post
func ParsePost(data []byte, md goldmark.Markdown) (*Post, error) {
    lines := bytes.Split(data, []byte("\n"))
    
    post := &Post{
        PublishedAt: time.Now(),
        UpdatedAt:   time.Now(),
        BackgroundName: "Background Interativo", // Nome padrão
    }

    // Processar cabeçalho YAML (formato simples)
    var contentStart int
    inHeader := false
    
    for i, line := range lines {
        lineStr := string(bytes.TrimSpace(line))
        
        if i == 0 && lineStr == "---" {
            inHeader = true
            continue
        }
        
        if inHeader && lineStr == "---" {
            inHeader = false
            contentStart = i + 1
            continue
        }
        
        if inHeader {
            parts := strings.SplitN(lineStr, ":", 2)
            if len(parts) < 2 {
                continue
            }
            
            key := strings.TrimSpace(parts[0])
            value := strings.TrimSpace(parts[1])
            
            switch key {
            case "title":
                post.Title = strings.Trim(value, "\"'")
            case "date":
                t, err := time.Parse("2006-01-02", value)
                if err == nil {
                    post.PublishedAt = t
                }
            case "tags":
                tagStr := strings.Split(value, ",")
                for _, tag := range tagStr {
                    tag = strings.TrimSpace(tag)
                    if tag != "" {
                        post.Tags = append(post.Tags, strings.Trim(tag, "\"[]"))
                    }
                }
            // Outros metadados processados aqui...
            }
        }
    }

    // Processar o conteúdo markdown
    if contentStart > 0 && contentStart < len(lines) {
        contentBytes := bytes.Join(lines[contentStart:], []byte("\n"))
        post.Content = string(contentBytes)
        
        var buf bytes.Buffer
        if err := md.Convert(contentBytes, &buf); err != nil {
            return nil, err
        }
        post.HTMLContent = buf.String()
        
        // Criar um resumo (excerpt) para o post
        if len(post.HTMLContent) > 0 {
            doc, err := goquery.NewDocumentFromReader(strings.NewReader(post.HTMLContent))
            if err == nil {
                firstP := doc.Find("p").First().Text()
                if len(firstP) > 160 {
                    post.Excerpt = firstP[:157] + "..."
                } else {
                    post.Excerpt = firstP
                }
            }
        }
    }
    
    return post, nil
}

Renderização do HTML com Templates

O sistema de renderização é responsável por carregar, analisar e renderizar os templates:

// Render renderiza um template com os dados fornecidos
func (r *Renderer) Render(w io.Writer, name string, data interface{}) error {
    // Busca pelo template exato
    tmpl, ok := r.templates[name]
    if !ok {
        // Busca por correspondência parcial
        for tName, t := range r.templates {
            if strings.HasSuffix(tName, name) {
                tmpl = t
                ok = true
                break
            }
        }
        
        if !ok {
            return fmt.Errorf("template não encontrado: %s", name)
        }
    }

    // Executa o template com o layout "base.html"
    return tmpl.ExecuteTemplate(w, "base.html", data)
}

Os controladores da aplicação processam as requisições HTTP e entregam os dados aos templates:

func (app *AppConfig) postViewHandler(w http.ResponseWriter, r *http.Request) {
    slug := chi.URLParam(r, "slug")
    
    post, err := app.PostStorage.GetPostBySlug(slug)
    if err != nil {
        http.Error(w, "Post não encontrado", http.StatusNotFound)
        return
    }
    
    data := map[string]interface{}{
        "Title":       post.Title,
        "Description": post.Excerpt,
        "Post":        post,
        "Tags":        app.PostStorage.GetAllTags(),
        "CurrentPage": "post",
    }
    
    err = app.Renderer.Render(w, "posts/single.html", data)
    if err != nil {
        http.Error(w, "Erro ao renderizar a página", http.StatusInternalServerError)
        log.Printf("Erro ao renderizar o post: %v", err)
    }
}

Roteamento e Manipulação de Requisições

O roteamento usa a biblioteca Chi, que oferece uma API expressiva e compatível com a interface http.Handler padrão do Go:

func main() {
    // Configuração da aplicação e carregamento de recursos...
    
    // Configura o roteador
    r := chi.NewRouter()

    // Middleware
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(60 * time.Second))
    
    // Rotas da aplicação
    r.Get("/", app.homeHandler)
    r.Get("/posts", app.postsListHandler)
    r.Get("/posts/{slug}", app.postViewHandler)
    r.Get("/tags/{tag}", app.tagViewHandler)
    r.Get("/about", app.aboutHandler)
    
    // Servir arquivos estáticos
    staticDir := filepath.Join(workDir, "static")
    fileServer(r, "/static", http.Dir(staticDir))
    
    // Iniciar servidor
    serverAddr := fmt.Sprintf(":%s", port)
    log.Fatal(http.ListenAndServe(serverAddr, r))
}

Sistema de Templates Eficiente

O sistema de templates inclui funções personalizadas para facilitar a formatação e apresentação dos dados:

func makeFuncMap() template.FuncMap {
    return template.FuncMap{
        "formatDate": func(date time.Time) string {
            return date.Format("02 Jan 2006")
        },
        "formatDateISO": func(date time.Time) string {
            return date.Format("2006-01-02")
        },
        "safe": func(html string) template.HTML {
            return template.HTML(html)
        },
        "truncate": func(s string, max int) string {
            if len(s) > max {
                return s[:max] + "..."
            }
            return s
        },
        "join": func(a []string, sep string) string {
            return strings.Join(a, sep)
        },
        "now": func() time.Time {
            return time.Now()
        },
    }
}

📊 Performance e Benefícios Técnicos

A implementação em Go traz alguns benefícios técnicos significativos:

  1. Velocidade excepcional: O site carrega quase instantaneamente devido à eficiência do Go
  2. Uso mínimo de memória: Todo o blog roda com menos de 20MB de RAM
  3. Segurança aprimorada: Não há banco de dados para ser hackeado, e Go é inerentemente mais seguro que linguagens interpretadas
  4. Escalabilidade: O servidor Go pode lidar com milhares de requisições simultâneas sem degradação de performance

🔧 Gerenciamento Simples via GitHub

Uma das grandes vantagens desta abordagem é a facilidade de gerenciamento do conteúdo diretamente pelo GitHub. Não há banco de dados, não há CMS complexo, apenas arquivos de texto e código.

Para adicionar um novo post, basta:

  1. Criar um novo arquivo Markdown na pasta content/posts/
  2. Adicionar os metadados YAML necessários
  3. Escrever o conteúdo usando Markdown
  4. Fazer commit e push para o GitHub

O sistema automaticamente reconhece o novo post e o adiciona ao blog com base em sua data de publicação.

💬 Sistema de Comentários via GitHub Issues

Em um post futuro, vou detalhar como implementei o sistema de comentários utilizando as Issues do GitHub - uma solução elegante que evita a necessidade de um banco de dados adicional ou serviços de terceiros. Esta integração permite que os leitores comentem nos posts utilizando suas contas do GitHub, mantendo tudo centralizado e fácil de moderar.

🔮 O Que Vem Por Aí

Este blog está em constante evolução, e tenho planos para adicionar mais recursos interessantes no futuro:

  1. Implementação de busca: Adição de um sistema de busca eficiente sem banco de dados
  2. Melhorias de SEO: Otimizações adicionais para melhor indexação
  3. Artigo detalhado sobre o sistema de comentários: Como implementei comentários usando GitHub Issues
  4. Performance e otimizações: Melhorias contínuas na velocidade e experiência do usuário

🔚 Conclusão

Criar este blog em Go foi uma jornada gratificante que me permitiu explorar as capacidades da linguagem para desenvolvimento web simples e eficiente. A abordagem minimalista, sem frameworks pesados ou bancos de dados complexos, resultou em um sistema extremamente rápido, fácil de manter e completamente personalizado para minhas necessidades.

Se você está pensando em criar seu próprio blog, considere esta abordagem - a combinação de Go, Markdown e GitHub oferece um fluxo de trabalho simples e eficiente.

Nota sobre o background: O background interativo deste post foi implementado com JavaScript e utiliza um algoritmo de simulação de bandos (boids) onde cada agente segue regras simples para criar comportamentos complexos. Você pode interagir com ele movendo o mouse pela tela.