Files
cv-site/docs/go-template-system.md
T
juanatsap 6c7595b041 feat: add tag-based validation system with reflection caching
Implement a declarative struct tag validation system for Go:

- Add validator.go with sync.Map caching for reflection metadata
- Add rules.go with 11 built-in validation rules (required, email,
  pattern, honeypot, timing, etc.)
- Add errors.go with FieldError and ValidationErrors types
- Update ContactFormRequest with validate tags
- Add ValidateContactFormV2() using the new tag-based validator

Rules implemented:
- required/optional: field presence validation
- trim/sanitize: automatic value transformations
- min/max: UTF-8 aware length validation
- email: RFC 5322 email format validation
- pattern: predefined regex patterns (name, subject, company)
- no_injection: email header injection prevention
- honeypot: bot trap (must be empty)
- timing: timestamp validation for bot detection

Documentation:
- docs/go-validation-system.md: complete validation guide
- docs/go-template-system.md: template manager documentation
- docs/go-routes-api.md: routes and API reference
- docs/README.md: documentation index
2025-12-06 15:20:45 +00:00

23 KiB

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

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

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:

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:

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

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:

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.

"iterate": func(count int) []int {
    var result []int
    for i := 0; i < count; i++ {
        result = append(result, i)
    }
    return result
}

Template Usage:

{{range iterate 5}}
    <div class="item-{{.}}">Item {{.}}</div>
{{end}}

Output:

<div class="item-0">Item 0</div>
<div class="item-1">Item 1</div>
<div class="item-2">Item 2</div>
<div class="item-3">Item 3</div>
<div class="item-4">Item 4</div>

Use Cases:

  • Generating placeholder items
  • Creating grid layouts
  • Sprite icon generation
  • Star ratings

Example (Star Rating):

<div class="stars">
    {{range iterate 5}}
        <span class="star {{if lt . $.Rating}}filled{{end}}"></span>
    {{end}}
</div>

2. eq(a, b string)

String equality check for conditional rendering.

"eq": func(a, b string) bool {
    return a == b
}

Template Usage:

{{if eq .Language "en"}}
    <p>English content</p>
{{else if eq .Language "es"}}
    <p>Contenido en español</p>
{{end}}

Common Patterns:

<!-- Active navigation item -->
<nav>
    <a href="/" class="{{if eq .Page "home"}}active{{end}}">Home</a>
    <a href="/cv" class="{{if eq .Page "cv"}}active{{end}}">CV</a>
</nav>

<!-- Theme selection -->
<select name="theme">
    <option value="light" {{if eq .Theme "light"}}selected{{end}}>Light</option>
    <option value="dark" {{if eq .Theme "dark"}}selected{{end}}>Dark</option>
</select>

3. safeHTML(s string)

Marks content as safe HTML to prevent escaping.

"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):

<!-- CV YAML has trusted HTML content -->
<div class="bio">
    {{safeHTML .CV.Bio}}
</div>

Example CV YAML:

bio: |
  I'm a <strong>Senior Engineer</strong> with expertise in
  <em>Go, HTMX, and cloud architecture</em>.

Rendered Output:

<div class="bio">
    I'm a <strong>Senior Engineer</strong> with expertise in
    <em>Go, HTMX, and cloud architecture</em>.
</div>

DANGEROUS Usage:

<!-- NEVER DO THIS -->
<div class="message">
    {{safeHTML .UserMessage}} <!-- XSS vulnerability! -->
</div>

Safe Alternative:

<!-- User content is auto-escaped -->
<div class="message">
    {{.UserMessage}} <!-- <script> becomes &lt;script&gt; -->
</div>

4. dict(values ...interface{})

Creates a map from key-value pairs for passing data to sub-templates.

"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:

{{template "user-card" dict "Name" .User.Name "Email" .User.Email "Active" true}}

Partial Template (user-card):

{{define "user-card"}}
<div class="user-card">
    <h3>{{.Name}}</h3>
    <p>{{.Email}}</p>
    {{if .Active}}
        <span class="badge">Active</span>
    {{end}}
</div>
{{end}}

Complex Example:

<!-- Main template -->
{{range .Experiences}}
    {{template "experience-card" dict
        "Title" .Title
        "Company" .Company
        "Duration" .Duration
        "Highlights" .Highlights
        "Language" $.Language
    }}
{{end}}

Partial Template (experience-card):

{{define "experience-card"}}
<article class="experience">
    <h3>{{.Title}}</h3>
    <p class="company">{{.Company}}</p>
    <time>{{.Duration}}</time>
    <ul>
        {{range .Highlights}}
            <li>{{.}}</li>
        {{end}}
    </ul>
    {{if eq .Language "en"}}
        <a href="#details">View Details</a>
    {{else}}
        <a href="#details">Ver Detalles</a>
    {{end}}
</article>
{{end}}

Hot Reload Mechanism

Development Mode

When HotReload is enabled, templates are reloaded on every request:

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:

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:

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

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

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

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:

<!DOCTYPE html>
<html lang="{{.Language}}">
<head>
    <meta charset="UTF-8">
    <title>{{block "title" .}}Default Title{{end}}</title>
    {{block "head" .}}{{end}}
</head>
<body>
    {{template "header" .}}

    <main>
        {{block "content" .}}
            <p>Default content</p>
        {{end}}
    </main>

    {{template "footer" .}}
    {{block "scripts" .}}{{end}}
</body>
</html>

home.html:

{{define "title"}}Juan's CV - Home{{end}}

{{define "content"}}
<section class="hero">
    <h1>Welcome to my CV</h1>
    <p>{{.Bio}}</p>
</section>

{{range .Experiences}}
    {{template "experience-card" dict "Experience" . "Language" $.Language}}
{{end}}
{{end}}

{{define "scripts"}}
<script src="/static/js/home.js"></script>
{{end}}

Reusable Partials

partials/header.html:

{{define "header"}}
<header>
    <nav>
        <a href="/" class="{{if eq .Page "home"}}active{{end}}">
            {{if eq .Language "en"}}Home{{else}}Inicio{{end}}
        </a>
        <a href="/cv" class="{{if eq .Page "cv"}}active{{end}}">CV</a>
    </nav>

    <div class="controls">
        <button hx-get="/switch-language" hx-swap="outerHTML">
            {{if eq .Language "en"}}ES{{else}}EN{{end}}
        </button>
        <button hx-get="/toggle/theme" hx-swap="outerHTML">
            {{if eq .Theme "dark"}}☀️{{else}}🌙{{end}}
        </button>
    </div>
</header>
{{end}}

Data-Driven Loops

{{define "skills-section"}}
<section class="skills">
    <h2>{{if eq .Language "en"}}Skills{{else}}Habilidades{{end}}</h2>

    {{range .Skills}}
    <div class="skill">
        <h3>{{.Name}}</h3>
        <div class="rating">
            {{range iterate 5}}
                <span class="star {{if lt . $.Level}}filled{{end}}"></span>
            {{end}}
        </div>
    </div>
    {{end}}
</section>
{{end}}

Configuration Examples

Development Setup

cfg := &config.TemplateConfig{
    Dir:         "templates",
    PartialsDir: "templates/partials",
    HotReload:   true, // Enable for development
}

Benefits:

  • Edit templates live
  • No server restarts
  • Instant feedback

Production Setup

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

func NewTemplateConfig() *config.TemplateConfig {
    return &config.TemplateConfig{
        Dir:         "templates",
        PartialsDir: "templates/partials",
        HotReload:   os.Getenv("GO_ENV") != "production",
    }
}

Template Organization

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:

<!-- User input: <script>alert('XSS')</script> -->
<p>{{.UserInput}}</p>

<!-- Output: <p>&lt;script&gt;alert('XSS')&lt;/script&gt;</p> -->

2. safeHTML Restrictions

// ✅ SAFE: Trusted CV data from YAML
{{safeHTML .CV.Bio}}

// ❌ UNSAFE: User-generated content
{{safeHTML .UserMessage}} // XSS vulnerability!

3. Template Injection Prevention

// ❌ 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

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

iterate(5)                    // → [0, 1, 2, 3, 4]
eq("en", .Language)           // → true/false
safeHTML("<strong>text</strong>")  // → template.HTML (unescaped)
dict "key1" val1 "key2" val2  // → map[string]interface{}

Template Execution

// Basic execution
err := tmpl.Execute(w, data)

// Execute named template
err := tmpl.ExecuteTemplate(w, "template-name", data)
  • internal/templates/template.go - Template Manager implementation
  • internal/config/config.go - TemplateConfig definition
  • templates/ - Main templates directory
  • templates/partials/ - Reusable partial templates

See Also