package main import ( "bytes" "embed" "html/template" "log/slog" "net/http" "net/url" "strconv" "time" ) //go:embed templates static var assetFS embed.FS var funcs = template.FuncMap{ "fidate": func(t time.Time) string { return t.Local().Format("2.1.2006 15:04") }, "score": func(f *float64) string { return strconv.FormatFloat(*f, 'f', 1, 64) }, } // Each page is parsed with the layout into its own set, so two pages may both define "content". var pages = map[string]*template.Template{} func init() { entries, err := assetFS.ReadDir("templates") if err != nil { panic(err) } for _, e := range entries { if e.Name() == "layout.html" { continue } if e.IsDir() { continue } pages[e.Name()] = template.Must(template.New("layout.html").Funcs(funcs). ParseFS(assetFS, "templates/layout.html", "templates/partials/*.html", "templates/"+e.Name())) } } // page is everything the layout needs, plus whatever the page itself wants in Data. type page struct { Title string Member *member Admin bool Flash string Path string Narrow bool // auth pages are a 420px column Data any } func (a *app) render(w http.ResponseWriter, r *http.Request, status int, name string, p page) { t, ok := pages[name] if !ok { slog.Error("unknown template", "name", name) http.Error(w, "template", http.StatusInternalServerError) return } p.Member = memberFrom(r.Context()) p.Path = r.URL.Path p.Flash = a.takeFlash(w, r) // Render to memory first: a template that fails halfway must not leave a half-written 200. var buf bytes.Buffer if err := t.ExecuteTemplate(&buf, "layout.html", p); err != nil { slog.Error("render", "name", name, "error", err) http.Error(w, "template", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) buf.WriteTo(w) } // Toasts are a cookie rendered server-side and cleared on read — no JS, no session storage. func (a *app) flash(w http.ResponseWriter, msg string) { http.SetCookie(w, &http.Cookie{ Name: "flash", Value: url.QueryEscape(msg), Path: "/", HttpOnly: true, Secure: a.cfg.secureCookies, SameSite: http.SameSiteLaxMode, }) } func (a *app) takeFlash(w http.ResponseWriter, r *http.Request) string { c, err := r.Cookie("flash") if err != nil || c.Value == "" { return "" } http.SetCookie(w, &http.Cookie{ Name: "flash", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: a.cfg.secureCookies, SameSite: http.SameSiteLaxMode, }) msg, err := url.QueryUnescape(c.Value) if err != nil { return "" } return msg }