diff --git a/internal/middleware/cache.go b/internal/middleware/cache.go new file mode 100644 index 0000000..0291f3c --- /dev/null +++ b/internal/middleware/cache.go @@ -0,0 +1,28 @@ +package middleware + +import "net/http" + +// CacheStatic wraps a handler to set long-lived cache headers for static assets. +// Assets use query-string cache busting (?v=), so it's safe to cache +// them indefinitely — the URL changes when the content changes. +func CacheStatic(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + h.ServeHTTP(w, r) + }) +} + +// NoCacheDynamic sets Cache-Control: no-cache on responses so browsers always +// revalidate with the server. This prevents stale HTML from being shown after +// navigation (e.g. back button) while still allowing conditional requests. +func NoCacheDynamic(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Skip static assets — they're handled by CacheStatic. + if len(r.URL.Path) >= 8 && r.URL.Path[:8] == "/assets/" { + next.ServeHTTP(w, r) + return + } + w.Header().Set("Cache-Control", "no-cache") + next.ServeHTTP(w, r) + }) +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 1612132..ec5a87d 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -22,9 +22,9 @@ func SetupRoutes(a *app.App) http.Handler { // PUBLIC ROUTES // ==================================================================================== - // Static + // Static assets with long-lived cache (cache-busted via ?v=) sub, _ := fs.Sub(assets.AssetsFS, ".") - mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))) + mux.Handle("GET /assets/", middleware.CacheStatic(http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))) // Home mux.HandleFunc("GET /{$}", home.HomePage) @@ -142,6 +142,7 @@ func SetupRoutes(a *app.App) http.Handler { mux, middleware.Config(a.Cfg), middleware.RequestLogging, + middleware.NoCacheDynamic, middleware.CSRFProtection, middleware.AuthMiddleware(a.AuthService, a.UserService, a.ProfileService), middleware.WithURLPath,