package api import ( "embed" "io/fs" "net/http" "path/filepath" "strings" ) // spaFS holds the built Vite SPA. // The directory backend/internal/api/webapp/ is populated by the Docker // multi-stage build (node → copy dist → go build). // A placeholder file keeps the embed valid when building without Docker. //go:embed webapp var spaFS embed.FS // SPAHandler serves the Vite SPA under the given prefix (e.g. "/app"). // Static assets (paths with file extensions) are served directly. // All other paths fall back to index.html for client-side routing. func SPAHandler(prefix string) http.Handler { sub, err := fs.Sub(spaFS, "webapp") if err != nil { return http.NotFoundHandler() } fileServer := http.FileServer(http.FS(sub)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Strip the mount prefix to get the file path path := strings.TrimPrefix(r.URL.Path, prefix) if path == "" || path == "/" { // Serve index.html r2 := r.Clone(r.Context()) r2.URL.Path = "/index.html" fileServer.ServeHTTP(w, r2) return } // Has a file extension → serve asset directly (JS, CSS, fonts, …) if filepath.Ext(path) != "" { r2 := r.Clone(r.Context()) r2.URL.Path = path fileServer.ServeHTTP(w, r2) return } // SPA route → serve index.html r2 := r.Clone(r.Context()) r2.URL.Path = "/index.html" fileServer.ServeHTTP(w, r2) }) }