package httpapi import ( "io/fs" "net/http" "path" "strings" "github.com/mithral/vellum/web" ) // mountWeb serves the embedded single-page app at the root path. // // Unknown non-API paths fall back to index.html so client-side routes work on // reload. API paths never fall back: a typo in an endpoint returns JSON, not // an HTML page. func (s *Server) mountWeb() { sub, err := fs.Sub(web.Dist, "dist") if err != nil { s.mux.HandleFunc("/", s.handleNotFound) return } index, err := fs.ReadFile(sub, "index.html") if err != nil { // Frontend not built into this binary. Keep JSON 404s rather than // pretending the app exists. s.log.Warn("web UI not embedded; run the frontend build to include it") s.mux.HandleFunc("/", s.handleNotFound) return } files := http.FileServer(http.FS(sub)) s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if isAPIPath(r.URL.Path) { s.handleNotFound(w, r) return } if r.URL.Path == "/" || r.URL.Path == "" { serveIndex(w, index) return } name := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if f, err := sub.Open(name); err == nil { _ = f.Close() files.ServeHTTP(w, r) return } // SPA route: hand back the shell and let the client router decide. 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) } func isAPIPath(p string) bool { return p == "/healthz" || strings.HasPrefix(p, "/api/") }