diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index b6809fc..e0b23bc 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -21,7 +21,7 @@ func (h *Handlers) HandleLoginPage() http.HandlerFunc { "Error": "", } - h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data) + h.renderTemplate(w, r, "login.html", data) } } @@ -44,7 +44,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc { "Error": "Username and password are required", } w.WriteHeader(http.StatusBadRequest) - h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data) + h.renderTemplate(w, r, "login.html", data) return } @@ -56,7 +56,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc { "Error": "Invalid username or password", } w.WriteHeader(http.StatusUnauthorized) - h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data) + h.renderTemplate(w, r, "login.html", data) return } @@ -74,7 +74,7 @@ func (h *Handlers) HandleLoginSubmit() http.HandlerFunc { "Error": "Invalid username or password", } w.WriteHeader(http.StatusUnauthorized) - h.renderTemplate(w, r, []string{"templates/base.html", "templates/login.html"}, data) + h.renderTemplate(w, r, "login.html", data) return } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 0183668..2819730 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -13,6 +13,7 @@ import ( "sneak.berlin/go/webhooker/internal/healthcheck" "sneak.berlin/go/webhooker/internal/logger" "sneak.berlin/go/webhooker/internal/session" + "sneak.berlin/go/webhooker/templates" ) // nolint:revive // HandlersParams is a standard fx naming convention @@ -26,11 +27,20 @@ type HandlersParams struct { } type Handlers struct { - params *HandlersParams - log *slog.Logger - hc *healthcheck.Healthcheck - db *database.Database - session *session.Session + params *HandlersParams + log *slog.Logger + hc *healthcheck.Healthcheck + db *database.Database + session *session.Session + templates map[string]*template.Template +} + +// parsePageTemplate parses a page-specific template set from the embedded FS. +// Each page template is combined with the shared base, htmlheader, and navbar templates. +func parsePageTemplate(pageFile string) *template.Template { + return template.Must( + template.ParseFS(templates.Templates, "htmlheader.html", "navbar.html", "base.html", pageFile), + ) } func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) { @@ -40,9 +50,16 @@ func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) { s.hc = params.Healthcheck s.db = params.Database s.session = params.Session + + // Parse all page templates once at startup + s.templates = map[string]*template.Template{ + "index.html": parsePageTemplate("index.html"), + "login.html": parsePageTemplate("login.html"), + "profile.html": parsePageTemplate("profile.html"), + } + lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - // FIXME compile some templates here or something return nil }, }) @@ -80,16 +97,11 @@ type UserInfo struct { Username string } -// renderTemplate renders a template with common data -func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templateFiles []string, data interface{}) { - // Always include the common templates - allTemplates := []string{"templates/htmlheader.html", "templates/navbar.html"} - allTemplates = append(allTemplates, templateFiles...) - - // Parse templates - tmpl, err := template.ParseFiles(allTemplates...) - if err != nil { - s.log.Error("failed to parse template", "error", err) +// renderTemplate renders a pre-parsed template with common data +func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, pageTemplate string, data interface{}) { + tmpl, ok := s.templates[pageTemplate] + if !ok { + s.log.Error("template not found", "template", pageTemplate) http.Error(w, "Internal server error", http.StatusInternalServerError) return } @@ -108,6 +120,16 @@ func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templa } } + // If data is a map, merge user info into it + if m, ok := data.(map[string]interface{}); ok { + m["User"] = userInfo + if err := tmpl.Execute(w, m); err != nil { + s.log.Error("failed to execute template", "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + return + } + // Wrap data with base template data type templateDataWrapper struct { User *UserInfo @@ -119,17 +141,6 @@ func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, templa Data: data, } - // If data is a map, merge user info into it - if m, ok := data.(map[string]interface{}); ok { - m["User"] = userInfo - if err := tmpl.Execute(w, m); err != nil { - s.log.Error("failed to execute template", "error", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - } - return - } - - // Otherwise use wrapper if err := tmpl.Execute(w, wrapper); err != nil { s.log.Error("failed to execute template", "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 83bf1dd..4acafca 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -87,10 +87,11 @@ func TestRenderTemplate(t *testing.T) { "Version": "1.0.0", } - // When templates don't exist, renderTemplate should return an error - h.renderTemplate(w, req, []string{"nonexistent.html"}, data) + // When a non-existent template name is requested, renderTemplate + // should return an internal server error + h.renderTemplate(w, req, "nonexistent.html", data) - // Should return internal server error when template parsing fails + // Should return internal server error when template is not found assert.Equal(t, http.StatusInternalServerError, w.Code) }) } diff --git a/internal/handlers/index.go b/internal/handlers/index.go index fb1a068..a08c765 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -34,7 +34,7 @@ func (s *Handlers) HandleIndex() http.HandlerFunc { } // Render the template - s.renderTemplate(w, req, []string{"templates/base.html", "templates/index.html"}, data) + s.renderTemplate(w, req, "index.html", data) } } diff --git a/internal/handlers/profile.go b/internal/handlers/profile.go index 2a36f12..88458be 100644 --- a/internal/handlers/profile.go +++ b/internal/handlers/profile.go @@ -54,6 +54,6 @@ func (h *Handlers) HandleProfile() http.HandlerFunc { } // Render the profile page - h.renderTemplate(w, r, []string{"templates/base.html", "templates/profile.html"}, data) + h.renderTemplate(w, r, "profile.html", data) } } diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..a87ad61 --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,8 @@ +package templates + +import ( + "embed" +) + +//go:embed *.html +var Templates embed.FS