package web import ( "crypto/sha256" "encoding/hex" "io/fs" "net/http" "sync" ) // The pages, scripts and styles are embedded, so they only change with a new build. They're served // with Cache-Control: no-cache and an ETag (a hash of the file): browsers, the Android app and // Cloudflare check each time and get a quick 304 while the file is unchanged, and a new deploy // shows up at once. Without these headers Cloudflare tells browsers to keep them for 4 hours. var assetTags = sync.OnceValue(func() map[string]string { tags := map[string]string{} static, _ := fs.Sub(assets, "assets") _ = fs.WalkDir(static, ".", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } data, err := fs.ReadFile(static, path) if err != nil { return err } sum := sha256.Sum256(data) tags[path] = `"` + hex.EncodeToString(sum[:8]) + `"` return nil }) return tags }) // revalidate sets the caching headers for an embedded file. http.ServeContent answers a matching // If-None-Match with 304 from the ETag set here. func revalidate(w http.ResponseWriter, name string) { w.Header().Set("Cache-Control", "no-cache") if tag, ok := assetTags()[name]; ok { w.Header().Set("ETag", tag) } } // serveAsset serves one embedded file, see revalidate. func serveAsset(w http.ResponseWriter, r *http.Request, static fs.FS, name string) { revalidate(w, name) http.ServeFileFS(w, r, static, name) }