// Package frontend serves the built SPA. It owns exactly the app routes // (/, /login, /dashboard) plus static assets; everything else is 404 here and // handled by the redirect command in production. package frontend import ( "net/http" "os" "path" "path/filepath" ) type Server struct { dist string fs http.Handler spa map[string]bool indexAbs string } func New(dist string) *Server { return &Server{ dist: dist, fs: http.FileServer(http.Dir(dist)), spa: map[string]bool{"/": true, "/login": true, "/dashboard": true}, indexAbs: filepath.Join(dist, "index.html"), } } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ok"}`)) }) mux.HandleFunc("/", s.serve) return mux } func (s *Server) serve(w http.ResponseWriter, r *http.Request) { clean := path.Clean(r.URL.Path) // App routes always render the SPA shell. if s.spa[clean] { http.ServeFile(w, r, s.indexAbs) return } // Real static file (assets, favicon, …)? if clean != "/" { full := filepath.Join(s.dist, filepath.FromSlash(clean)) if st, err := os.Stat(full); err == nil && !st.IsDir() { s.fs.ServeHTTP(w, r) return } } // Unknown path: not this command's concern. http.NotFound(w, r) }