- Create internal/constants package with all hardcoded values (environment, cookies, themes, headers, routes, cache) - Create internal/httputil package for HTTP helper functions - Update all handlers and middleware to use centralized constants - Reorganize documentation with numbered prefixes (00-26) - Remove duplicate docs from validation folder and docs/ - Delete handlers/constants.go (moved to internal/constants)
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 <script> -->
</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:
- Lock for exclusive access (full lock)
- Reload templates from disk
- Update internal template cache
- 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:
- Logs warning:
"Warning: template reload failed: %v" - Falls back to cached templates
- 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
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:
- Check file exists in templates directory
- Verify file extension is
.html - Check template name in
Render()call matches filename - 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:
- Check line number in error message
- Verify all control structures are closed
- 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:
- Verify data structure matches template expectations
- Add nil checks:
{{if .CV}}{{.CV.Title}}{{end}} - Use debug output:
{{printf "%#v" .}}
Performance Considerations
Production Optimizations
- Disable Hot Reload: Set
HotReload: false - Use Partials: Reduce duplication, smaller memory footprint
- Minimize Template Complexity: Simple templates execute faster
- 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><script>alert('XSS')</script></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)
Related Files
internal/templates/template.go- Template Manager implementationinternal/config/config.go- TemplateConfig definitiontemplates/- Main templates directorytemplates/partials/- Reusable partial templates