package web import ( "embed" "html/template" "io" "io/fs" "path/filepath" "strings" ) //go:embed templates/* var templateFS embed.FS // Templates holds the parsed templates type Templates struct { templates map[string]*template.Template } // NewTemplates creates and parses all templates func NewTemplates() (*Templates, error) { t := &Templates{ templates: make(map[string]*template.Template), } // Load all layout and partial templates first layoutFiles, err := fs.Glob(templateFS, "templates/layouts/*.html") if err != nil { return nil, err } partialFiles, err := fs.Glob(templateFS, "templates/partials/*.html") if err != nil { return nil, err } // Load page templates pageFiles, err := fs.Glob(templateFS, "templates/pages/*.html") if err != nil { return nil, err } // Combine layouts and partials baseFiles := append(layoutFiles, partialFiles...) // Parse each page template with layouts and partials for _, pageFile := range pageFiles { files := append([]string{pageFile}, baseFiles...) // Read and parse all files tmpl := template.New(filepath.Base(pageFile)) for _, file := range files { content, err := templateFS.ReadFile(file) if err != nil { return nil, err } _, err = tmpl.Parse(string(content)) if err != nil { return nil, err } } // Extract page name (e.g., "login" from "templates/pages/login.html") name := strings.TrimSuffix(filepath.Base(pageFile), ".html") t.templates[name] = tmpl } // Also parse partials standalone for htmx partial responses for _, partialFile := range partialFiles { content, err := templateFS.ReadFile(partialFile) if err != nil { return nil, err } tmpl, err := template.New(filepath.Base(partialFile)).Parse(string(content)) if err != nil { return nil, err } name := "partial:" + strings.TrimSuffix(filepath.Base(partialFile), ".html") t.templates[name] = tmpl } return t, nil } // RenderPage renders a full page template func (t *Templates) RenderPage(w io.Writer, name string, data interface{}) error { tmpl, ok := t.templates[name] if !ok { return &ErrTemplateNotFound{Name: name} } return tmpl.ExecuteTemplate(w, "base", data) } // RenderPartial renders a partial template (for htmx responses) func (t *Templates) RenderPartial(w io.Writer, name string, data interface{}) error { tmpl, ok := t.templates["partial:"+name] if !ok { return &ErrTemplateNotFound{Name: name} } return tmpl.ExecuteTemplate(w, name, data) } // ErrTemplateNotFound is returned when a template is not found type ErrTemplateNotFound struct { Name string } func (e *ErrTemplateNotFound) Error() string { return "template not found: " + e.Name }