CahBagus
Menu

🌿 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.

Best Practice

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

KelompokTest
CoreNew, Load (valid/empty/invalid), AddParse (valid/invalid), AddParseFS
Namespace Isolation ⭐Index tidak bocor ke about, about tidak bocor ke index, render berulang konsisten
InheritanceHalaman pages mewarisi layout + komponen
Error handlingPage not found β†’ error, root not found β†’ error, template invalid β†’ error
HTTP RenderStatus 200, Content-Type HTML, Content-Type CSS, Error 500, Funcs di template
funcs.goTruncate (6 kasus), TimeFormat (3 kasus), Dict (4 kasus), UUID unik, Now akurat
ConcurrencyRace-free dengan -race detector

Hasil Benchmark (Intel Pentium Gold G6400 @ 4GHz)

Hasil banchmark bisa berbeda di perangkat lain.

Benchmarkops/sns/opallocs/op
Render_Simple~142K/s7,035 ns19
Render_WithLayout~67K/s14,965 ns35
Render_MultiPage~66K/s15,124 ns35
Render_Parallel~427K/s2,340 ns36
ExecuteTemplate (murni)~105K/s9,530 ns26
BuiltinsFuncs_InTemplate~78K/s12,847 ns37
Dict~1.5M/s686 ns2
Truncate~65M/s15.24 ns0