package frontend import ( "RenewCMS/internal/infrastructure/assets" "io" "io/fs" "net/http" "path" "strings" "time" "github.com/go-chi/chi/v5" ) const hashedAssetsPrefix = "assets/" const ( hashedAssetCacheControl = "public, max-age=31536000, immutable" staticFileCacheControl = "public, max-age=3600" indexCacheControl = "no-store, no-cache, must-revalidate" ) const devNotice = "RenewCMS was built with -tags nofrontend: the SPA is served by the Vite dev server (http://localhost:5173)." type spaHandler struct { staticFS fs.FS modTime time.Time } func NewFrontendRouter() http.Handler { r := chi.NewRouter() if assets.Dist == nil { r.Get("/*", serveDevNotice) return r } handler := &spaHandler{ staticFS: assets.Dist, modTime: time.Now(), } r.Get("/*", handler.ServeHTTP) return r } func serveDevNotice(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusNotFound) _, _ = io.WriteString(w, devNotice) } func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := h.cleanPath(r.URL.Path) if h.fileExists(path) { h.serveAsset(w, r, path) return } h.serveIndex(w, r) } func (h *spaHandler) cleanPath(p string) string { return strings.TrimPrefix(path.Clean("/"+p), "/") } func (h *spaHandler) fileExists(path string) bool { if path == "" || path == "." { return false } f, err := h.staticFS.Open(path) if err != nil { return false } defer f.Close() stat, err := f.Stat() return err == nil && !stat.IsDir() } func (h *spaHandler) serveAsset(w http.ResponseWriter, r *http.Request, path string) { if strings.HasPrefix(path, hashedAssetsPrefix) { w.Header().Set("Cache-Control", hashedAssetCacheControl) } else { w.Header().Set("Cache-Control", staticFileCacheControl) } http.FileServer(http.FS(h.staticFS)).ServeHTTP(w, r) } func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) { f, err := h.staticFS.Open("index.html") if err != nil { http.Error(w, "Index not found", http.StatusNotFound) return } defer f.Close() content, ok := f.(io.ReadSeeker) if !ok { http.Error(w, "File not seekable", http.StatusInternalServerError) return } w.Header().Set("Cache-Control", indexCacheControl) http.ServeContent(w, r, "index.html", h.modTime, content) }