carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movement
2
3import (
4 "bytes"
5 "embed"
6 "html/template"
7 "net/http"
8
9 "github.com/carlosframework/rastrillo"
10 "github.com/carlosframework/rastrillo/ui"
11
12 "amadan.net/carlos/movement/internal/movement/icons"
13)
14
15//go:embed templates static
16var appFS embed.FS
17
18// assets fingerprints static/: {{asset "static/tokens.css"}} in a
19// template returns a content-hashed URL, served immutable-cacheable by
20// the handler App mounts.
21var assets = rastrillo.NewAssets(appFS)
22
23// pages is one *template.Template per page, each combining layout.html
24// with that page's own file — kept separate so every page can define a
25// template named "content" without colliding with the others.
26var pages = map[string]*template.Template{}
27
28func init() {
29 for _, name := range []string{"index", "stance", "sent", "confirm", "signin", "admin", "errors"} {
30 pages[name] = template.Must(
31 // ui's partials and their helpers first, so every page can
32 // call one without registering anything. WithIcons points
33 // ui's two icon seams at the app's own set
34 // (internal/movement/icons, scaffolded by rastrillo new) —
35 // both of them, always: passing only icon would leave
36 // iconAssets on the framework default and silently break
37 // cdn and js delivery. iconAssets is whatever <head> markup
38 // the chosen delivery needs, empty for the vendored-inline
39 // default, so the layout calls it unconditionally and
40 // switching delivery later needs no template edit.
41 template.Must(template.New("layout").
42 Funcs(ui.Funcs(ui.WithIcons(icons.Icon, icons.Assets))).
43 // say is how every user-facing string reaches a page. No
44 // template writes a literal sentence: a guard test in
45 // movementtest fails the build if one does, so that
46 // unreviewed copy cannot arrive by being typed into markup.
47 //
48 // Named "say" rather than "t" on purpose — ui.Funcs already
49 // registers T for the framework's own strings, and two
50 // lookups differing only in case is a mistake waiting to be
51 // made in a template nobody type-checks.
52 Funcs(template.FuncMap{"asset": assets.Path, "say": T}).
53 ParseFS(ui.Templates(), "*.html")).
54 // Then the app's own: the shell and this page's body.
55 ParseFS(appFS, "templates/layout.html", "templates/"+name+".html"))
56 }
57}
58
59// render executes name's layout into a buffer before anything touches
60// the wire: a template error becomes a clean 500 instead of garbage
61// appended to a half-written page, and headers (flash cookies, say)
62// stay settable until the status line is written.
63func render(w http.ResponseWriter, name string, data any) {
64 var buf bytes.Buffer
65 if err := pages[name].ExecuteTemplate(&buf, "layout", data); err != nil {
66 http.Error(w, "something went wrong", http.StatusInternalServerError)
67 return
68 }
69 buf.WriteTo(w)
70}
71
72// ErrorPage renders the "errors" page — templates/errors.html, which
73// wraps ui's error-page partial in the layout — for status inside ref
74// (rastrillo.NewRef, or empty when there is none to show). It is the
75// rastrillo.ErrorPageFunc cmd/movement/main.go points Options.ErrorPage at,
76// so a panic gets the same page a handled failure would; wire it to
77// Ctx.ErrorPage too as the app grows handlers of its own.
78func ErrorPage(w http.ResponseWriter, r *http.Request, status int, ref string) {
79 w.WriteHeader(status)
80 render(w, "errors", map[string]any{"Status": status, "Ref": ref})
81}
82