package main import ( "fmt" "net/http" "os" "path" "path/filepath" "strings" ) // Frontend (Vite build) als SPA ausliefern: Dateien aus dist, sonst index.html func registerFrontend(mux *http.ServeMux) { // Kandidaten: zuerst ENV, dann typische Ordner candidates := []string{ strings.TrimSpace(os.Getenv("FRONTEND_DIST")), "web/dist", "dist", } var distAbs string for _, c := range candidates { if c == "" { continue } abs, err := resolvePathRelativeToApp(c) if err != nil { continue } if fi, err := os.Stat(filepath.Join(abs, "index.html")); err == nil && !fi.IsDir() { distAbs = abs break } } if distAbs == "" { fmt.Println("⚠️ Frontend dist nicht gefunden (tried: FRONTEND_DIST, frontend/dist, dist) – API läuft trotzdem.") return } fmt.Println("🖼️ Frontend dist:", distAbs) fileServer := http.FileServer(http.Dir(distAbs)) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // /api bleibt bei deinen API-Routen (längeres Pattern gewinnt), // aber falls mal was durchrutscht: if strings.HasPrefix(r.URL.Path, "/api/") { http.NotFound(w, r) return } // 1) Wenn echte Datei existiert -> ausliefern reqPath := r.URL.Path if reqPath == "" || reqPath == "/" { // index.html w.Header().Set("Cache-Control", "no-store") http.ServeFile(w, r, filepath.Join(distAbs, "index.html")) return } // URL-Pfad in Dateisystem-Pfad umwandeln (ohne Traversal) clean := path.Clean("/" + reqPath) // path.Clean (für URL-Slashes) rel := strings.TrimPrefix(clean, "/") onDisk := filepath.Join(distAbs, filepath.FromSlash(rel)) if fi, err := os.Stat(onDisk); err == nil && !fi.IsDir() { // Statische Assets ruhig cachen (Vite hashed assets) ext := strings.ToLower(filepath.Ext(onDisk)) if ext != "" && ext != ".html" { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "no-store") } fileServer.ServeHTTP(w, r) return } // 2) SPA-Fallback: alle "Routen" ohne Datei -> index.html w.Header().Set("Cache-Control", "no-store") http.ServeFile(w, r, filepath.Join(distAbs, "index.html")) }) } func makeFrontendHandler() (http.Handler, bool) { // Kandidaten: zuerst ENV, dann typische Ordner candidates := []string{ strings.TrimSpace(os.Getenv("FRONTEND_DIST")), "web/dist", "dist", } var distAbs string for _, c := range candidates { if c == "" { continue } abs, err := resolvePathRelativeToApp(c) if err != nil { continue } if fi, err := os.Stat(filepath.Join(abs, "index.html")); err == nil && !fi.IsDir() { distAbs = abs break } } if distAbs == "" { fmt.Println("⚠️ Frontend dist nicht gefunden (tried: FRONTEND_DIST, web/dist, dist) – API läuft trotzdem.") return nil, false } fmt.Println("🖼️ Frontend dist:", distAbs) fileServer := http.FileServer(http.Dir(distAbs)) h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // /api bleibt API if strings.HasPrefix(r.URL.Path, "/api/") { http.NotFound(w, r) return } reqPath := r.URL.Path if reqPath == "" || reqPath == "/" { w.Header().Set("Cache-Control", "no-store") http.ServeFile(w, r, filepath.Join(distAbs, "index.html")) return } // URL-Pfad in Dateisystem-Pfad umwandeln (ohne Traversal) clean := path.Clean("/" + reqPath) rel := strings.TrimPrefix(clean, "/") onDisk := filepath.Join(distAbs, filepath.FromSlash(rel)) if fi, err := os.Stat(onDisk); err == nil && !fi.IsDir() { ext := strings.ToLower(filepath.Ext(onDisk)) if ext != "" && ext != ".html" { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "no-store") } fileServer.ServeHTTP(w, r) return } // SPA-Fallback w.Header().Set("Cache-Control", "no-store") http.ServeFile(w, r, filepath.Join(distAbs, "index.html")) }) return h, true }