Files
cv-site/internal/templates/template.go
T
juanatsap a90d923956 fix: restore HTML rendering in ShortDescription fields
Previously, HTML in short descriptions was being escaped and displayed
as raw text instead of rendering properly. This happened because the
safeHTML template function had been removed for security reasons.

Changes:
- Added safeHTML function back to template.FuncMap (template.go:53-55)
- Updated three template locations to use safeHTML pipe:
  * Experience descriptions (cv-content.html:122)
  * Award descriptions (cv-content.html:180)
  * Project descriptions (cv-content.html:232)

Security note:
The safeHTML function is safe to use here because CV data comes from
trusted YAML files controlled by the site owner, not user input.
Clear documentation added to prevent misuse with untrusted content.

Examples now rendering correctly:
- Award: "Premio por excelencia en marketing B2B...con <a href=...>Clicplan</a>"
- Projects: Links to Lidering, Jorpack, Delivery Bikes BCN, Mobbeel
2025-11-12 11:52:52 +00:00

106 lines
2.5 KiB
Go

package templates
import (
"fmt"
"html/template"
"log"
"path/filepath"
"sync"
"github.com/juanatsap/cv-site/internal/config"
)
// Manager handles template parsing and rendering
type Manager struct {
templates *template.Template
config *config.TemplateConfig
mu sync.RWMutex
}
// NewManager creates a new template manager
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
}
// loadTemplates parses all templates from the configured directory
func (m *Manager) loadTemplates() error {
m.mu.Lock()
defer m.mu.Unlock()
// Create template with custom functions
funcMap := template.FuncMap{
"iterate": func(count int) []int {
var result []int
for i := 0; i < count; i++ {
result = append(result, i)
}
return result
},
"eq": func(a, b string) bool {
return a == b
},
// safeHTML marks string as safe HTML to prevent escaping
// SECURITY NOTE: Only use with trusted content from CV YAML files
// Never use with user-generated content to prevent XSS attacks
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
}
// Parse main templates
pattern := filepath.Join(m.config.Dir, "*.html")
tmpl, err := template.New("").Funcs(funcMap).ParseGlob(pattern)
if err != nil {
return fmt.Errorf("error parsing templates from %s: %w", pattern, err)
}
// Try to parse partials if they exist
partialsPattern := filepath.Join(m.config.PartialsDir, "*.html")
partials, _ := filepath.Glob(partialsPattern)
if len(partials) > 0 {
tmpl, err = tmpl.ParseGlob(partialsPattern)
if err != nil {
log.Printf("Warning: error parsing partials: %v", err)
}
}
m.templates = tmpl
log.Printf("✓ Templates loaded successfully from %s", m.config.Dir)
return nil
}
// Reload reloads all templates (useful for hot-reload in development)
func (m *Manager) Reload() error {
return m.loadTemplates()
}
// Render executes a template with the given data
func (m *Manager) Render(name string) (*template.Template, error) {
// Hot reload in development mode
if m.config.HotReload {
if err := m.Reload(); err != nil {
log.Printf("Warning: template reload failed: %v", err)
// Continue with cached templates
}
}
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
}