Commit 0a4bb99d63

0a4bb99d63653021f509101d804ddcf516f75722

parent: 7effe3feb4

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-04 01:37 UTC

web: parse every page template once, at start-up

Render parsed the layout and the page on every request, and a template
that did not parse failed the first visit to its page rather than the
process. All page templates parse with the layout at package init;
Render looks the page up. TestEveryPageTemplateParses asserts the set.

Closes #116

Layout: unified · split

internal/web/web.go +37 −5
@@ -4,10 +4,13 @@ package web
4 4
5import ( 5import (
6 "embed" 6 "embed"
7 "fmt"
7 "html/template" 8 "html/template"
8 "io" 9 "io"
10 "io/fs"
9 "reflect" 11 "reflect"
10 "runtime/debug" 12 "runtime/debug"
13 "sort"
11 "strconv" 14 "strconv"
12 "strings" 15 "strings"
13 "sync" 16 "sync"
@@ -225,13 +228,42 @@ var funcs = template.FuncMap{
225 }, 228 },
226} 229}
227 230
231// pages holds every page template parsed once with the layout, at
232// start-up: a template that does not parse fails the process before it
233// serves anything, rather than the first visit to a rarely-hit page, and
234// a request no longer re-parses the layout (#116).
235var pages = func() map[string]*template.Template {
236 entries, err := fs.ReadDir(templateFS, "templates")
237 if err != nil {
238 panic(err)
239 }
240 m := map[string]*template.Template{}
241 for _, e := range entries {
242 name := e.Name()
243 if name == "layout.html" || !strings.HasSuffix(name, ".html") {
244 continue
245 }
246 layout := template.Must(template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html"))
247 m[name] = template.Must(layout.ParseFS(templateFS, "templates/"+name))
248 }
249 return m
250}()
251
252// Pages lists the page template names, for tests that render each one.
253func Pages() []string {
254 names := make([]string, 0, len(pages))
255 for name := range pages {
256 names = append(names, name)
257 }
258 sort.Strings(names)
259 return names
260}
261
228// Render executes the named page template with the shared layout. 262// Render executes the named page template with the shared layout.
229func Render(w io.Writer, page string, data any) error { 263func Render(w io.Writer, page string, data any) error {
230 t, err := template.Must( 264 t, ok := pages[page]
231 template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html"), 265 if !ok {
232 ).ParseFS(templateFS, "templates/"+page) 266 return fmt.Errorf("no page template %q", page)
233 if err != nil {
234 return err
235 } 267 }
236 return t.ExecuteTemplate(w, "layout", data) 268 return t.ExecuteTemplate(w, "layout", data)
237} 269}
internal/web/web_test.go added +19
@@ -0,0 +1,19 @@
1package web
2
3import "testing"
4
5// Every page template parses with the layout at start-up, so a broken
6// template fails the process rather than the first visit to its page.
7// Package init has already done the work; this asserts it covered every
8// file (#116).
9func TestEveryPageTemplateParses(t *testing.T) {
10 names := Pages()
11 if len(names) < 30 {
12 t.Fatalf("only %d page templates parsed: %v", len(names), names)
13 }
14 for _, want := range []string{"tree.html", "blob.html", "mr.html", "issue.html", "dashboard.html", "registered.html"} {
15 if _, ok := pages[want]; !ok {
16 t.Errorf("%s not parsed", want)
17 }
18 }
19}