mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-09 21:25:34 +00:00
Cache templates
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBuildPagesComposeAndExecute verifies every content page is pre-built with
|
||||
// the shared layouts and can execute against each root layout without a parse
|
||||
// or template-resolution error. This guards the startup-time template
|
||||
// composition (each page redefines content/title/header onto the base set).
|
||||
func TestBuildPagesComposeAndExecute(t *testing.T) {
|
||||
pages, err := buildPages()
|
||||
if err != nil {
|
||||
t.Fatalf("buildPages: %v", err)
|
||||
}
|
||||
if len(pages) == 0 {
|
||||
t.Fatal("buildPages returned no pages")
|
||||
}
|
||||
// The shared partials are not pages and must not be rendered directly.
|
||||
for _, name := range []string{"base.html", "icons.html"} {
|
||||
if _, ok := pages[name]; ok {
|
||||
t.Errorf("%s should not be registered as a page", name)
|
||||
}
|
||||
}
|
||||
|
||||
layouts := []string{"base", "authbase", "landingbase"}
|
||||
for name, tmpl := range pages {
|
||||
for _, layout := range layouts {
|
||||
if tmpl.Lookup(layout) == nil {
|
||||
t.Errorf("page %s missing layout %q", name, layout)
|
||||
continue
|
||||
}
|
||||
// Execute with empty data: this catches references to undefined
|
||||
// partials (e.g. {{template "missing"}}). Runtime errors driven by
|
||||
// missing data (e.g. len of an absent map key) are expected here and
|
||||
// don't indicate a composition problem.
|
||||
err := tmpl.ExecuteTemplate(io.Discard, layout, map[string]any{})
|
||||
if err != nil && strings.Contains(err.Error(), "no such template") {
|
||||
t.Errorf("execute %s with layout %q: %v", name, layout, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-16
@@ -28,12 +28,15 @@ var staticFS embed.FS
|
||||
|
||||
// Server holds the dependencies shared by HTTP handlers.
|
||||
type Server struct {
|
||||
store *store.Store
|
||||
auth *auth.Service
|
||||
identity *identity.Service
|
||||
cfg *config.Config
|
||||
templates *template.Template
|
||||
router chi.Router
|
||||
store *store.Store
|
||||
auth *auth.Service
|
||||
identity *identity.Service
|
||||
cfg *config.Config
|
||||
// pages holds each content page pre-composed with the shared layouts and
|
||||
// partials, keyed by file name (e.g. "dashboard.html"). Built once at
|
||||
// startup since the templates are embedded and never change at runtime.
|
||||
pages map[string]*template.Template
|
||||
router chi.Router
|
||||
// lookupTXT resolves DNS TXT records; injectable so domain verification is
|
||||
// testable. Defaults to net.LookupTXT.
|
||||
lookupTXT func(name string) ([]string, error)
|
||||
@@ -41,15 +44,55 @@ type Server struct {
|
||||
|
||||
// NewServer constructs the HTTP server and its routes.
|
||||
func NewServer(st *store.Store, authSvc *auth.Service, idSvc *identity.Service, cfg *config.Config) (*Server, error) {
|
||||
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
|
||||
pages, err := buildPages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{store: st, auth: authSvc, identity: idSvc, cfg: cfg, templates: tmpl, lookupTXT: net.LookupTXT}
|
||||
s := &Server{store: st, auth: authSvc, identity: idSvc, cfg: cfg, pages: pages, lookupTXT: net.LookupTXT}
|
||||
s.routes()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// sharedTemplates are the layouts and partials shared by every page (the root
|
||||
// layouts live in base.html; reusable snippets like the icon set in icons.html).
|
||||
// They define no "content"/"title" blocks of their own, so they can be the
|
||||
// common base each page is composed onto.
|
||||
var sharedTemplates = []string{"templates/base.html", "templates/icons.html"}
|
||||
|
||||
// buildPages composes each content page with the shared layouts/partials once,
|
||||
// returning a map keyed by the page's file name. Each page redefines the
|
||||
// "content"/"title"/"header" blocks, so every page needs its own template set
|
||||
// rather than one shared set (where the blocks would collide).
|
||||
func buildPages() (map[string]*template.Template, error) {
|
||||
base, err := template.New("").ParseFS(templatesFS, sharedTemplates...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all, err := fs.Glob(templatesFS, "templates/*.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared := map[string]bool{}
|
||||
for _, p := range sharedTemplates {
|
||||
shared[p] = true
|
||||
}
|
||||
pages := map[string]*template.Template{}
|
||||
for _, p := range all {
|
||||
if shared[p] {
|
||||
continue
|
||||
}
|
||||
clone, err := base.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := clone.ParseFS(templatesFS, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages[strings.TrimPrefix(p, "templates/")] = clone
|
||||
}
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
// Handler returns the root HTTP handler.
|
||||
func (s *Server) Handler() http.Handler { return s.router }
|
||||
|
||||
@@ -188,14 +231,9 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, page string, dat
|
||||
data["CanAdmin"] = u.CapManageUsers || u.CapManageCatalog
|
||||
}
|
||||
}
|
||||
// Clone so we can associate the page's blocks without mutating the shared set.
|
||||
t, err := s.templates.Clone()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := t.ParseFS(templatesFS, "templates/"+page); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
t, ok := s.pages[page]
|
||||
if !ok {
|
||||
http.Error(w, "unknown page: "+page, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Pages may opt into an alternate root layout (e.g. the centered "authbase"
|
||||
|
||||
Reference in New Issue
Block a user