895 lines
23 KiB
Markdown
895 lines
23 KiB
Markdown
|
|
# 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}}
|
||
|
|
<div class="item-{{.}}">Item {{.}}</div>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Output:**
|
||
|
|
```html
|
||
|
|
<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):**
|
||
|
|
```html
|
||
|
|
<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.
|
||
|
|
|
||
|
|
```go
|
||
|
|
"eq": func(a, b string) bool {
|
||
|
|
return a == b
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Template Usage:**
|
||
|
|
```html
|
||
|
|
{{if eq .Language "en"}}
|
||
|
|
<p>English content</p>
|
||
|
|
{{else if eq .Language "es"}}
|
||
|
|
<p>Contenido en español</p>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Common Patterns:**
|
||
|
|
```html
|
||
|
|
<!-- 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.
|
||
|
|
|
||
|
|
```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
|
||
|
|
<!-- CV YAML has trusted HTML content -->
|
||
|
|
<div class="bio">
|
||
|
|
{{safeHTML .CV.Bio}}
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example CV YAML:**
|
||
|
|
```yaml
|
||
|
|
bio: |
|
||
|
|
I'm a <strong>Senior Engineer</strong> with expertise in
|
||
|
|
<em>Go, HTMX, and cloud architecture</em>.
|
||
|
|
```
|
||
|
|
|
||
|
|
**Rendered Output:**
|
||
|
|
```html
|
||
|
|
<div class="bio">
|
||
|
|
I'm a <strong>Senior Engineer</strong> with expertise in
|
||
|
|
<em>Go, HTMX, and cloud architecture</em>.
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**❌ DANGEROUS Usage:**
|
||
|
|
```html
|
||
|
|
<!-- NEVER DO THIS -->
|
||
|
|
<div class="message">
|
||
|
|
{{safeHTML .UserMessage}} <!-- XSS vulnerability! -->
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**✅ Safe Alternative:**
|
||
|
|
```html
|
||
|
|
<!-- 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.
|
||
|
|
|
||
|
|
```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"}}
|
||
|
|
<div class="user-card">
|
||
|
|
<h3>{{.Name}}</h3>
|
||
|
|
<p>{{.Email}}</p>
|
||
|
|
{{if .Active}}
|
||
|
|
<span class="badge">Active</span>
|
||
|
|
{{end}}
|
||
|
|
</div>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Complex Example:**
|
||
|
|
```html
|
||
|
|
<!-- Main template -->
|
||
|
|
{{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"}}
|
||
|
|
<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**:
|
||
|
|
|
||
|
|
```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
|
||
|
|
<!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:**
|
||
|
|
```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:**
|
||
|
|
```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
|
||
|
|
|
||
|
|
```html
|
||
|
|
{{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
|
||
|
|
|
||
|
|
```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
|
||
|
|
<!-- User input: <script>alert('XSS')</script> -->
|
||
|
|
<p>{{.UserInput}}</p>
|
||
|
|
|
||
|
|
<!-- Output: <p><script>alert('XSS')</script></p> -->
|
||
|
|
```
|
||
|
|
|
||
|
|
### 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("<strong>text</strong>") // → 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)
|