| 1 | package home |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | |
| 6 | "amadan.net/oficina/estilo.git/sky" |
| 7 | "bytes" |
| 8 | "embed" |
| 9 | "net/http" |
| 10 | "sync" |
| 11 | |
| 12 | "amadan.net/oficina/estilo.git/theme" |
| 13 | "github.com/go-chi/chi/v5" |
| 14 | ) |
| 15 | |
| 16 | //go:embed static/site |
| 17 | var siteFS embed.FS |
| 18 | |
| 19 | // The front page is a real page now, not the skeleton's bare fact. It is |
| 20 | // authored as a file rather than a Go string because it is long enough |
| 21 | // that a const would hide its diffs, and it has no dynamic content and no |
| 22 | // user input to escape — so it is served as bytes, with no template step |
| 23 | // standing between the file and the response. |
| 24 | // |
| 25 | // It is drawn in Calma, the suite's look: estilo's theme and Onest, the |
| 26 | // sky behind the hero, and each app's own colour from estilo's ring |
| 27 | // (appsCSS). Everything it links is on this origin; SecurityPolicy's |
| 28 | // `default-src 'self'` already admits the fonts and pictures, and the |
| 29 | // stylesheets are covered by `style-src 'self'`. |
| 30 | // |
| 31 | // Everything the page points at has to exist. The app links go to the six |
| 32 | // live app hosts; there is no sign-up form, because there is no route that |
| 33 | // would accept one (provisioning is still `oficina provision`, an operator |
| 34 | // CLI). A form posting into a 404 is worse than saying so plainly, and the |
| 35 | // page says so plainly. |
| 36 | var indexPage = siteAsset("index.html") |
| 37 | |
| 38 | func siteAsset(name string) []byte { |
| 39 | b, err := siteFS.ReadFile("static/site/" + name) |
| 40 | if err != nil { |
| 41 | panic(err) // Embedded above; unreachable unless the build is broken. |
| 42 | } |
| 43 | return b |
| 44 | } |
| 45 | |
| 46 | // appsCSS gives each app on the page its own accent: estilo's per-app |
| 47 | // accent blocks, scoped from :root to [data-app="<slug>"], so a card for |
| 48 | // Sheets tints green from the same ring the Sheets app itself uses, with |
| 49 | // no colour copied here to drift. |
| 50 | var appsCSS = func() []byte { |
| 51 | var b bytes.Buffer |
| 52 | b.WriteString("/* Each app's accent, from estilo's colour ring (theme.AccentCSS). */\n") |
| 53 | for _, a := range theme.Apps() { |
| 54 | block, _ := theme.AccentCSS(a.Slug) |
| 55 | b.Write(bytes.Replace(block, []byte(":root {"), []byte(`[data-app="`+a.Slug+`"] {`), 1)) |
| 56 | } |
| 57 | return b.Bytes() |
| 58 | }() |
| 59 | |
| 60 | // Public on both Home origins, and a closed list for the same reason |
| 61 | // mountBrand keeps one: a request path must never be treated as a file |
| 62 | // name. |
| 63 | func mountSite(r chi.Router) { |
| 64 | mountCSS(r, "/static/site/home.css", siteAsset("home.css")) |
| 65 | mountCSS(r, "/static/site/apps.css", appsCSS) |
| 66 | } |
| 67 | |
| 68 | // skyMarker is where the front page's sky goes: rendered from estilo's |
| 69 | // own template, so the page carries exactly the layers (and the inline |
| 70 | // placeholder) the pinned estilo draws, never a hand-copied set of URLs. |
| 71 | const skyMarker = "<!-- estilo-sky: the suite's sky, rendered from estilo's template on first serve (page.go) -->" |
| 72 | |
| 73 | func withSky(page []byte) []byte { |
| 74 | t := template.Must(template.ParseFS(sky.Templates(), "templates/*.html")) |
| 75 | var b bytes.Buffer |
| 76 | if err := t.ExecuteTemplate(&b, "estilo-sky", sky.For(sky.SuiteSeed)); err != nil { |
| 77 | panic(err) // estilo's own template over its own data; a broken pin |
| 78 | } |
| 79 | return bytes.Replace(page, []byte(skyMarker), b.Bytes(), 1) |
| 80 | } |
| 81 | |
| 82 | // servedIndex is the front page with every stylesheet and picture linked |
| 83 | // by its content version. Built on first serve, once the routers have |
| 84 | // mounted (and so versioned) everything the page links. |
| 85 | var ( |
| 86 | servedIndexOnce sync.Once |
| 87 | servedIndex []byte |
| 88 | ) |
| 89 | |
| 90 | // handleIndex serves the front page. |
| 91 | func (a *App) handleIndex(w http.ResponseWriter, _ *http.Request) { |
| 92 | servedIndexOnce.Do(func() { servedIndex = versionStaticRefs(withSky(indexPage)) }) |
| 93 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 94 | w.Write(servedIndex) |
| 95 | } |
| 96 | |