// Package web embeds the SvelteKit SPA build output and serves it over HTTP. // Requests for real files under build/ are served verbatim; anything else — // deep links like /artists/abc, missing asset paths — returns build/index.html // so the SPA can resolve the route client-side. package web import ( "embed" "io/fs" "net/http" "path" "strings" "git.fabledsword.com/bvandeusen/minstrel/internal/config" ) //go:embed all:build var buildFS embed.FS // Handler returns an http.Handler that serves the embedded SPA. // // Behavior: // - GET / -> build/index.html // - GET / -> that file, via http.FileServer // - GET / -> build/index.html (SPA fallback) // // Callers are expected to register this as the router's NotFound/catch-all so // explicitly-routed paths (like /api/* and /rest/*) take precedence. // // The branding parameter is applied to index.html once at construction via // html/template; all requests serve the resulting cached bytes. func Handler(branding config.BrandingConfig) http.Handler { sub, err := fs.Sub(buildFS, "build") if err != nil { // fs.Sub on a valid embed path can only fail if the embed directive // is malformed, which the compile would have caught. panic("web: fs.Sub(build) failed: " + err.Error()) } raw, err := fs.ReadFile(sub, "index.html") if err != nil { panic("web: embedded build/index.html missing: " + err.Error()) } index, err := applyBrandingTemplate(string(raw), branding) if err != nil { panic("web: branding template failed: " + err.Error()) } fileServer := http.FileServer(http.FS(sub)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { clean := path.Clean(r.URL.Path) if clean == "/" || clean == "." { serveIndex(w, index) return } name := strings.TrimPrefix(clean, "/") if info, err := fs.Stat(sub, name); err == nil && !info.IsDir() { fileServer.ServeHTTP(w, r) return } serveIndex(w, index) }) } func serveIndex(w http.ResponseWriter, index []byte) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") _, _ = w.Write(index) }