Páginas Go a partir de pastas.
Trilha é um framework web para Go: crie um arquivo em app/ e ele vira uma rota. Layouts aninhados, formulários, APIs, middleware, servidor de desenvolvimento com recarga e um binário só. Nada além da biblioteca padrão.
go install github.com/emersonjoe/trilha/cmd/trilha@latest
trilha new agenda && cd agenda && trilha devUma pasta, uma rota
Não existe tabela de rotas. A árvore de app/ é a tabela: page.go responde uma página, route.go responde JSON, layout.go envolve tudo que está abaixo e middleware.go intercepta a subárvore. Segmentos dinâmicos usam o sufixo _ e grupos de rota o sufixo -, porque [slug] não é um caminho de import válido em Go.
app/
├── layout.go ← <html> de todas as páginas
├── page.go ← GET /
├── eventos/
│ ├── layout.go ← envolve /eventos/**
│ ├── page.go ← GET /eventos
│ ├── novo/page.go ← GET /eventos/novo + POST do formulário
│ └── slug_/page.go ← GET /eventos/{slug}
├── organizador-/ ← grupo: não entra na URL
│ ├── middleware.go ← exige login
│ └── painel/page.go ← GET /painel
└── api/eventos/route.go ← GET e POST /api/eventosHTML em Go, com escape por padrão
O pacote h é um DSL tipado: elementos e atributos são funções, h.Map e h.If cobrem o fluxo de controle e todo texto é escapado. O resultado ao lado não foi escrito à mão: é o código executado durante a construção deste site.
func Page(c *trilha.Ctx) (h.Node, error) {
return h.Ul(h.Class("eventos"),
h.Map(eventos, func(e Evento) h.Node {
return h.Li(
h.Strong(h.Text(e.Nome)),
h.Textf(" · %s · ", e.Cidade),
h.IfElse(e.Vagas > 0,
h.Textf("%d vagas", e.Vagas),
h.Em(h.Text("lotado"))),
)
}),
), nil
}- Encontro Go Campinas · Campinas · 12 vagas
- Oficina de HTTP · Recife · lotado
- Noite de código · Curitiba · 4 vagas
nome := "<script>alert('oi')</script>"
return h.P(
h.Text("Olá, "),
h.Strong(h.Text(nome)),
), nilOlá, <script>alert('oi')</script>
Layouts que se encaixam
Cada pasta pode ter um layout.go. A página é renderizada primeiro e passa para o layout mais interno, que passa para o de fora, até o <html> raiz. O título definido pela página com c.SetTitle chega a todos eles.
// app/eventos/layout.go
func Layout(c *trilha.Ctx, children h.Node) (h.Node, error) {
return h.Section(h.Class("agenda"),
h.Nav(h.A(h.Href("/eventos"), h.Text("Todos")),
h.A(h.Href("/eventos/novo"), h.Text("Novo"))),
children,
), nil
}
// app/eventos/page.go
func Page(c *trilha.Ctx) (h.Node, error) {
c.SetTitle("Eventos")
return h.H2(h.Text("Próximos eventos")), nil
}Próximos eventos
Formulários e APIs sem cerimônia
Exporte POST no mesmo page.go e o formulário chega com token CSRF verificado. Em route.go, cada método HTTP é uma função. Erros são valores: trilha.ErrNotFound vira 404, c.Redirect vira 303, qualquer outro erro vira 500 com stack só em desenvolvimento.
func Page(c *trilha.Ctx) (h.Node, error) {
return h.Form(h.Method("post"), h.Action("/eventos/novo"),
trilha.CSRFInput(c),
h.Label(h.For("nome"), h.Text("Nome do evento")),
h.Input(h.ID("nome"), h.Name("nome"), h.Required()),
h.Button(h.Type("submit"), h.Text("Publicar")),
), nil
}
func POST(c *trilha.Ctx) error {
ev := agenda.Criar(c.Form("nome"))
return c.Redirect("/eventos/" + ev.Slug)
}Salvou, viu.
trilha dev observa app/, regenera o registro de rotas, recompila, troca o processo e avisa o navegador. Um ciclo leva cerca de um segundo; mudanças em CSS chegam em milissegundos, sem recompilar. Erro de compilação vira uma página no navegador que some quando você corrige.
Zero dependências
Runtime e CLI usam só a biblioteca padrão. O roteador é o http.ServeMux do Go 1.22.
Sem mágica em runtime
A CLI gera um arquivo Go com o registro de rotas. O compilador confere cada assinatura. Sem reflect.
Um binário
trilha build embute public/ e produz um executável que sobe sozinho. trilha export gera um site estático, como este.
Comece pela primeira página
A trilha de aprendizado vai do trilha new até o deploy, construindo uma agenda de eventos. Cada capítulo termina com um desafio.