# Go Template System Documentation ## Overview The CV site uses Go's `html/template` package with a custom **Manager** that provides thread-safe template handling, hot reload for development, and custom template functions. The system automatically loads templates and partials from configured directories. ### Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Template Manager │ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ Config │───>│ sync.RWMutex │ │ Custom │ │ │ │ (dirs) │ │ (thread │<──│ Functions │ │ │ └──────────────┘ │ safe) │ └───────────────┘ │ │ │ └──────────────┘ │ │ │ v │ v │ │ ┌──────────────┐ v ┌───────────────┐ │ │ │ loadTemplates│ ┌─────────────┐ │ FuncMap │ │ │ │ (ParseGlob) │───>│ *template. │<──│ - iterate │ │ │ └──────────────┘ │ Template │ │ - eq │ │ │ │ └─────────────┘ │ - safeHTML │ │ │ v │ - dict │ │ │ ┌──────────────┐ └───────────────┘ │ │ │ Partials │ │ │ │ (ParseFiles) │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ v ┌──────────────────┐ │ Render(name) │ │ - Hot Reload │ │ - Thread-Safe │ └──────────────────┘ ``` ## Core Components ### Manager Struct **File:** `internal/templates/template.go` ```go type Manager struct { templates *template.Template // Parsed templates config *config.TemplateConfig // Configuration mu sync.RWMutex // Thread-safety lock } ``` **Responsibilities:** - Load and parse templates - Manage hot reload in development - Provide thread-safe rendering - Cache parsed templates ### Configuration ```go type TemplateConfig struct { Dir string // Main templates directory (e.g., "templates") PartialsDir string // Partials directory (e.g., "templates/partials") HotReload bool // Enable hot reload in development } ``` ## Template Loading ### Main Templates Templates are loaded from the configured directory using glob patterns: ```go pattern := filepath.Join(m.config.Dir, "*.html") tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern) ``` **Example Directory Structure:** ``` templates/ ├── base.html # Base layout ├── home.html # Home page ├── cv.html # CV content └── partials/ ├── header.html ├── footer.html └── contact/ └── form.html ``` ### Partials Loading Partials are loaded recursively from subdirectories: ```go // Recursive subdirectories: templates/partials/**/*.html partialsPattern := filepath.Join(m.config.PartialsDir, "**", "*.html") partialsMatches, _ := filepath.Glob(partialsPattern) // Direct children: templates/partials/*.html partialsDirectPattern := filepath.Join(m.config.PartialsDir, "*.html") directMatches, _ := filepath.Glob(partialsDirectPattern) // Combine and parse allPartials := append(partialsMatches, directMatches...) if len(allPartials) > 0 { tmpl, err = tmpl.ParseFiles(allPartials...) } ``` **Logged Output:** ``` 📦 Loaded 12 partial templates 📋 Templates loaded successfully from templates ``` ### Initialization ```go func NewManager(cfg *config.TemplateConfig) (*Manager, error) { m := &Manager{config: cfg} if err := m.loadTemplates(); err != nil { return nil, fmt.Errorf("failed to load templates: %w", err) } return m, nil } ``` **Usage:** ```go cfg := &config.TemplateConfig{ Dir: "templates", PartialsDir: "templates/partials", HotReload: true, // Development mode } manager, err := templates.NewManager(cfg) if err != nil { log.Fatal(err) } ``` ## Custom Template Functions ### 1. iterate(count int) Generates a range of integers for loop iteration. ```go "iterate": func(count int) []int { var result []int for i := 0; i < count; i++ { result = append(result, i) } return result } ``` **Template Usage:** ```html {{range iterate 5}}
Item {{.}}
{{end}} ``` **Output:** ```html
Item 0
Item 1
Item 2
Item 3
Item 4
``` **Use Cases:** - Generating placeholder items - Creating grid layouts - Sprite icon generation - Star ratings **Example (Star Rating):** ```html
{{range iterate 5}} {{end}}
``` ### 2. eq(a, b string) String equality check for conditional rendering. ```go "eq": func(a, b string) bool { return a == b } ``` **Template Usage:** ```html {{if eq .Language "en"}}

English content

{{else if eq .Language "es"}}

Contenido en español

{{end}} ``` **Common Patterns:** ```html ``` ### 3. safeHTML(s string) Marks content as safe HTML to prevent escaping. ```go "safeHTML": func(s string) template.HTML { return template.HTML(s) } ``` **⚠️ SECURITY WARNING:** - **ONLY** use with trusted content from YAML/config files - **NEVER** use with user-generated content - Prevents XSS attacks by restricting usage **Safe Usage (CV Data):** ```html
{{safeHTML .CV.Bio}}
``` **Example CV YAML:** ```yaml bio: | I'm a Senior Engineer with expertise in Go, HTMX, and cloud architecture. ``` **Rendered Output:** ```html
I'm a Senior Engineer with expertise in Go, HTMX, and cloud architecture.
``` **❌ DANGEROUS Usage:** ```html
{{safeHTML .UserMessage}}
``` **✅ Safe Alternative:** ```html
{{.UserMessage}}
``` ### 4. dict(values ...interface{}) Creates a map from key-value pairs for passing data to sub-templates. ```go "dict": func(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("dict requires even number of arguments") } dict := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, fmt.Errorf("dict keys must be strings") } dict[key] = values[i+1] } return dict, nil } ``` **Template Usage:** ```html {{template "user-card" dict "Name" .User.Name "Email" .User.Email "Active" true}} ``` **Partial Template (user-card):** ```html {{define "user-card"}}

{{.Name}}

{{.Email}}

{{if .Active}} Active {{end}}
{{end}} ``` **Complex Example:** ```html {{range .Experiences}} {{template "experience-card" dict "Title" .Title "Company" .Company "Duration" .Duration "Highlights" .Highlights "Language" $.Language }} {{end}} ``` **Partial Template (experience-card):** ```html {{define "experience-card"}}

{{.Title}}

{{.Company}}

{{if eq .Language "en"}} View Details {{else}} Ver Detalles {{end}}
{{end}} ``` ## Hot Reload Mechanism ### Development Mode When `HotReload` is enabled, templates are reloaded on **every request**: ```go func (m *Manager) Render(name string) (*template.Template, error) { if m.config.HotReload { m.mu.Lock() if err := m.loadTemplatesLocked(); err != nil { // Reload failed, fall back to cached templates m.mu.Unlock() m.mu.RLock() defer m.mu.RUnlock() // ... return cached template ... } tmpl := m.templates.Lookup(name) m.mu.Unlock() // ... return template ... } // ... production path ... } ``` **Behavior:** 1. **Lock** for exclusive access (full lock) 2. **Reload** templates from disk 3. **Update** internal template cache 4. **Unlock** and return template **Benefits:** - Edit templates without restarting server - Instant feedback during development - Faster iteration cycles **Fallback Strategy:** If reload fails (e.g., syntax error), the manager: 1. Logs warning: `"Warning: template reload failed: %v"` 2. Falls back to cached templates 3. Continues serving with last known good templates ### Production Mode In production (`HotReload = false`), templates are loaded **once at startup**: ```go func (m *Manager) Render(name string) (*template.Template, error) { // Production mode: just read m.mu.RLock() defer m.mu.RUnlock() tmpl := m.templates.Lookup(name) if tmpl == nil { return nil, fmt.Errorf("template %q not found", name) } return tmpl, nil } ``` **Benefits:** - Zero reload overhead - Maximum performance - Read-only lock (concurrent safe) - Lower memory usage ## Thread Safety ### Locking Strategy ``` ┌─────────────────────────────────────────────────────────┐ │ Lock Strategy │ ├─────────────────────────────────────────────────────────┤ │ │ │ Development (Hot Reload): │ │ ┌────────────────────────────────────┐ │ │ │ 1. mu.Lock() (exclusive) │ │ │ │ 2. Reload templates │ │ │ │ 3. Update m.templates │ │ │ │ 4. mu.Unlock() │ │ │ └────────────────────────────────────┘ │ │ │ │ Production (No Hot Reload): │ │ ┌────────────────────────────────────┐ │ │ │ 1. mu.RLock() (shared read) │ │ │ │ 2. Lookup template │ │ │ │ 3. mu.RUnlock() │ │ │ └────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ ``` ### Concurrent Rendering Multiple goroutines can safely render templates: ```go // Handler 1 func (h *Handler) ServeHome(w http.ResponseWriter, r *http.Request) { tmpl, _ := h.templates.Render("home.html") // Thread-safe tmpl.Execute(w, data) } // Handler 2 (concurrent with Handler 1) func (h *Handler) ServeCV(w http.ResponseWriter, r *http.Request) { tmpl, _ := h.templates.Render("cv.html") // Thread-safe tmpl.Execute(w, data) } ``` **Production:** Both handlers use `RLock()` - fully concurrent **Development:** Serialized during reload, concurrent after unlock ## Usage in Handlers ### Basic Rendering ```go func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { // Get template (thread-safe, hot-reload aware) tmpl, err := h.templates.Render("home.html") if err != nil { http.Error(w, "Template error", http.StatusInternalServerError) return } // Prepare data data := map[string]interface{}{ "Title": "Juan's CV", "Language": h.getLanguage(r), "CV": h.cvData, } // Execute template if err := tmpl.Execute(w, data); err != nil { log.Printf("Template execution error: %v", err) } } ``` ### HTMX Partial Rendering ```go func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) { // Render partial for HTMX swap tmpl, err := h.templates.Render("cv-content.html") if err != nil { http.Error(w, "Template error", http.StatusInternalServerError) return } data := map[string]interface{}{ "CV": h.cvData, "Language": r.URL.Query().Get("lang"), "Length": r.URL.Query().Get("length"), } w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) } ``` ### Error Handling ```go func (h *CVHandler) HandleRequest(w http.ResponseWriter, r *http.Request) { tmpl, err := h.templates.Render("page.html") if err != nil { // Template not found or parse error log.Printf("Template error: %v", err) // Fallback to error template errorTmpl, _ := h.templates.Render("error.html") errorTmpl.Execute(w, map[string]interface{}{ "Error": "Page not available", }) return } // Render normally if err := tmpl.Execute(w, data); err != nil { log.Printf("Execution error: %v", err) } } ``` ## Template Patterns ### Base Layout with Blocks **base.html:** ```html {{block "title" .}}Default Title{{end}} {{block "head" .}}{{end}} {{template "header" .}}
{{block "content" .}}

Default content

{{end}}
{{template "footer" .}} {{block "scripts" .}}{{end}} ``` **home.html:** ```html {{define "title"}}Juan's CV - Home{{end}} {{define "content"}}

Welcome to my CV

{{.Bio}}

{{range .Experiences}} {{template "experience-card" dict "Experience" . "Language" $.Language}} {{end}} {{end}} {{define "scripts"}} {{end}} ``` ### Reusable Partials **partials/header.html:** ```html {{define "header"}}
{{end}} ``` ### Data-Driven Loops ```html {{define "skills-section"}}

{{if eq .Language "en"}}Skills{{else}}Habilidades{{end}}

{{range .Skills}}

{{.Name}}

{{range iterate 5}} {{end}}
{{end}}
{{end}} ``` ## Configuration Examples ### Development Setup ```go cfg := &config.TemplateConfig{ Dir: "templates", PartialsDir: "templates/partials", HotReload: true, // Enable for development } ``` **Benefits:** - Edit templates live - No server restarts - Instant feedback ### Production Setup ```go cfg := &config.TemplateConfig{ Dir: "templates", PartialsDir: "templates/partials", HotReload: false, // Disable for production } ``` **Benefits:** - Maximum performance - No reload overhead - Lower resource usage ### Environment-Based Configuration ```go func NewTemplateConfig() *config.TemplateConfig { return &config.TemplateConfig{ Dir: "templates", PartialsDir: "templates/partials", HotReload: os.Getenv("GO_ENV") != "production", } } ``` ## Template Organization ### Recommended Structure ``` templates/ ├── base.html # Base layout ├── home.html # Home page ├── cv.html # CV page ├── error.html # Error page │ ├── partials/ │ ├── header.html # Global header │ ├── footer.html # Global footer │ ├── nav.html # Navigation │ │ │ ├── cv/ │ │ ├── experience.html # Experience card │ │ ├── education.html # Education card │ │ ├── skills.html # Skills section │ │ └── languages.html # Languages section │ │ │ └── contact/ │ ├── form.html # Contact form │ └── success.html # Success message │ └── htmx/ ├── language-toggle.html # Language switcher ├── theme-toggle.html # Theme switcher └── cv-controls.html # CV controls ``` ### Naming Conventions **Main Templates:** - `page-name.html` (e.g., `home.html`, `cv.html`) - Define blocks that extend `base.html` **Partials:** - `component-name.html` (e.g., `header.html`, `experience-card.html`) - Define reusable `{{define "name"}}...{{end}}` blocks **HTMX Fragments:** - `feature-action.html` (e.g., `language-toggle.html`) - Small HTML fragments for HTMX swaps ## Debugging Templates ### Template Not Found Error ``` Error: template "cv.html" not found ``` **Troubleshooting:** 1. Check file exists in templates directory 2. Verify file extension is `.html` 3. Check template name in `Render()` call matches filename 4. Ensure templates loaded successfully (check logs) ### Parse Error ``` Error: template: cv.html:15: unexpected "}" in operand ``` **Common Causes:** - Unclosed `{{if}}` or `{{range}}` - Missing `{{end}}` - Syntax errors in expressions **Fix:** 1. Check line number in error message 2. Verify all control structures are closed 3. Use editor with Go template syntax highlighting ### Execution Error ``` Error: template: cv.html:20:15: executing "cv.html" at <.CV.Title>: can't evaluate field Title in type *models.CV ``` **Common Causes:** - Accessing non-existent field - Wrong data type passed to template - Nil pointer dereference **Fix:** 1. Verify data structure matches template expectations 2. Add nil checks: `{{if .CV}}{{.CV.Title}}{{end}}` 3. Use debug output: `{{printf "%#v" .}}` ## Performance Considerations ### Production Optimizations 1. **Disable Hot Reload:** Set `HotReload: false` 2. **Use Partials:** Reduce duplication, smaller memory footprint 3. **Minimize Template Complexity:** Simple templates execute faster 4. **Cache Data:** Don't fetch data in template functions ### Memory Usage ``` Single Template: ~2-5 KB With 10 Partials: ~15-25 KB Total Manager Overhead: ~50 KB ``` **Optimization:** - Templates loaded once at startup (production) - Shared across all requests - No per-request allocations ### Render Performance ``` Cold render (first time): ~100-200 µs Warm render (cached): ~50-100 µs Hot reload impact: ~1-2 ms (development only) ``` ## Security Best Practices ### 1. Auto-Escaping Go templates **automatically escape** HTML by default: ```html

{{.UserInput}}

``` ### 2. safeHTML Restrictions ```go // ✅ SAFE: Trusted CV data from YAML {{safeHTML .CV.Bio}} // ❌ UNSAFE: User-generated content {{safeHTML .UserMessage}} // XSS vulnerability! ``` ### 3. Template Injection Prevention ```go // ❌ NEVER DO THIS: Dynamic template names from user input tmpl, _ := h.templates.Render(r.URL.Query().Get("template")) // ✅ SAFE: Whitelist allowed templates allowedTemplates := map[string]bool{ "home.html": true, "cv.html": true, } templateName := r.URL.Query().Get("template") if !allowedTemplates[templateName] { templateName = "home.html" // Default } tmpl, _ := h.templates.Render(templateName) ``` ## Quick Reference ### Manager Methods ```go // Create manager manager, err := NewManager(cfg) // Check if initialized (useful in tests) if manager.IsInitialized() { ... } // Render template (thread-safe, hot-reload aware) tmpl, err := manager.Render("template.html") // Manual reload (rarely needed) err := manager.Reload() ``` ### Custom Functions ```go iterate(5) // → [0, 1, 2, 3, 4] eq("en", .Language) // → true/false safeHTML("text") // → template.HTML (unescaped) dict "key1" val1 "key2" val2 // → map[string]interface{} ``` ### Template Execution ```go // Basic execution err := tmpl.Execute(w, data) // Execute named template err := tmpl.ExecuteTemplate(w, "template-name", data) ``` ## Related Files - `internal/templates/template.go` - Template Manager implementation - `internal/config/config.go` - TemplateConfig definition - `templates/` - Main templates directory - `templates/partials/` - Reusable partial templates ## See Also - [Validation System Documentation](go-validation-system.md) - [Routes and API Documentation](go-routes-api.md) - [Go html/template Package](https://pkg.go.dev/html/template)