πΏ Go Mint | Lightweight HTML Template
Lightweight, modular, and isolated HTML template engine for Go.
Mint adalah template engine berbasis html/template standar Go yang
memecahkan masalah block namespace collision melalui mekanisme isolasi
template per halaman secara otomatis. Modul ini murni membungkus html/template
bawaan Go dan tidak membawa dependensi tambahan apa pun.
Repositori: https://codeberg.org/masputrawae/mint
Latar Belakang & Permasalahan
Pengembangan modul ini bermula dari sulitnya mengelola templating di Go,
terutama terkait penamaan template yang sering kali membingungkan. Secara
default, Go menggunakan basename alih-alih pathname sebagai nama template.
Walaupun hal ini bisa diakali menggunakan blok {{ define }}, pendekatan
tersebut sangat rawan mengalami bentrokan (collision) dan menjadi tidak
efisien.
Untuk kasus yang sederhana, hal ini mungkin tidak terlalu terasa. Namun, masalah mulai muncul ketika struktur halaman bertambah.
Sebagai contoh, mari kita lihat kasus dengan satu base layout berikut:
<!-- base.html -->
<html>
<head>
{{ block "head" }}{{ end }}
</head>
<body>
{{ block "main" }}{{ end }}
</body>
</html>
Layout tersebut digunakan di halaman utama (index.html):
<!-- index.html -->
{{ template "base.html" . }}
<!-- head -->
{{ define "head" }}
<title>Home | Site Title</title>
{{ end }}
<!-- main -->
{{ define "main" }}
<h1>Home</h1>
{{ end }}
Dan juga digunakan di halaman kedua (about.html):
<!-- about.html -->
{{ template "base.html" . }}
<!-- head -->
{{ define "head" }}
<title>About | Site Title</title>
<meta name="description" content="Deskripsi halaman about" />
{{ end }}
<!-- main -->
{{ define "main" }}
<h1>About</h1>
{{ end }}
Saat kita mencoba merendernya secara standar melalui main.go:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := template.Must(template.New("").ParseFiles(
"base.html",
"index.html",
"about.html",
))
_ = tmpl.ExecuteTemplate(os.Stdout, "index.html", nil)
}
Hasilnya tidak konsisten. Alih-alih selalu mendapatkan halaman index.html,
Go terkadang merender halaman index.html dan terkadang about.html.
Hal ini terjadi karena nama {{ define }} (βmainβ dan βheadβ) dideklarasikan di
dua tempat yang berbeda, sehingga Go menimpanya dan tidak bisa membedakannya.
Kesimpulannya: templating di Go tidak memisahkan namespace untuk setiap file.
Semuanya digabungkan menjadi satu, dan Go mencari {{ define }} sebagai nama
template (jika kosong, barulah basename digunakan).
Pendekatan Solusi (Cloning)
Cara bawaan untuk mengatasi masalah namespace ini adalah dengan memanfaatkan
metode Clone() yang telah disediakan oleh html/template. Contoh
implementasinya adalah sebagai berikut:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := template.Must(template.New("").ParseFiles("base.html"))
index, _ := tmpl.Clone()
_, _ = index.ParseFiles("index.html")
_ = index.ExecuteTemplate(os.Stdout, "index.html", nil)
about, _ := tmpl.Clone()
_, _ = about.ParseFiles("about.html")
_ = about.ExecuteTemplate(os.Stdout, "about.html", nil)
}
Melalui cloning, index dan about menjadi instance yang sepenuhnya
independen. Keduanya tidak lagi berada di ruang (namespace) yang sama namun
tetap berhasil mewarisi layout dari base.html. Hasilnya, kedua halaman dapat
dirender dengan benar tanpa saling menimpa.
Pada tahap produksi, jangan abaikan penanganan error. Kode di atas ditulis singkat hanya sebagai contoh.
Otomatisasi dengan Mint
Walaupun metode cloning menyelesaikan masalah, metode ini sangat repetitif dan sulit dikelola (di-scale) jika proyek Anda memiliki puluhan halaman serta banyak base layout yang bervariasi.
Mint dirancang untuk mengotomatiskan kerumitan tersebut. Ide utamanya sederhana: memindai (scan) seluruh file template Anda secara otomatis, lalu memisahkan mana template yang independen (sebagai halaman) dan mana template yang sifatnya dapat digunakan kembali (reusable components).
Instalasi
Anda dapat langsung mengimpor modul ini ke dalam proyek Go Anda:
go get codeberg.org/masputrawae/mint
Struktur Direktori
Kumpulkan semua file template ke dalam satu direktori khusus (misalnya view,
web, atau templates).
Aturan Penting: Simpan semua template halaman (yang mewarisi layout dasar)
secara spesifik di dalam folder pages. Selebihnya, Anda bebas mengatur folder
lain (layout, komponen, dll.) selama masih berada di dalam root direktori
template yang sama.
Memulai Cepat (Quick Start)
Berikut adalah contoh penggunaan mint yang digabungkan dengan embed.FS dan
modul http standar dari Go:
1. Struktur Folder
view/
βββ layouts/
β βββ base.html
βββ components/
β βββ navbar.html
βββ pages/
βββ index.html
βββ about.html
2. Komponen (view/components/navbar.html)
<header>
<a href="/about">About {{ .Username }}</a>
</header>
3. Layout (view/layouts/base.html)
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<title>{{ block "title" . }}Mint Web{{ end }}</title>
</head>
<body>
{{ template "components/navbar.html" . }}
<main>{{ block "main" . }}{{ end }}</main>
</body>
</html>
4. Halaman (view/pages/index.html)
{{ template "layouts/base.html" . }}
<!-- title -->
{{ define "title" }}Beranda{{ end }}
<!-- main -->
{{ define "main" }}
<h1>Halo, {{ .Username }}!</h1>
{{ end }}
5. Halaman (view/pages/about.html)
{{ template "layouts/base.html" . }}
<!-- title -->
{{ define "title" }}About{{ end }}
<!-- main -->
{{ define "main" }}
<h1>Hi, my name is {{ .Username }}!</h1>
{{ end }}
package main
import (
"embed"
"log"
"net/http"
"codeberg.org/masputrawae/mint"
)
//go:embed view/*
var ViewFS embed.FS
func main() {
tmpl := mint.New()
if err := tmpl.Load(ViewFS, "view"); err != nil {
log.Fatal(err)
}
data := map[string]any{
"Username": "John Doe",
}
// Render halaman ber-layout
http.HandleFunc("/{$}", tmpl.Render("pages/index.html", data))
http.HandleFunc("/about", tmpl.Render("pages/about.html", data))
// Render komponen mandiri (misal untuk request HTMX)
http.HandleFunc("/navbar", tmpl.Render("components/navbar.html", data))
log.Println("Server berjalan di http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
Seluruh method yang tersedia di package bawaan html/template (seperti
fungsionalitas .Funcs(), .ExecTemplate(), dsbβ¦) tetap diekspos dan dapat
Anda gunakan seperti biasa pada instance Mint.
Hasil Test & Benchmark
30 Unit Test
| Kelompok | Test |
|---|---|
| Core | New, Load (valid/empty/invalid), AddParse (valid/invalid), AddParseFS |
| Namespace Isolation β | Index tidak bocor ke about, about tidak bocor ke index, render berulang konsisten |
| Inheritance | Halaman pages mewarisi layout + komponen |
| Error handling | Page not found β error, root not found β error, template invalid β error |
| HTTP Render | Status 200, Content-Type HTML, Content-Type CSS, Error 500, Funcs di template |
| funcs.go | Truncate (6 kasus), TimeFormat (3 kasus), Dict (4 kasus), UUID unik, Now akurat |
| Concurrency | Race-free dengan -race detector |
Hasil Benchmark (Intel Pentium Gold G6400 @ 4GHz)
Hasil banchmark bisa berbeda di perangkat lain.
| Benchmark | ops/s | ns/op | allocs/op |
|---|---|---|---|
| Render_Simple | ~142K/s | 7,035 ns | 19 |
| Render_WithLayout | ~67K/s | 14,965 ns | 35 |
| Render_MultiPage | ~66K/s | 15,124 ns | 35 |
| Render_Parallel | ~427K/s | 2,340 ns | 36 |
| ExecuteTemplate (murni) | ~105K/s | 9,530 ns | 26 |
| BuiltinsFuncs_InTemplate | ~78K/s | 12,847 ns | 37 |
| Dict | ~1.5M/s | 686 ns | 2 |
| Truncate | ~65M/s | 15.24 ns | 0 |