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:
-
Familiaridade com a linguagem: Tenho um relacionamento próximo com Go e queria aproveitar seus pontos fortes: simplicidade, performance e eficiência.
-
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.
-
Personalização completa: Queria algo mais pessoal, que pudesse expandir conforme minhas necessidades específicas, sem limitações impostas por plataformas de terceiros.
-
Aprendizado contínuo: Desenvolver meu próprio blog foi uma excelente oportunidade para aprofundar meus conhecimentos em desenvolvimento web com Go.
-
Compilação para um único binário: Go compila para um único executável, facilitando imensamente o processo de implantação e distribuição.
-
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:
- Velocidade excepcional: O site carrega quase instantaneamente devido à eficiência do Go
- Uso mínimo de memória: Todo o blog roda com menos de 20MB de RAM
- Segurança aprimorada: Não há banco de dados para ser hackeado, e Go é inerentemente mais seguro que linguagens interpretadas
- 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:
- Criar um novo arquivo Markdown na pasta
content/posts/
- Adicionar os metadados YAML necessários
- Escrever o conteúdo usando Markdown
- 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:
- Implementação de busca: Adição de um sistema de busca eficiente sem banco de dados
- Melhorias de SEO: Otimizações adicionais para melhor indexação
- Artigo detalhado sobre o sistema de comentários: Como implementei comentários usando GitHub Issues
- 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.