package main import ( "crypto/sha256" "encoding/hex" "fmt" "html/template" "io/fs" "net/http" "strings" ) // assetVersions maps each embedded asset (path relative to assets/, e.g. // "page/toc.js") to a short hash of its content. Templates reference assets // through assetURL, which appends the hash as ?v=…, so a versioned URL names // one exact file content and can be cached forever; a rebuilt asset gets a // new URL. var assetVersions = hashAssets() func hashAssets() map[string]string { versions := map[string]string{} _ = fs.WalkDir(assets, "assets", func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } b, err := assets.ReadFile(p) if err != nil { return err } sum := sha256.Sum256(b) versions[strings.TrimPrefix(p, "assets/")] = hex.EncodeToString(sum[:])[:12] return nil }) return versions } // assetURL is the "asset" template func: {{asset "style.css"}} renders // /_/style.css?v=. An unknown name fails the template so a typo or a // removed file surfaces as a render error instead of a silent 404. func assetURL(name string) (string, error) { v, ok := assetVersions[name] if !ok { return "", fmt.Errorf("unknown asset %q", name) } return "/_/" + name + "?v=" + v, nil } // assetFuncs is for the standalone fragment templates that do not take the // full tmplFuncs. var assetFuncs = template.FuncMap{"asset": assetURL} // serveStatic serves the embedded assets under /_/. Embedded files carry no // modtime, so http.FileServer alone sends no validators and browsers refetch // every asset on every page. Instead: // - a URL carrying the current ?v= hash is immutable; // - fonts are immutable too (referenced unversioned from style.css, never // change); // - anything else (unversioned or stale ?v=, e.g. icons from CSS) gets the // content hash as ETag and revalidates, answered with 304 when unchanged. func serveStatic() http.Handler { staticFS, _ := fs.Sub(assets, "assets") static := http.StripPrefix("/_/", http.FileServer(http.FS(staticFS))) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/_/") v, ok := assetVersions[name] switch { case ok && r.URL.Query().Get("v") == v, strings.HasPrefix(name, "fonts/"): w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") case ok: w.Header().Set("Cache-Control", "no-cache") w.Header().Set("ETag", `"`+v+`"`) } static.ServeHTTP(w, r) }) }