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

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:

```go
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:

```xml
<!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:

```bash
/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`

```xml
{{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`

```xml
{{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`

```xml
{{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:

```go
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):

```xml
<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):**

```bash
yarn add -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
```

`tailwind.config.js`

```javascript
module.exports = {
  content: ["./views/**/*.html", "./assets/**/*.js"],
  theme: { extend: {} },
  plugins: []
}
```

`/assets/tailwind.css`

```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:**

```bash
npx tailwindcss -i ./assets/tailwind.css -o ./public/app.css --watch
```

**Prod build (minify):**

```bash
npx tailwindcss -i ./assets/tailwind.css -o ./public/app.css --minify
```

## 🧪 Örnek Sayfa: “Blog Listesi”

### Go (veri + render)

### `internal/http/router.go`

```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`

```xml
{{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`

```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ı izler
    
* **Air** → Go kod değişikliklerini izler, otomatik yeniden başlatır
    

### 1️⃣ Air Kurulumu

```bash
go install github.com/cosmtrek/air@latest
```

`.air.toml` (örnek):

```ini
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`

```javascript
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

```makefile
dev:
\t# Vite + Air aynı anda çalışsın
\tconcurrently "yarn dev" "air"
```

`package.json`

```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` 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

<details data-node-type="hn-details-summary"><summary>template.Must prod’da kalmalı mı?</summary><div data-type="detailsContent">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.</div></details><details data-node-type="hn-details-summary"><summary>Template’leri embed edebilir miyim?</summary><div data-type="detailsContent">Evet, Go <code>embed</code> ile binary içine gömebilirsin. Deploy sadeleşir.</div></details><details data-node-type="hn-details-summary"><summary>Tailwind JIT prod’da ağır mı?</summary><div data-type="detailsContent">Build aşamasında minify eder ve kullanılmayan class’ları budar. Üretimde tek <code>app.css</code> ile gayet hafif.</div></details><details data-node-type="hn-details-summary"><summary>HTML’i nasıl güvenle göstereceğim?</summary><div data-type="detailsContent">Admin onaylı/sanitize edilmiş içerikleri <code>template.HTML</code> olarak ver. Kullanıcı input’unu ASLA doğrudan bastırma.</div></details>

## 🏁 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**
