SPAHandler serves static files from the given FS, falling back to index.html for SPA routing. index.html is served with Cache-Control: no-cache so browsers always fetch the latest version. Hashed asset files (JS/CSS) rely on Vite content-hash filenames and can be cached long-term.
(fsys fs.FS)
| 9 | // index.html is served with Cache-Control: no-cache so browsers always fetch the latest version. |
| 10 | // Hashed asset files (JS/CSS) rely on Vite content-hash filenames and can be cached long-term. |
| 11 | func SPAHandler(fsys fs.FS) http.Handler { |
| 12 | fileServer := http.FileServer(http.FS(fsys)) |
| 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 14 | path := r.URL.Path |
| 15 | if len(path) > 0 && path[0] == '/' { |
| 16 | path = path[1:] |
| 17 | } |
| 18 | if path == "" { |
| 19 | path = "index.html" |
| 20 | } |
| 21 | |
| 22 | f, err := fsys.Open(path) |
| 23 | if err == nil { |
| 24 | _ = f.Close() |
| 25 | if path == "index.html" || path == "" { |
| 26 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") |
| 27 | w.Header().Set("Pragma", "no-cache") |
| 28 | w.Header().Set("Expires", "0") |
| 29 | } |
| 30 | fileServer.ServeHTTP(w, r) |
| 31 | return |
| 32 | } |
| 33 | |
| 34 | // fall back to index.html for client-side routing |
| 35 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") |
| 36 | w.Header().Set("Pragma", "no-cache") |
| 37 | w.Header().Set("Expires", "0") |
| 38 | r2 := r.Clone(r.Context()) |
| 39 | r2.URL.Path = "/" |
| 40 | fileServer.ServeHTTP(w, r2) |
| 41 | }) |
| 42 | } |