Go HTML Template ile Temiz UI: Base, Partial, FuncMap

Table of contents
- 🔧 Neyi Çözüyoruz?
- 🧠 Temel Kavramlar
- 🗂 Proje Yapısı ve Başlangıç
- 🏗 Layout & Partial Sistemi
- 🧰 FuncMap ile Helper’lar
- 🎨 TailwindCSS Entegrasyonu
- 🧪 Örnek Sayfa: “Blog Listesi”
- 🔒 Güvenlik & Anti-Pattern’ler
- 🥊 Zorluklar vs Kolaylıklar
- 🚀 Bonus: Geliştirme Deneyimini İyileştirme (Vite + Air)
- 🧭 html/template'i Tercih Etmek İçin Uygun Senaryolar
- ⚠️ Ne Zaman Alternatif Düşünmeli?
- 🚨 Sık Yapılan Hatalar
- ❓ SSS
- 🏁 Kapanış
- 🔜 Bir Sonraki Bölümde Ne Var?
- 📬 Kaynak Kodu
Go’da html/template
ile sunucu tarafı HTML üretimini daha düzenli ve güvenli hale getiriyoruz. Layout sistemiyle tekrar eden yapıları azaltıyor, FuncMap ile şablonlara küçük ama etkili yardımcılar ekliyoruz. Arayüzü de TailwindCSS ile sade ve şık tutuyoruz.
🔧 Neyi Çözüyoruz?
Sunucu-taraflı render: SEO dostu, hızlı ilk yükleme, sade mimari.
XSS koruması:
html/template
otomatik escape ile “yanlışlıkla XSS” şovunu bitiriyor.Parça/kalıp: Layout + partial düzeniyle kopyala-yapıştır çilesi yok.
Tailwind ile hız: Component kütüphanesine boğulmadan temiz UI.
🧠 Temel Kavramlar
text/template vs html/template
Go'da iki farklı template paketi var.
text/template
metin çıktıları için,html/template
ise HTML çıktıları için kullanılır. HTML'de XSS koruması sağladığı için web projelerinde her zamanhtml/template
tercih edilir.define / block
Template miras yapısını kurmak için kullanılır.
define
ile bir şablon bloğu tanımlanır,block
ile bu blok başka bir dosyada override edilebilir. Örneğinbase.html
içinde{{block "content" .}}...{{end}}
varsa,home.html
bu bloğu doldurur. Böylece base → sayfa ilişkisi kurulur.template
Başka bir template dosyasını çağırmak için kullanılır. Genellikle partial (parça) dosyaları çağırmak için kullanılır:
{{template "partials/nav" .}}
. Bu sayede tekrar eden yapılar (örn. navigasyon) tek bir yerde tanımlanır.FuncMap
Template içinde özel fonksiyonlar tanımlamak için kullanılır. Örneğin
upper
,truncate
,fmtDate
gibi fonksiyonlarla veriyi biçimlendirebilirsin. Go tarafındatemplate.FuncMap
ile tanımlanır veParse*
öncesinde şablona eklenir.Otomatik Escape (auto-escape)
html/template
kullanıcıdan gelen verileri otomatik olarak encode eder. Örneğin<script>
gibi zararlı içerikler HTML olarak değil, düz metin olarak basılır. Bu sayede XSS saldırılarına karşı koruma sağlanır. Ancaktemplate.HTML
ile escape’i devre dışı bırakmak mümkündür — dikkatli kullanılmalıdır.
Basit örnek:
type PageData struct{ Title, User string }
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tpl := template.Must(template.ParseFiles("views/home.html"))
_ = tpl.Execute(w, PageData{Title: "Anasayfa", User: "Uygar"})
})
views/home.
html:
<!doctype html>
<html>
<head><title>{{.Title}}</title></head>
<body><h1>Merhaba {{.User}}</h1></body>
</html>
🗂 Proje Yapısı ve Başlangıç
Hızlı bir iskelet:
/cmd/server/main.go
/internal/http/router.go
/views
/layouts/base.html
/partials/nav.html
/pages/home.html
/assets/tailwind.css
/public/app.css (build çıkışı)
🏗 Layout & Partial Sistemi
views/layouts/base.html
{{define "base"}}
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{block "title" .}}Varsayılan Başlık{{end}}</title>
<link rel="stylesheet" href="/app.css" />
</head>
<body class="bg-neutral-50 text-neutral-900">
{{template "partials/nav" .}}
<main class="container mx-auto max-w-4xl px-4 py-8">
{{block "content" .}}{{end}}
</main>
<footer class="py-8 text-center text-sm text-neutral-500">
© {{.Year}} MyBlog
</footer>
</body>
</html>
{{end}}
views/partials/nav.html
{{define "partials/nav"}}
<header class="border-b bg-white/80 backdrop-blur">
<div
class="container mx-auto max-w-4xl px-4 h-14 flex items-center justify-between"
>
<a href="/" class="font-semibold">uygarceylan.net</a>
<nav class="flex gap-4 text-sm">
<a class="hover:underline" href="/">Ana Sayfa</a>
<a class="hover:underline" href="/posts">Bloglar</a>
</nav>
</div>
</header>
{{end}}
views/pages/home.html
{{define "content"}}
<section class="space-y-6">
<h1 class="text-3xl font-bold">Merhaba {{.User}}</h1>
<p class="text-neutral-600">
Bu blog Go <code>html/template</code> ile render ediliyor.
</p>
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
<strong class="block mb-1">İpucu:</strong>
Layout + partial ile tekrar eden HTML’i çöpe at.
</div>
</section>
{{end}}
Küçük ama kritik: Sayfa dosyası sadece
content
block’larını tanımlar; base her şeyi çerçeveler.
🧰 FuncMap ile Helper’lar
Go tarafında:
import (
"html/template"
"strings"
"time"
)
func newTemplates() *template.Template {
return template.Must(template.New("").Funcs(template.FuncMap{
"upper": strings.ToUpper,
"truncate": func(s string, n int) string {
if len(s) <= n { return s }
return s[:n] + "…"
},
"fmtDate": func(t time.Time) string {
return t.Format("02 Jan 2006")
},
}).ParseGlob("views/**/*.html"))
}
Kullanımı (örnek):
<h2 class="text-xl">{{upper .Title}}</h2>
<p class="text-sm text-neutral-500">{{fmtDate .PublishedAt}}</p>
<p>{{truncate .Excerpt 120}}</p>
Diğer bölümlerde burayı kodumuza entegre edeceğiz
🎨 TailwindCSS Entegrasyonu
Kurulum (Node gerektirir):
yarn add -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js
module.exports = {
content: ["./views/**/*.html", "./assets/**/*.js"],
theme: { extend: {} },
plugins: []
}
/assets/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
.container { @apply mx-auto px-4; }
.btn { @apply inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium border bg-white hover:bg-neutral-50; }
.input { @apply w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-neutral-900/10; }
.card { @apply rounded-lg border bg-white p-4 shadow-sm; }
Geliştirme build:
npx tailwindcss -i ./assets/tailwind.css -o ./public/app.css --watch
Prod build (minify):
npx tailwindcss -i ./assets/tailwind.css -o ./public/app.css --minify
🧪 Örnek Sayfa: “Blog Listesi”
Go (veri + render)
internal/http/router.go
type Post struct {
ID int
Title string
Excerpt string
ExcerptShort string
PublishedAt time.Time
PublishedAtStr string
}
type Router struct {
// render, HTTP yanıtını yöneten ve HTML şablonunu işleyen fonksiyondur.
// Bu fonksiyon, bir HTTP yanıt yazıcısı (ResponseWriter), bir şablon adı ve veriyi bekler.
render func(http.ResponseWriter, string, any)
}
func New(render func(http.ResponseWriter, string, any)) http.Handler {
r := &Router{render: render}
mux := chi.NewRouter()
// Ana sayfa ve blog listesi için GET isteklerini dinler.
mux.Get("/home", r.Home)
mux.Get("/posts", r.PostList)
return mux
}
// Home fonksiyonu, ana sayfa için gerekli veriyi hazırlar ve render fonksiyonuna gönderir.
func (r *Router) Home(w http.ResponseWriter, _ *http.Request) {
// Şablona gönderilecek veriyi bir map içinde topluyoruz.
data := map[string]any{
"Title": "Anasayfa",
"User": "Onur",
"Year": time.Now().Year(),
}
// "home" adlı şablonu (views/pages/home.html) bu veriyle işliyor ve yanıt olarak gönderiyoruz.
r.render(w, "home", data)
}
// PostList fonksiyonu, blog yazıları listesi için veriyi hazırlar.
func (r *Router) PostList(w http.ResponseWriter, _ *http.Request) {
// Örnek blog yazıları oluşturup bir slice içine ekliyoruz.
posts := []Post{
{
ID: 1,
Title: "Go Templates ile Başlarken",
Excerpt: "Template mimarisi, base ve partial pratikleri…",
ExcerptShort: "Template mimarisi, base ve partial pratikleri…",
PublishedAt: time.Now().AddDate(0, 0, -2),
PublishedAtStr: time.Now().AddDate(0, 0, -2).Format("02 Jan 2006"),
},
{
ID: 2,
Title: "HTMX’a Giriş",
Excerpt: "Partial render ve progressive enhancement…",
ExcerptShort: "Partial render ve progressive enhancement…",
PublishedAt: time.Now().AddDate(0, 0, -1),
PublishedAtStr: time.Now().AddDate(0, 0, -1).Format("02 Jan 2006"),
},
}
// Şablona gönderilecek veriyi hazırlıyoruz.
data := map[string]any{
"Title": "Yazılar",
"Year": time.Now().Year(),
"Posts": posts,
}
// "posts" adlı şablonu (views/pages/posts.html) bu veriyle işliyoruz.
r.render(w, "posts", data)
}
views/pages/posts.html
{{define "content"}}
<section class="space-y-6">
<header class="flex items-end justify-between">
<h1 class="text-3xl font-bold">{{.Title}}</h1>
<div class="w-64">
<input class="input" placeholder="Ara (dummy — HTMX sonraki yazıda)" />
</div>
</header>
<ul class="grid gap-4 md:grid-cols-2">
{{range .Posts}}
<li class="card">
<a class="block group" href="/posts/{{.ID}}">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold group-hover:underline">
{{.Title}}
</h2>
<span class="text-xs text-neutral-500">{{.PublishedAtStr}}</span>
</div>
<p class="mt-2 text-sm text-neutral-600">{{.ExcerptShort}}</p>
<div class="mt-4">
<span class="btn text-neutral-700">Oku</span>
</div>
</a>
</li>
{{else}}
<li class="card">
<p class="italic text-neutral-600">Henüz yazı yok.</p>
</li>
{{end}}
</ul>
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4">
<strong class="block mb-1">Sonraki adım:</strong>
Bu listeyi HTMX ile <em>partial</em> olarak yeniler hale getireceğiz.
</div>
</section>
{{end}}
cmd/server/main.go
func render(w http.ResponseWriter, name string, data any) {
// template.ParseFiles fonksiyonu, tüm şablon dosyalarını tek tek okuyup ayrıştırır.
// Bu örnekte, her istekte şablonlar yeniden okunup ayrıştırılıyor.
// Production ortamında bu işlemi sadece uygulama başlangıcında yapmak daha performanslıdır.
tpl := template.Must(template.ParseFiles(
"views/layouts/base.html",
"views/partials/nav.html",
"views/pages/"+name+".html",
))
// ExecuteTemplate, ana (base) şablonu çağırır ve diğer şablonları (partial, content) onun içine yerleştirir.
if err := tpl.ExecuteTemplate(w, "base", data); err != nil {
// Herhangi bir hata oluşursa, sunucu hatası (500) döndürürüz.
http.Error(w, err.Error(), 500)
}
}
func main() {
mux := http.NewServeMux()
// /app.css isteğini, public klasöründeki dosyayı sunacak şekilde yönlendiriyoruz.
mux.Handle("/app.css", http.FileServer(http.Dir("public")))
// Router'ı oluştururken, yukarıdaki render fonksiyonunu parametre olarak veriyoruz.
app := routerpkg.New(render)
// Gelen tüm HTTP isteklerini (varsayılan "/" yolu üzerinden) oluşturduğumuz router'a yönlendiriyoruz.
mux.Handle("/", app)
log.Println("http://localhost:8081")
_ = http.ListenAndServe("localhost:8081", mux)
}
🔒 Güvenlik & Anti-Pattern’ler
html/template
otomatik escape yapar; kullanıcı verisini güvenle basarsın.Asla doğrulanmamış HTML’i
template.HTML
ile “güvenli” yapma.Unutma: escape,
{{...}}
çıktısında çalışır; JS eventlerinde inline string kaçışını da düzgün yap.
“HTML basmam lazım” diyorsan:
İçeriği back-office’te sanitize et (ör. allowlist).
Sonra
template.HTML
ver — ama gerçekten güvenli olduğundan emin ol.
🥊 Zorluklar vs Kolaylıklar
✅ Kolaylıklar
Basit yığın: Go + Template + (ileride) HTMX/Alpine. Build karmaşası yok.
SEO ve hız: SSR ile hızlı FCP, botlar mutlu.
Güvenlik: Oto-escape default güvenli.
⚠️ Zorluklar
Büyük front-end interaktivite: SPA konforunu birebir bekleme. (Çözüm: HTMX/Alpine parça parça.)
Template mirası:
define/block
isim çakışmaları, hangi dosyada hangi block override ediliyor — dikkat ister.Önbellekleme & hot-reload: Dev’de her istekte parse rahat; prod’da parse-cache veya build-time embed istersin.
🚀 Bonus: Geliştirme Deneyimini İyileştirme (Vite + Air)
Hot-reload eksikliğini tek komutla çözmek mümkün.
İşin püf noktası:
Vite → TailwindCSS derlemesi +
views
klasöründeki.html
dosyalarını izlerAir → Go kod değişikliklerini izler, otomatik yeniden başlatır
1️⃣ Air Kurulumu
go install github.com/cosmtrek/air@latest
.air.toml
(örnek):
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ./cmd/server"
bin = "tmp/main"
include_ext = ["go", "html"]
exclude_dir = ["node_modules", "public"]
[log]
time = true
2️⃣ Vite Konfigürasyonu
vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'
export default defineConfig({
server: {
watch: {
// .html dosyalarını izleyelim
paths: ['views/**/*.html']
}
},
css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}
})
3️⃣ Makefile ile Tek Komut
dev:
\t# Vite + Air aynı anda çalışsın
\tconcurrently "yarn dev" "air"
package.json
{
"scripts": {
"dev": "vite",
"dev:css": "tailwindcss -i ./assets/tailwind.css -o ./public/app.css --watch"
},
"devDependencies": {
"vite": "^5.0.0",
"tailwindcss": "^3.4.0",
"autoprefixer": "^10.4.0",
"concurrently": "^8.0.0"
}
}
🧭 html/template'i
Tercih Etmek İçin Uygun Senaryolar
✅ Statik ağırlıklı içerikler
Blog, haber, dokümantasyon gibi sayfa odaklı projelerde mükemmel çalışır.
Hızlı ilk yükleme, SEO dostu yapı.
✅ Yönetim panelleri ve dashboard’lar
SSR ile hızlı tepki veren sayfalar üretilebilir.
Görsel tutarlılığı layout/partial yapısı sağlar. ✅ Basit formlar, CRUD işlemleri
Backend’de üretilen form yapıları için uygundur.
Validasyon sonrası sayfanın tamamını ya da bir kısmını kolayca güncelleyebilirsiniz.
⚠️ Ne Zaman Alternatif Düşünmeli?
❌ Ağır client-side etkileşimler
Offline modlar
Karmaşık form sihirbazları
Çok adımlı state’li UI’ler
Bu tür senaryolarda SSR tabanlı başla, gerektiğinde HTMX veya Alpine.js gibi çözümlerle client tarafını güçlendir.
🚨 Sık Yapılan Hatalar
text/template
ile HTML basmak → XSS riski.Execute
yerineExecuteTemplate("base", ...)
çağırmamak → layout çalışmaz.Funcs()
’ıParse*
’dan sonra çağırmak → helper’lar tanınmaz.Aynı
define
adını iki farklı dosyada hatalı kullanmak → override karmaşası.Template’te
.Title
beklerken Go tarafındaTitle
vermemek →zero
string ve sessiz hatalar.
❓ SSS
template.Must prod’da kalmalı mı?
Template’leri embed edebilir miyim?
embed
ile binary içine gömebilirsin. Deploy sadeleşir.Tailwind JIT prod’da ağır mı?
app.css
ile gayet hafif.HTML’i nasıl güvenle göstereceğim?
template.HTML
olarak ver. Kullanıcı input’unu ASLA doğrudan bastırma.🏁 Kapanış
Bu bölümde:
html/template
ile base/partial mimarisini kurduk,FuncMap
ile helper yazdık,“Blog Listesi” sayfası yaptık,
Güvenlik ve pratik tuzakları konuştuk.
🔜 Bir Sonraki Bölümde Ne Var?
Bölüm 2: HTMX ile Dinamik Parçalar
Sıradaki yazımızda, html/template
sistemimizi HTMX ile zenginleştireceğiz.
Arama kutusu gibi bir input ile, sadece sayfanın bir bölümünü (örneğin liste alanını) güncelleyen interaktif yapılar kuracağız:
Sayfa yenilemeden içerik güncelleme
hx-get
,hx-target
,hx-swap
gibi temel HTMX kavramlarıListe filtreleme örneği
Kodla açıklanan canlı demo parçaları
👉 Hiçbir framework ya da build sistemi kullanmadan, sadece HTML ve Go ile SPA benzeri deneyimler oluşturmanın yolunu göreceğiz.
📬 Kaynak Kodu
- GitHub repo: https://github.com/uodev/go-htmx-blog
Subscribe to my newsletter
Read articles from Uygar Öztürk Ceylan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
