package gateway import ( "net/http" "path" "strings" ) // spa wraps the API mux with the SPA file server. Requests under /api/ and /ws/ // are served by the API handler; every other path is served from the configured // frontend filesystem with a fallback to index.html so client-side routes (e.g. // /chat) resolve in a single-page app. func (s *Server) spa(api http.Handler) http.Handler { fileServer := s.spaFileServer() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if isAPIRoute(r.URL.Path) { api.ServeHTTP(w, r) return } fileServer.ServeHTTP(w, r) }) } // spaFileServer serves static assets, falling back to index.html for paths that // are not real files in the embedded/external dist. func (s *Server) spaFileServer() http.Handler { fsrv := http.FileServer(http.FS(s.frontend)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rel := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if rel != "" { if f, err := s.frontend.Open(rel); err == nil { _ = f.Close() fsrv.ServeHTTP(w, r) return } } r2 := r.Clone(r.Context()) r2.URL.Path = "/" fsrv.ServeHTTP(w, r2) }) } // isAPIRoute reports whether path belongs to the REST/WS API surface the // gateway owns, which the SPA handler must leave untouched. func isAPIRoute(p string) bool { return strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ws/") }