package main import ( "embed" "flag" "fmt" "io/fs" "log" "net/http" "net/http/httputil" "net/url" "os" "strings" ) //go:embed dist/* var distFS embed.FS func main() { port := flag.Int("port", 1112, "port to listen on") apiTarget := flag.String("api", "http://localhost:8000", "backend API URL to proxy /api requests") flag.Parse() // Proxy for /api -> backend backendURL, err := url.Parse(*apiTarget) if err != nil { log.Fatalf("invalid api url: %v", err) } proxy := httputil.NewSingleHostReverseProxy(backendURL) originalDirector := proxy.Director proxy.Director = func(r *http.Request) { originalDirector(r) // Rewrite /api/... -> /... r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") if r.URL.Path == "" { r.URL.Path = "/" } r.Host = backendURL.Host } // Static files from embedded dist/ distSub, err := fs.Sub(distFS, "dist") if err != nil { log.Fatalf("failed to open embedded dist: %v", err) } fileServer := http.FileServer(http.FS(distSub)) mux := http.NewServeMux() // API proxy mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) }) // SPA: serve static files, fallback to index.html mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Try to serve the file directly path := strings.TrimPrefix(r.URL.Path, "/") if path == "" { path = "index.html" } // Check if file exists in embedded FS if f, err := distSub.Open(path); err == nil { f.Close() fileServer.ServeHTTP(w, r) return } // Fallback to index.html for SPA routing indexFile, err := fs.ReadFile(distSub, "index.html") if err != nil { http.Error(w, "index.html not found", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(indexFile) }) addr := fmt.Sprintf(":%d", *port) log.Printf("Serving on http://localhost%s", addr) log.Printf("API proxy -> %s", *apiTarget) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) os.Exit(1) } }