carlos / contra Public

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

Plain git — no account needed to clone.

Download

Download this file

1package contra
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 "github.com/carlosframework/contra/internal/contra/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", "errors", "signup", "signup_sent", "signup_passphrase", "signup_passkey", "signin", "vault", "device", "kit"} {
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/contra/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 Funcs(template.FuncMap{"asset": assets.Path}).
44 ParseFS(ui.Templates(), "*.html")).
45 // Then the app's own: the shell and this page's body.
46 ParseFS(appFS, "templates/layout.html", "templates/"+name+".html"))
47 }
48}
49
50// render executes name's layout into a buffer before anything touches
51// the wire: a template error becomes a clean 500 instead of garbage
52// appended to a half-written page, and headers (flash cookies, say)
53// stay settable until the status line is written.
54func render(w http.ResponseWriter, name string, data any) {
55 // A name with no template is a wiring mistake, not a runtime one. Without
56 // this it is a nil-map dereference inside html/template, which panics in
57 // the handler and reports a segfault rather than the missing page.
58 page, ok := pages[name]
59 if !ok {
60 http.Error(w, "something went wrong", http.StatusInternalServerError)
61 return
62 }
63
64 var buf bytes.Buffer
65 if err := page.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/contra/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