Files
bookhoard/templates/bundles.go
T
John O'Keefe 4c43caa639 fix(search): dropdown dead on htmx-less pages; Alpine-side debounce; auto bundle stamps
The "top search bar doesn't work" report had TWO stacked causes, found
by reproducing with real keypress-by-keypress typing instead of
Playwright's fill() (which fires a single input event and masked both):

1. htmx.min.js was included PER PAGE. The header's live search is
   htmx-driven on every page, but five pages rendering @Header —
   series, admin, admin hash-conflicts, admin processing-issues,
   browse-detail — never loaded it: the input's hx-* attributes were
   inert (htmx-internal-data absent), so typing produced no request at
   all. The include now lives in the header partial itself (classic
   script, before the module bundle); the sixteen per-page copies on
   @Header pages are removed (double-processing guard). Non-header
   pages (login/register/setup/reader/index) keep their own.

2. htmx's delay: modifier on a PROGRAMMATICALLY dispatched custom event
   (htmx.trigger) loses the request entirely under rapid successive
   triggers — every re-fire canceled the pending timer and the last
   never issued, so fast typing (the normal kind) never opened the
   dropdown while one-shot fill() worked. The debounce moved into the
   Alpine component (client-local state, where it belongs); htmx now
   executes the bare `search` event immediately.

Also made bundle staleness structurally impossible: each bundle's
script stamp is a content hash computed at startup (templates/
bundles.go), so a rebuild that changes bundle bytes changes its cache
key with no manual ?v=N bump — the manual stamp was how the first
dropdown fix stayed invisible for a deploy. The reader bundle (never
stamped at all) is covered too. HTML pages additionally send
Cache-Control: no-cache (they are dynamic), so a stale page can no
longer reference a stale stamp; /api, /static and /uploads keep their
native caching, pinned by a middleware test.
2026-10-27 23:33:52 -04:00

47 lines
1.5 KiB
Go

package templates
import "sync"
// Bundle versions: a short content hash per static JS bundle, set once
// at server startup. Script tags render /static/<bundle>.js?v=<hash>,
// so any rebuild that changes a bundle's bytes automatically changes
// its cache key — browsers fetch the new bundle with no manual stamp
// bumps (the manual ?v=N stamp is exactly how a fixed search dropdown
// stayed broken for one deploy: the HTML kept pointing at the old,
// cached URL).
//
// An unset bundle renders ?v=0; the /static route also sends
// Cache-Control: no-cache, so worst case browsers revalidate — the
// hash is the fast path, not the correctness mechanism.
var bundleVersionsMu sync.RWMutex
var bundleVersions = map[string]string{}
// SetBundleVersion records the content hash for a bundle ("main.js").
// Called once at startup for each built bundle.
func SetBundleVersion(name, hash string) {
bundleVersionsMu.Lock()
defer bundleVersionsMu.Unlock()
bundleVersions[name] = hash
}
// BundleVersion returns the recorded version for a bundle, or "0".
func BundleVersion(name string) string {
bundleVersionsMu.RLock()
defer bundleVersionsMu.RUnlock()
if v, ok := bundleVersions[name]; ok && v != "" {
return v
}
return "0"
}
// MainJSVersion is the templ-side accessor for the main bundle stamp.
func MainJSVersion() string {
return BundleVersion("main.js")
}
// ReaderJSVersion is the templ-side accessor for the reader bundle stamp.
func ReaderJSVersion() string {
return BundleVersion("reader.js")
}