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 zaman html/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ğin base.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ında template.FuncMap ile tanımlanır ve Parse* ö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. Ancak template.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 func(http.ResponseWriter, string, any)
}
func New(render func(http.ResponseWriter, string, any)) http.Handler {
r := &Router{render: render}
mux := chi.NewRouter()
mux.Get("/home", r.Home)
mux.Get("/posts", r.PostList)
return mux
}
func (r *Router) Home(w http.ResponseWriter, _ *http.Request) {
data := map[string]any{
"Title": "Anasayfa",
"User": "Onur",
"Year": time.Now().Year(),
}
r.render(w, "home", data)
}
func (r *Router) PostList(w http.ResponseWriter, _ *http.Request) {
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"),
},
}
data := map[string]any{
"Title": "Yazılar",
"Year": time.Now().Year(),
"Posts": posts,
}
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) {
tpl := template.Must(template.ParseFiles(
"views/layouts/base.html",
"views/partials/nav.html",
"views/pages/"+name+".html",
))
if err := tpl.ExecuteTemplate(w, "base", data); err != nil {
http.Error(w, err.Error(), 500)
}
}
func main() {
mux := http.NewServeMux()
mux.Handle("/app.css", http.FileServer(http.Dir("public")))
app := routerpkg.New(render)
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:
🥊 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ı izler
Air → 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: {
paths: ['views/**/*.html']
}
},
css: {
postcss: {
plugins: [tailwindcss, autoprefixer]
}
}
})
3️⃣ Makefile ile Tek Komut
dev:
\t
\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
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 yerine ExecuteTemplate("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ında Title vermemek → zero string ve sessiz hatalar.
❓ SSS
template.Must prod’da kalmalı mı?
Güvenli ama panik attırır. Prod’da template’leri uygulama açılışında parse etmek zaten mantıklı — parse hatası varsa servis ayağa kalkmasın.
Template’leri embed edebilir miyim?
Evet, Go embed ile binary içine gömebilirsin. Deploy sadeleşir.
Tailwind JIT prod’da ağır mı?
Build aşamasında minify eder ve kullanılmayan class’ları budar. Üretimde tek app.css ile gayet hafif.
HTML’i nasıl güvenle göstereceğim?
Admin onaylı/sanitize edilmiş içerikleri 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