72 lines
2.2 KiB
Go
72 lines
2.2 KiB
Go
// 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 /<path that exists in build/> -> that file, via http.FileServer
|
|
// - GET /<anything else> -> 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)
|
|
}
|