package webapp import ( "embed" "fmt" "html/template" "io" "io/fs" "strings" "sync" ) //go:embed templates/*.html static/* var contentFS embed.FS // staticFS exposes the embedded static assets (CSS, JS, favicon) so // they can be served by http.FileServer. There is no CDN dependency; // everything ships inside the worker binary. var staticFS = mustSubFS("static") // mustSubFS returns the named sub-filesystem inside contentFS, or // panics if it is missing. The embed directive above guarantees the // dir exists at build time. func mustSubFS(name string) fs.FS { sub, err := fs.Sub(contentFS, name) if err != nil { panic(fmt.Sprintf("webapp: embedded sub %q: %v", name, err)) } return sub } // Templates is a thin wrapper around html/template that lazily // parses the embedded *.html files on first Execute. The FuncMap is // intentionally minimal so adding global helpers does not blow up // the per-page test surface. type Templates struct { mu sync.RWMutex cache map[string]*template.Template funcs template.FuncMap } // loadTemplates parses every embedded template into the cache. The // shared layout (templates/layout.html) is parsed into every page // so each {{template "content" .}} substitution resolves cleanly. func loadTemplates() (*Templates, error) { t := &Templates{ cache: map[string]*template.Template{}, funcs: template.FuncMap{ "fmtBytes": fmtBytes, "fmtPercent": fmtPercent, "fmtDuration": fmtDuration, "fmtTime": fmtTime, "fmtBool": fmtBool, "hasPrefix": strings.HasPrefix, "sanitizeName": sanitizeName, }, } pages, err := fs.ReadDir(contentFS, "templates") if err != nil { return nil, fmt.Errorf("webapp: read templates: %w", err) } for _, p := range pages { if p.IsDir() || !strings.HasSuffix(p.Name(), ".html") { continue } if _, err := t.parse(p.Name()); err != nil { return nil, fmt.Errorf("webapp: parse %s: %w", p.Name(), err) } } return t, nil } // parse loads the named template plus the shared layout. Page // templates "{{define" body"}}" themselves. func (t *Templates) parse(name string) (*template.Template, error) { tmpl := template.New(name).Funcs(t.funcs) if _, err := tmpl.ParseFS(contentFS, "templates/layout.html"); err != nil { return nil, err } if _, err := tmpl.ParseFS(contentFS, "templates/"+name); err != nil { return nil, err } t.cache[name] = tmpl return tmpl, nil } // Execute renders the named page template into w. The page is // wrapped by the shared layout block via the "layout" template // defined in templates/layout.html. func (t *Templates) Execute(w io.Writer, name string, data interface{}) error { t.mu.RLock() tmpl, ok := t.cache[name] t.mu.RUnlock() if !ok { return fmt.Errorf("webapp: no such template %q", name) } return tmpl.ExecuteTemplate(w, "layout", data) }