package main import ( "bytes" "compress/gzip" "crypto/sha256" "encoding/hex" "fmt" "html/template" "io/fs" "net/http" "strings" "time" ) // 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 } // assetGzip holds a gzip copy of every embedded asset that compresses well, // keyed like assetVersions. Built once at startup so serving compressed costs // no CPU per request; already-compressed files (fonts) are left out. var assetGzip = gzipAssets() func gzipAssets() map[string][]byte { out := map[string][]byte{} _ = 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 } var buf bytes.Buffer zw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression) zw.Write(b) zw.Close() // Not worth a Content-Encoding round trip unless it saves ≥10%. if buf.Len() < len(b)*9/10 { out[strings.TrimPrefix(p, "assets/")] = buf.Bytes() } return nil }) return out } // acceptsGzip reports whether the client accepts a gzip response body. func acceptsGzip(r *http.Request) bool { for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") { enc, q, _ := strings.Cut(strings.TrimSpace(enc), ";") if strings.TrimSpace(enc) == "gzip" && strings.ReplaceAll(q, " ", "") != "q=0" { return true } } return false } // 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. // // Assets with a precompressed copy (see assetGzip) are sent gzipped to // clients that accept it; the gzip variant gets its own ETag. 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] gz, hasGz := assetGzip[name] useGz := hasGz && acceptsGzip(r) if hasGz { w.Header().Set("Vary", "Accept-Encoding") } 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") if useGz { w.Header().Set("ETag", `"`+v+`-gz"`) } else { w.Header().Set("ETag", `"`+v+`"`) } } if useGz { w.Header().Set("Content-Encoding", "gzip") // ServeContent derives Content-Type from the name's extension. http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(gz)) return } static.ServeHTTP(w, r) }) }