refactor: centralize constants and reorganize documentation

- 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)
This commit is contained in:
juanatsap
2025-12-06 16:27:12 +00:00
parent 71d9258c58
commit 2c7f8de242
37 changed files with 732 additions and 343 deletions
+477
View File
@@ -0,0 +1,477 @@
# CV Site Go Documentation
Comprehensive documentation for the Go implementation of the CV site.
## Documentation Overview
This documentation covers the core Go systems that power the CV site, with a focus on architecture, implementation details, and practical usage examples.
### 📚 Documentation Files
1. **[Go Validation System](24-GO-VALIDATION-SYSTEM.md)** (739 lines)
- Tag-based validation with reflection caching
- Built-in validation rules (required, email, pattern, etc.)
- Security validation (injection prevention, honeypot, timing)
- Custom rule extension guide
- Complete ContactFormRequest example
2. **[Go Template System](25-GO-TEMPLATE-SYSTEM.md)** (894 lines)
- Thread-safe template manager
- Hot reload mechanism for development
- Custom template functions (iterate, eq, safeHTML, dict)
- Template organization and patterns
- Performance optimizations
3. **[Go Routes and API](26-GO-ROUTES-API.md)** (1,203 lines)
- Complete route table with descriptions
- Middleware chain architecture
- Security features (CSP, HSTS, rate limiting)
- Protected endpoints and authentication
- API request/response formats
## Quick Navigation
### By Feature
**Validation:**
- [Tag Syntax](24-GO-VALIDATION-SYSTEM.md#tag-syntax)
- [Available Rules](24-GO-VALIDATION-SYSTEM.md#available-validation-rules)
- [ContactFormRequest Example](24-GO-VALIDATION-SYSTEM.md#complete-example-contactformrequest)
- [Error Handling](24-GO-VALIDATION-SYSTEM.md#error-handling)
- [Security Rules](24-GO-VALIDATION-SYSTEM.md#5-security-validation)
**Templates:**
- [Custom Functions](25-GO-TEMPLATE-SYSTEM.md#custom-template-functions)
- [Hot Reload](25-GO-TEMPLATE-SYSTEM.md#hot-reload-mechanism)
- [Thread Safety](25-GO-TEMPLATE-SYSTEM.md#thread-safety)
- [Template Patterns](25-GO-TEMPLATE-SYSTEM.md#template-patterns)
- [Security Best Practices](25-GO-TEMPLATE-SYSTEM.md#security-best-practices)
**Routes:**
- [Route Table](26-GO-ROUTES-API.md#route-table)
- [Middleware Stack](26-GO-ROUTES-API.md#middleware-stack)
- [Contact Form API](26-GO-ROUTES-API.md#apicontact---contact-form-submission)
- [PDF Export](26-GO-ROUTES-API.md#exportpdf---pdf-export)
- [Security Features](26-GO-ROUTES-API.md#security-features)
### By Use Case
**Setting Up Validation:**
1. [Define struct with tags](24-GO-VALIDATION-SYSTEM.md#struct-definition)
2. [Call validator](24-GO-VALIDATION-SYSTEM.md#validation-execution)
3. [Handle errors](24-GO-VALIDATION-SYSTEM.md#error-handling-example)
**Creating Templates:**
1. [Initialize manager](25-GO-TEMPLATE-SYSTEM.md#initialization)
2. [Use custom functions](25-GO-TEMPLATE-SYSTEM.md#custom-template-functions)
3. [Render in handlers](25-GO-TEMPLATE-SYSTEM.md#usage-in-handlers)
**Adding Routes:**
1. [Configure middleware](26-GO-ROUTES-API.md#middleware-stack)
2. [Register handlers](26-GO-ROUTES-API.md#route-table)
3. [Apply security](26-GO-ROUTES-API.md#route-specific-middleware)
## System Architecture
### Overall Flow
```
┌──────────────────────────────────────────────────────────────┐
│ HTTP Request │
└──────────────────────┬───────────────────────────────────────┘
v
┌──────────────────────────────────────────────────────────────┐
│ Middleware Chain │
│ Recovery → Logger → SecurityHeaders → DynamicCache → │
│ Preferences → Router │
└──────────────────────┬───────────────────────────────────────┘
v
┌──────────────────────────────────────────────────────────────┐
│ Route Handler │
│ 1. Parse request │
│ 2. Get preferences from context │
│ 3. Load data (CV, config) │
│ 4. Validate input (if needed) │
│ 5. Render template │
└──────────────────────┬───────────────────────────────────────┘
v
┌──────────────────────────────────────────────────────────────┐
│ Template Rendering │
│ 1. Load template (hot reload in dev) │
│ 2. Execute with data │
│ 3. Apply custom functions │
│ 4. Output HTML │
└──────────────────────┬───────────────────────────────────────┘
v
┌──────────────────────────────────────────────────────────────┐
│ HTTP Response │
│ - Security headers │
│ - Cache headers │
│ - Content-Type │
│ - HTML/JSON/PDF body │
└──────────────────────────────────────────────────────────────┘
```
### Contact Form Flow
```
POST /api/contact
v
┌─────────────────────┐
│ BrowserOnly │ Check User-Agent, Referer, Headers
│ Middleware │ → 403 if not browser
└─────────┬───────────┘
v
┌─────────────────────┐
│ RateLimiter │ 5 requests/hour per IP
│ (5/hour) │ → 429 if exceeded
└─────────┬───────────┘
v
┌─────────────────────┐
│ Parse JSON │ Decode ContactFormRequest
│ Request Body │
└─────────┬───────────┘
v
┌─────────────────────┐
│ Validate with │ Tag-based validation:
│ ValidateV2() │ - required, trim, max
│ │ - email, pattern
│ │ - no_injection, honeypot
│ │ - timing (2s-24h)
└─────────┬───────────┘
├─> Validation Failed → 400 + errors
v
┌─────────────────────┐
│ Send Email │ SMTP or email service
└─────────┬───────────┘
v
┌─────────────────────┐
│ 200 OK │ Success response
│ {success: true} │
└─────────────────────┘
```
## Key Features
### 1. Validation System
**Highlights:**
- Reflection-based with `sync.Map` caching for performance
- Declarative tag syntax: `validate:"required,email,max=254"`
- 11+ built-in rules including security rules
- Extensible with custom rules
- Thread-safe concurrent validation
**Performance:**
- First validation: ~2000 ns/op
- Cached validations: ~1500 ns/op
- Pre-compiled regex patterns
**Security:**
- Email header injection prevention
- Honeypot bot detection
- Timing-based bot detection
- HTML sanitization
- UTF-8 aware length validation
### 2. Template System
**Highlights:**
- Thread-safe with `sync.RWMutex`
- Hot reload in development (edit without restart)
- 4 custom template functions
- Recursive partial loading
- Production caching
**Custom Functions:**
- `iterate(count)` - Generate integer ranges
- `eq(a, b)` - String equality
- `safeHTML(s)` - Safe HTML (trusted content only)
- `dict(k1, v1, ...)` - Create maps for sub-templates
**Thread Safety:**
- Development: Full lock during reload
- Production: Read-only lock (concurrent)
### 3. Routes and Middleware
**Highlights:**
- 15+ routes (public, HTMX, API, protected)
- 8 middleware layers
- Comprehensive security headers
- Rate limiting (contact: 5/hour, PDF: 3/min)
- Origin checking for PDF exports
**Security Features:**
- Content Security Policy (CSP)
- HTTP Strict Transport Security (HSTS)
- BrowserOnly middleware (blocks curl/Postman)
- Email header injection prevention
- Rate limiting per IP
- Origin/Referer validation
## Code Examples
### Validation Example
```go
// Define struct with validation tags
type ContactFormRequest struct {
Name string `json:"name" validate:"required,trim,max=100,pattern=name"`
Email string `json:"email" validate:"required,email,no_injection"`
Message string `json:"message" validate:"required,trim,max=5000,sanitize"`
}
// Validate
if err := validation.ValidateContactFormV2(req); err != nil {
// Handle validation errors
validationErrors := err.(validation.ValidationErrors)
return c.JSON(400, map[string]interface{}{
"errors": validationErrors,
})
}
```
### Template Example
```go
// Initialize template manager
cfg := &config.TemplateConfig{
Dir: "templates",
PartialsDir: "templates/partials",
HotReload: true, // Development
}
manager, _ := templates.NewManager(cfg)
// Render in handler
tmpl, _ := manager.Render("home.html")
tmpl.Execute(w, map[string]interface{}{
"Title": "CV",
"CV": cvData,
})
```
### Route Example
```go
// Protected contact endpoint
contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)
protectedHandler := middleware.BrowserOnly(
contactRateLimiter.Middleware(
http.HandlerFunc(cvHandler.HandleContact),
),
)
mux.Handle("/api/contact", protectedHandler)
```
## Testing
### Run All Tests
```bash
# Run all tests
go test ./...
# Run with coverage
go test -cover ./...
# Run specific package
go test ./internal/validation/...
# Run with verbose output
go test -v ./internal/routes/...
```
### Test Examples
```bash
# Validation tests
go test ./internal/validation/ -v
# Template tests
go test ./internal/templates/ -v
# Middleware tests
go test ./internal/middleware/ -v
# Handler tests
go test ./internal/handlers/ -v
```
## Performance
### Benchmarks
```bash
# Run benchmarks
go test -bench=. ./...
# Validation benchmarks
go test -bench=Validate ./internal/validation/
# Template benchmarks
go test -bench=Render ./internal/templates/
```
### Typical Performance
**Validation:**
- Contact form validation: ~1.5 µs
- Email validation: ~500 ns
- Pattern matching: ~300 ns (pre-compiled)
**Templates:**
- Template render (cached): ~50-100 µs
- Hot reload: ~1-2 ms (development only)
**Routes:**
- Middleware overhead: ~10-20 µs per request
- Rate limiter check: ~100-200 ns
- Total request latency: <5 ms (p50), <20 ms (p99)
## Environment Configuration
### Development
```bash
export GO_ENV=development
export TEMPLATE_HOT_RELOAD=true
export PORT=8080
```
### Production
```bash
export GO_ENV=production
export TEMPLATE_HOT_RELOAD=false
export ALLOWED_ORIGINS="juan.andres.morenorub.io"
export PORT=8080
```
## File Organization
```
cv/
├── doc/
│ ├── 24-GO-VALIDATION-SYSTEM.md # Validation docs
│ ├── 25-GO-TEMPLATE-SYSTEM.md # Template docs
│ ├── 26-GO-ROUTES-API.md # Routes/API docs
│ └── 00-GO-DOCUMENTATION-INDEX.md # This file
├── internal/
│ ├── validation/
│ │ ├── validator.go # Core validator
│ │ ├── rules.go # Validation rules
│ │ ├── errors.go # Error types
│ │ └── contact.go # ContactFormRequest
│ │
│ ├── templates/
│ │ └── template.go # Template manager
│ │
│ ├── routes/
│ │ └── routes.go # Route setup
│ │
│ ├── middleware/
│ │ ├── security.go # Security middleware
│ │ ├── browser_only.go # BrowserOnly middleware
│ │ ├── contact_rate_limit.go # Rate limiting
│ │ ├── logger.go # Request logging
│ │ ├── recovery.go # Panic recovery
│ │ └── preferences.go # User preferences
│ │
│ └── handlers/
│ ├── cv.go # CV handlers
│ ├── cv_contact.go # Contact handler
│ ├── cv_pdf.go # PDF handler
│ └── health.go # Health check
└── templates/
├── *.html # Main templates
└── partials/ # Partial templates
```
## Best Practices
### Validation
1. **Use tag-based validation** for all struct validation
2. **Order rules correctly**: transformations first (trim, sanitize), then validations
3. **Use global validator** instance to benefit from caching
4. **Combine security rules** for defense in depth
5. **UTF-8 aware**: Use max/min for character count, not byte count
### Templates
1. **Disable hot reload** in production for performance
2. **Use safeHTML only** with trusted content (YAML/config)
3. **Organize templates** logically (main, partials, HTMX)
4. **Leverage custom functions** for reusable logic
5. **Test template execution** to catch errors early
### Routes
1. **Register specific routes first** to avoid conflicts
2. **Apply security middleware** to sensitive endpoints
3. **Use rate limiting** for resource-intensive operations
4. **Log all requests** for monitoring
5. **Implement health checks** for load balancers
## Troubleshooting
### Common Issues
**Validation not working:**
- Check tag syntax: `validate:"rule1,rule2=param"`
- Ensure field is exported (capitalized)
- Verify validator instance is created
**Template not found:**
- Check file exists in templates directory
- Verify filename matches `Render("name")`
- Check template loading logs
**Rate limit too strict:**
- Adjust limit in middleware initialization
- Clear rate limiter state (restart or implement clear endpoint)
**CORS errors:**
- Add domain to `ALLOWED_ORIGINS` environment variable
- Check `OriginChecker` middleware configuration
## Contributing
When adding new features:
1. **Update documentation** in relevant .md file
2. **Add tests** for new functionality
3. **Update route table** if adding endpoints
4. **Document security implications** if applicable
5. **Add examples** for complex features
## Version History
- **v1.0** (2025-12-06) - Initial comprehensive documentation
- Validation system with tag-based approach
- Template system with hot reload
- Complete route and middleware documentation
## License
This documentation is part of the CV site project.
---
**Last Updated:** December 6, 2025
**Total Documentation:** 2,836+ lines across 3 files
**Coverage:** Validation, Templates, Routes, Middleware, Security
View File
+264
View File
@@ -0,0 +1,264 @@
# Cache Package
## Overview
The `cache` package provides application-level caching for CV and UI data, eliminating per-request file I/O by loading all data once at application startup. This improves performance and reduces latency for all handler operations.
**Key Benefits:**
- Single load at startup, fast reads during request handling
- Thread-safe concurrent access using `sync.RWMutex`
- Language-keyed access ("en", "es")
- Fast-fail strategy: fails at startup if any language data cannot be loaded
## Architecture
### DataCache Structure
```go
type DataCache struct {
cv map[string]*cvmodel.CV // CV data indexed by language
ui map[string]*uimodel.UI // UI data indexed by language
mu sync.RWMutex // Protects concurrent reads
}
```
The cache stores pointer references to CV and UI models, loaded from YAML files. Since reads are frequent and writes never occur, `sync.RWMutex` provides efficient concurrent access.
## Usage
### Initialization
The cache is created once at application startup in `main.go`:
```go
// Initialize data cache (load CV and UI data once at startup)
dataCache, err := cache.New([]string{"en", "es"})
if err != nil {
log.Fatalf("Failed to initialize data cache: %v", err)
}
```
This loads CV and UI data for English and Spanish. If any language fails to load, the entire startup fails—catch errors early rather than on first request.
### Handler Integration
The cache is injected into handlers via constructor:
```go
cvHandler := handlers.NewCVHandler(templateMgr, serverAddr, emailService, dataCache)
```
Handlers access cached data using language-specific getters:
```go
func (h *CVHandler) renderPage(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
cv := h.dataCache.GetCV(lang)
ui := h.dataCache.GetUI(lang)
// Use cv and ui data for rendering...
}
```
## API Reference
### `New(languages []string) (*DataCache, error)`
Creates and initializes a new cache with data for the specified languages.
**Parameters:**
- `languages`: List of language codes to load (e.g., `[]string{"en", "es"}`)
**Returns:**
- `*DataCache`: Initialized cache instance
- `error`: Non-nil if any language fails to load
**Behavior:**
- Returns `nil` and error if any language's CV or UI data fails to load
- Empty language list creates empty cache (no error)
- Fails at startup rather than deferring errors to request time
**Example:**
```go
cache, err := cache.New([]string{"en", "es"})
if err != nil {
log.Fatalf("Failed to initialize cache: %v", err)
}
```
### `GetCV(lang string) *cvmodel.CV`
Retrieves cached CV data for the specified language.
**Parameters:**
- `lang`: Language code (e.g., "en", "es")
**Returns:**
- `*cvmodel.CV`: Pointer to CV data, or `nil` if language not found
- **Note:** Callers must check for `nil` before dereferencing
**Thread Safety:** Safe for concurrent reads
**Example:**
```go
cv := cache.GetCV("en")
if cv == nil {
// Handle missing language
return fmt.Errorf("CV not available for language: en")
}
// Use cv...
```
### `GetUI(lang string) *uimodel.UI`
Retrieves cached UI data for the specified language.
**Parameters:**
- `lang`: Language code (e.g., "en", "es")
**Returns:**
- `*uimodel.UI`: Pointer to UI data, or `nil` if language not found
**Thread Safety:** Safe for concurrent reads
**Example:**
```go
ui := cache.GetUI("es")
if ui != nil {
title := ui.Navigation.Title
}
```
### `Languages() []string`
Returns all language codes currently cached.
**Returns:**
- `[]string`: Slice of available language codes (order not guaranteed)
**Thread Safety:** Safe for concurrent reads
**Example:**
```go
langs := cache.Languages()
for _, lang := range langs {
cv := cache.GetCV(lang)
// Process CV for each language...
}
```
## Mutating Cached Data
### Important: Deep Copies for Mutable Fields
Since cache stores pointer references, handlers that modify CV slices must create deep copies before modification:
```go
// In handlers that modify experience/projects:
func prepareTemplateData(cv *cvmodel.CV) *cvmodel.CV {
// Create copies of mutable slices
copy := &cvmodel.CV{
Personal: cv.Personal,
Experience: append([]cvmodel.Experience{}, cv.Experience...), // Deep copy
Projects: append([]cvmodel.Project{}, cv.Projects...), // Deep copy
Education: cv.Education,
Skills: cv.Skills,
}
// Now safe to modify copy.Experience and copy.Projects
for i := range copy.Experience {
copy.Experience[i].YearsOfExperience = calculateYears()
}
return copy
}
```
This prevents handlers from accidentally mutating cached data during request processing.
## Supported Languages
Currently configured for:
- `"en"` - English
- `"es"` - Spanish
To add a new language, update `main.go`:
```go
dataCache, err := cache.New([]string{"en", "es", "fr"}) // Add "fr"
```
Ensure YAML data files exist in the data directory for the new language, or startup will fail.
## Error Handling
### Startup Failures
The fast-fail strategy ensures all data issues are caught before the server starts:
```go
dataCache, err := cache.New([]string{"en", "es"})
if err != nil {
// Example error messages:
// "load CV for 'fr': file not found"
// "load UI for 'es': invalid YAML"
log.Fatalf("Failed to initialize data cache: %v", err)
}
```
### Runtime Handling
Handlers should gracefully handle missing languages:
```go
cv := cache.GetCV(lang)
if cv == nil {
http.Error(w, "Language not supported", http.StatusNotFound)
return
}
```
## Performance Considerations
### I/O Efficiency
- **Single Load:** CV and UI YAML files are parsed once at startup
- **No Per-Request I/O:** Handler requests never touch disk
- **Memory Trade-off:** Stores decoded objects in memory
### Concurrency
- **RWMutex:** Optimized for high read throughput, zero writes
- **No Contention:** 100+ concurrent reads verified in tests
- **Nil Returns:** Fast path for missing languages (map lookup only)
### Memory Usage
- Minimal overhead: Two maps + one mutex
- Proportional to number of languages loaded
- Shared object references (no duplication per request)
## Testing
Run the comprehensive test suite:
```bash
go test ./internal/cache -v
```
Test coverage includes:
- Cache initialization with valid/invalid languages
- CV and UI data retrieval
- Thread safety with concurrent reads
- Data integrity verification
- Empty language list handling
## Dependencies
- `internal/models/cv` - CV data model
- `internal/models/ui` - UI data model
- Go standard library: `sync`
## Related Files
- **`internal/cache/data_cache.go`** - Cache implementation
- **`internal/cache/data_cache_test.go`** - Comprehensive test suite
- **`main.go`** - Cache initialization at startup
- **`internal/handlers/cv.go`** - Handler injection point
+739
View File
@@ -0,0 +1,739 @@
# Go Validation System Documentation
## Overview
The CV site implements a **tag-based validation system** with reflection caching for high-performance struct validation. The system uses struct tags (similar to JSON tags) to declaratively define validation rules, eliminating repetitive validation code.
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Validator Core │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Reflection │───>│ sync.Map │ │ Validation │ │
│ │ Parser │ │ Cache │<──│ Rules │ │
│ └──────────────┘ └──────────────┘ └───────────────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Field Metadata (index, name, rules[]) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
v
┌──────────────────┐
│ ValidationErrors │
│ ([]FieldError) │
└──────────────────┘
```
**Key Components:**
- **`Validator`** (`internal/validation/validator.go`) - Core reflection-based validator with caching
- **`ValidationRules`** (`internal/validation/rules.go`) - Built-in validation rule registry
- **`FieldError`** (`internal/validation/errors.go`) - Structured error types
- **`ContactFormRequest`** (`internal/validation/contact.go`) - Example struct with validation tags
### Performance Benefits
**Reflection Caching with sync.Map:**
- Struct metadata parsed **once** per type
- Subsequent validations use cached field metadata
- Thread-safe concurrent validation
- Zero GC pressure for metadata lookups
**Benchmark Results:**
```
V1 (manual validation): ~2000 ns/op
V2 (tag-based cached): ~1500 ns/op
```
## Tag Syntax
### Basic Format
```go
type MyStruct struct {
Field string `validate:"rule1,rule2=param,rule3"`
}
```
**Rules are comma-separated:**
- Simple rule: `required`
- Rule with parameter: `max=100`
- Multiple rules: `required,trim,max=100,email`
**Field name resolution:**
- Uses `json` tag name if present
- Falls back to struct field name
- Example: `Name string `json:"name"`` → field name is "name"
## Available Validation Rules
### 1. Required Fields
#### `required`
Validates that the field is not empty (after trimming whitespace).
```go
type User struct {
Name string `json:"name" validate:"required"`
}
```
**Error Message:** `"name is required"`
---
#### `optional`
Explicit marker for optional fields (always passes, used for documentation).
```go
type User struct {
Company string `json:"company" validate:"optional"`
}
```
### 2. String Transformations
#### `trim`
Auto-trims leading/trailing whitespace from the field value.
```go
type User struct {
Name string `json:"name" validate:"required,trim"`
}
```
**Behavior:**
- Transformation happens **before** validation
- Modifies the field value in-place
- UTF-8 aware
---
#### `sanitize`
HTML-escapes the field value and removes newlines from header fields.
```go
type ContactForm struct {
Message string `json:"message" validate:"required,trim,sanitize"`
}
```
**Transformation:**
- Trims whitespace
- Removes `\r` and `\n` characters
- HTML-escapes content (prevents XSS)
### 3. Length Validation
#### `min=N`
Validates minimum rune length (UTF-8 aware, not byte length).
```go
type Password struct {
Value string `json:"password" validate:"required,min=8"`
}
```
**Error Message:** `"password must be at least 8 characters"`
**Note:** Uses `utf8.RuneCountInString()` to support international characters correctly.
---
#### `max=N`
Validates maximum rune length (UTF-8 aware).
```go
type User struct {
Name string `json:"name" validate:"required,max=100"`
}
```
**Error Message:** `"name must be 100 characters or less"`
### 4. Format Validation
#### `email`
Validates email format per RFC 5322 (simplified).
```go
type User struct {
Email string `json:"email" validate:"required,email"`
}
```
**Validation Rules:**
- Length: 3-254 characters
- Must contain exactly one `@`
- Local part: max 64 characters
- Domain must contain at least one `.`
- Regex pattern: `/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/`
**Error Message:** `"Invalid email address format"`
---
#### `pattern=name|subject|company`
Validates against predefined regex patterns.
```go
type ContactForm struct {
Name string `json:"name" validate:"pattern=name"`
Subject string `json:"subject" validate:"pattern=subject"`
Company string `json:"company" validate:"pattern=company"`
}
```
**Supported Patterns:**
| Pattern | Regex | Description |
|---------|-------|-------------|
| `name` | `^[\p{L}\s'-]+$` | Letters (any language), spaces, hyphens, apostrophes |
| `subject` | `^[\p{L}\p{N}\s.,!?'"()\-:;#]+$` | Alphanumeric + safe punctuation including # |
| `company` | `^[\p{L}\p{N}\s.,&'()\-]+$` | Alphanumeric + business punctuation |
**Error Message:** `"name contains invalid characters for name (letters, spaces, hyphens, apostrophes only)"`
**Pre-compiled for Performance:**
All patterns are pre-compiled in `init()` for zero-allocation validation.
### 5. Security Validation
#### `no_injection`
Prevents email header injection attacks.
```go
type ContactForm struct {
Email string `json:"email" validate:"required,email,no_injection"`
Subject string `json:"subject" validate:"required,no_injection"`
}
```
**Detects:**
- Newline characters (`\r`, `\n`)
- Email header patterns (case-insensitive):
- `content-type:`
- `mime-version:`
- `bcc:`, `cc:`, `to:`, `from:`
- `subject:`, `reply-to:`
- `x-mailer:`
**Error Message:** `"email contains invalid characters (possible injection attempt)"`
**Security Note:** This prevents attackers from injecting additional email headers via form fields.
---
#### `honeypot`
Bot detection - field must be empty.
```go
type ContactForm struct {
Honeypot string `json:"website" validate:"honeypot"`
}
```
**Behavior:**
- Hidden field in form (CSS: `display: none`)
- Legitimate users never fill it
- Bots auto-fill all fields
**Error Message:** `"Bot detected"`
---
#### `timing=min:max`
Validates form submission timing to prevent bot submissions.
```go
type ContactForm struct {
Timestamp int64 `json:"timestamp" validate:"timing=2:86400"`
}
```
**Parameters:**
- `min`: Minimum seconds between page load and submit (e.g., `2`)
- `max`: Maximum seconds allowed (e.g., `86400` = 24 hours)
**Validation:**
- Timestamp is set when form loads (JavaScript)
- Submitted with form
- Server validates `now - timestamp` is within `[min, max]`
**Error Messages:**
- Too fast: `"Form submitted too quickly (bot detected)"`
- Invalid: `"Invalid timestamp"` (future or too old)
**Security Note:** Prevents automated bot submissions that submit forms instantly.
## Complete Example: ContactFormRequest
### Struct Definition
```go
package validation
type ContactFormRequest struct {
Name string `json:"name" validate:"required,trim,max=100,pattern=name,no_injection"`
Email string `json:"email" validate:"required,trim,max=254,email,no_injection"`
Company string `json:"company" validate:"optional,trim,max=100,pattern=company"`
Subject string `json:"subject" validate:"required,trim,max=200,pattern=subject,no_injection"`
Message string `json:"message" validate:"required,trim,max=5000,sanitize"`
Honeypot string `json:"website" validate:"honeypot"`
Timestamp int64 `json:"timestamp" validate:"timing=2:86400"`
}
```
### Validation Execution
```go
// Validate contact form
req := &ContactFormRequest{
Name: " Juan José ",
Email: "juan@example.com",
Subject: "Question about #golang",
Message: "<script>alert('xss')</script>Hello!",
Honeypot: "",
Timestamp: time.Now().Unix() - 5,
}
// V2 validation (tag-based with caching)
if err := ValidateContactFormV2(req); err != nil {
// Handle validation errors
if validationErrors, ok := err.(ValidationErrors); ok {
for _, fieldErr := range validationErrors {
fmt.Printf("%s: %s\n", fieldErr.Field, fieldErr.Message)
}
}
}
// After validation, req.Name is "Juan José" (trimmed)
// req.Message is "&lt;script&gt;alert('xss')&lt;/script&gt;Hello!" (sanitized)
```
### Validation Flow
```
1. Reflection Cache Lookup
├─> Cache Hit: Use cached field metadata
└─> Cache Miss: Parse struct, cache metadata
2. For Each Field:
├─> Apply Transformations (trim, sanitize)
│ └─> Update field value in-place
└─> Apply Validation Rules
├─> required: Check non-empty
├─> max=100: Check UTF-8 rune count
├─> pattern=name: Validate against regex
├─> no_injection: Check for malicious patterns
└─> Collect errors
3. Return Results
├─> Success: nil error
└─> Failure: ValidationErrors ([]FieldError)
```
## Error Handling
### FieldError Structure
```go
type FieldError struct {
Field string `json:"field"` // "name"
Tag string `json:"tag"` // "max"
Param string `json:"param,omitempty"` // "100"
Message string `json:"message"` // "name must be 100 characters or less"
}
```
### ValidationErrors (Multiple Errors)
```go
type ValidationErrors []FieldError
// Methods
func (ve ValidationErrors) Error() string
func (ve ValidationErrors) HasErrors() bool
func (ve ValidationErrors) GetFieldError(field string) *FieldError
func (ve ValidationErrors) GetFieldErrors(field string) []FieldError
```
### Error Handling Example
```go
err := ValidateContactFormV2(req)
if err != nil {
validationErrors, ok := err.(ValidationErrors)
if !ok {
// Not a validation error (e.g., struct type error)
return err
}
// Get specific field error
if nameErr := validationErrors.GetFieldError("name"); nameErr != nil {
fmt.Printf("Name error: %s\n", nameErr.Message)
}
// Get all errors for a field
emailErrors := validationErrors.GetFieldErrors("email")
for _, err := range emailErrors {
fmt.Printf("Email: %s (%s)\n", err.Message, err.Tag)
}
// Convert to JSON for API response
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"errors": validationErrors,
})
}
```
## V1 vs V2 Validation Comparison
### V1: Manual Validation (Legacy)
```go
func ValidateContactForm(req *ContactFormRequest) error {
// Honeypot check
if req.Honeypot != "" {
return &ValidationError{Field: "website", Message: "Bot detected"}
}
// Timing check
if req.Timestamp > 0 {
now := time.Now().Unix()
timeTaken := now - req.Timestamp
if timeTaken < 2 {
return &ValidationError{
Field: "timestamp",
Message: "Form submitted too quickly (bot detected)",
}
}
}
// Required fields
if strings.TrimSpace(req.Name) == "" {
return &ValidationError{Field: "name", Message: "Name is required"}
}
// ... 100+ more lines of manual validation ...
}
```
**Drawbacks:**
- Verbose and repetitive
- Error-prone (easy to forget validations)
- Hard to maintain
- No reusability across structs
### V2: Tag-Based Validation (Current)
```go
type ContactFormRequest struct {
Name string `json:"name" validate:"required,trim,max=100,pattern=name,no_injection"`
// ... more fields ...
}
func ValidateContactFormV2(req *ContactFormRequest) error {
return globalValidator.Validate(req)
}
```
**Benefits:**
- Declarative and self-documenting
- Consistent validation across all structs
- Reflection caching for performance
- Extensible with custom rules
- Type-safe with compile-time struct validation
## Extension Guide: Custom Validation Rules
### Step 1: Define Validation Function
```go
// ruleCustom validates against a custom pattern
func ruleCustom(field string, value string, param string) *FieldError {
if value == "" {
return nil // Skip validation for empty values
}
// Custom validation logic
if !isValid(value) {
return &FieldError{
Field: field,
Tag: "custom",
Param: param,
Message: field + " does not meet custom criteria",
}
}
return nil
}
```
### Step 2: Register Rule
```go
func init() {
validationRules["custom"] = ruleCustom
}
```
### Step 3: Use in Struct Tags
```go
type MyStruct struct {
Field string `json:"field" validate:"custom=param"`
}
```
### Example: URL Validation Rule
```go
func ruleURL(field string, value string, param string) *FieldError {
if value == "" {
return nil
}
if _, err := url.Parse(value); err != nil {
return &FieldError{
Field: field,
Tag: "url",
Message: field + " must be a valid URL",
}
}
return nil
}
func init() {
validationRules["url"] = ruleURL
}
```
**Usage:**
```go
type Website struct {
URL string `json:"url" validate:"required,url"`
}
```
## Thread Safety
### sync.Map for Caching
```go
type Validator struct {
cache sync.Map // map[reflect.Type]*structMeta
}
```
**Characteristics:**
- Thread-safe concurrent reads and writes
- Optimized for mostly-read workloads (perfect for caching)
- No locks needed for cache lookups
- Automatic memory management
### Safe Concurrent Validation
```go
// Global validator instance (shared across goroutines)
var globalValidator = NewValidator()
// Safe to call from multiple goroutines
func handler1(req *ContactFormRequest) error {
return globalValidator.Validate(req) // Thread-safe
}
func handler2(req *ContactFormRequest) error {
return globalValidator.Validate(req) // Thread-safe
}
```
## Best Practices
### 1. Use Appropriate Rule Order
```go
// ✅ GOOD: Transformations first, validations second
Name string `validate:"trim,required,max=100,pattern=name"`
// ❌ BAD: Validations before transformations
Name string `validate:"required,max=100,trim,pattern=name"`
```
**Why:** Transformations modify the value before validation runs.
### 2. Combine Security Rules
```go
// ✅ GOOD: Multiple layers of security
Email string `validate:"required,trim,max=254,email,no_injection"`
// ❌ BAD: Missing injection protection
Email string `validate:"required,email"`
```
### 3. Use Global Validator Instance
```go
// ✅ GOOD: Reuse cached metadata
var globalValidator = NewValidator()
func Validate(req interface{}) error {
return globalValidator.Validate(req)
}
// ❌ BAD: Creates new validator every time (no caching)
func Validate(req interface{}) error {
v := NewValidator()
return v.Validate(req)
}
```
### 4. Explicit Optional Fields
```go
// ✅ GOOD: Clearly marked as optional
Company string `validate:"optional,trim,max=100"`
// ⚠️ ACCEPTABLE: No validate tag (implicitly optional)
Company string `json:"company"`
```
### 5. UTF-8 Awareness
```go
// ✅ GOOD: max=100 counts runes (supports "José" = 4 runes)
Name string `validate:"max=100"`
// Note: Never use len() for validation - it counts bytes!
```
## Security Considerations
### 1. Email Header Injection Prevention
**Attack Vector:**
```
Subject: Hello\r\nBcc: attacker@evil.com\r\n\r\nInjected content
```
**Protection:**
```go
Subject string `validate:"no_injection"`
```
### 2. XSS Prevention
**Attack Vector:**
```
Message: <script>alert('XSS')</script>
```
**Protection:**
```go
Message string `validate:"sanitize"`
// Result: &lt;script&gt;alert('XSS')&lt;/script&gt;
```
### 3. Bot Detection
**Multi-Layer Approach:**
```go
type ContactForm struct {
Honeypot string `validate:"honeypot"` // Must be empty
Timestamp int64 `validate:"timing=2:86400"` // 2s-24h submission time
}
```
### 4. Safe HTML Handling
```go
// ⚠️ SECURITY WARNING: Only use safeHTML with trusted content
// NEVER use with user-generated content!
// ✅ GOOD: Sanitize user input
Message string `validate:"sanitize"`
// ❌ BAD: Trusting user HTML directly
Message template.HTML // DANGEROUS!
```
## Quick Reference
### Common Validation Patterns
```go
// Required text field with length limit
Name string `validate:"required,trim,max=100"`
// Email field
Email string `validate:"required,trim,max=254,email,no_injection"`
// Optional field with validation when provided
Company string `validate:"optional,trim,max=100,pattern=company"`
// Message with XSS protection
Message string `validate:"required,trim,max=5000,sanitize"`
// Honeypot bot trap
Honeypot string `validate:"honeypot"`
// Timing-based bot detection
Timestamp int64 `validate:"timing=2:86400"`
```
### Error Response Format
```json
{
"errors": [
{
"field": "name",
"tag": "max",
"param": "100",
"message": "name must be 100 characters or less"
},
{
"field": "email",
"tag": "email",
"message": "Invalid email address format"
}
]
}
```
## Performance Metrics
### Reflection Caching Impact
```
First validation (cold cache): ~2000 ns/op
Subsequent validations (warm): ~1500 ns/op
Cache hit rate: 99.9%
Memory overhead: ~500 bytes per struct type
```
### Pattern Compilation
```
Pre-compiled patterns (init): One-time cost
Pattern matching: Zero allocations
Regex cache: Global, shared
```
## Related Files
- `internal/validation/validator.go` - Core validator with caching
- `internal/validation/rules.go` - Validation rule implementations
- `internal/validation/errors.go` - Error types and methods
- `internal/validation/contact.go` - ContactFormRequest example and V1/V2 validation
## See Also
- [Template System Documentation](go-template-system.md)
- [Routes and API Documentation](go-routes-api.md)
+894
View File
@@ -0,0 +1,894 @@
# 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 &lt;script&gt; -->
</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>&lt;script&gt;alert('XSS')&lt;/script&gt;</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)
File diff suppressed because it is too large Load Diff