// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 package web import ( "bytes" "crypto/sha256" "embed" "encoding/hex" "io/fs" "net/http" "path" "strings" "time" ) // adminAssets carries the interface sheets, the self-hosted fonts and the // glyph sprite of the admin UI, packaged next to the templates so one // binary serves the whole product. // //go:embed static/*.css static/graphis.svg static/fonts/*.woff2 var adminAssets embed.FS // asset is one packaged file with its content type and an ETag derived // from its bytes. type asset struct { body []byte contentType string etag string } // assets indexes the packaged files by their URL tail under // /admin/assets/. The index doubles as the allow-list: a path the binary // does not hold is a 404, so no traversal can reach the disk. var assets = func() map[string]*asset { entries, err := fs.ReadDir(adminAssets, "static") if err != nil { panic("web: embedded assets unreadable: " + err.Error()) } out := map[string]*asset{} add := func(name, tail, contentType string) { body, err := adminAssets.ReadFile(name) if err != nil { panic("web: embedded asset unreadable: " + name) } sum := sha256.Sum256(body) out[tail] = &asset{ body: body, contentType: contentType, etag: `"` + hex.EncodeToString(sum[:])[:16] + `"`, } } for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() switch { case strings.HasSuffix(name, ".css"): add("static/"+name, name, "text/css; charset=utf-8") case strings.HasSuffix(name, ".svg"): add("static/"+name, name, "image/svg+xml; charset=utf-8") } } fonts, err := fs.ReadDir(adminAssets, "static/fonts") if err != nil { panic("web: embedded fonts unreadable: " + err.Error()) } for _, font := range fonts { add("static/fonts/"+font.Name(), "fonts/"+font.Name(), "font/woff2") } return out }() // AssetHandler serves one embedded asset by its tail under // /admin/assets/. Responses are revalidatable: the ETag is the content, // so a deploy refreshes every page while a visit fetches nothing new. func AssetHandler(w http.ResponseWriter, r *http.Request) { tail := path.Clean(strings.TrimPrefix(r.URL.Path, "/admin/assets/")) found, ok := assets[strings.TrimPrefix(tail, "./")] if !ok || tail == "." { http.NotFound(w, r) return } w.Header().Set("Content-Type", found.contentType) w.Header().Set("ETag", found.etag) w.Header().Set("Cache-Control", "public, max-age=0, must-revalidate") http.ServeContent(w, r, tail, time.Time{}, bytes.NewReader(found.body)) }