From 9a8fe4534df166f8b8b839e3cb112360a9d47f26 Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:26:52 -1000 Subject: [PATCH] feat: add hashfs for static asset cache busting and live clock - Add assets package with dev/prod build tags - Dev: serve from filesystem with Cache-Control: no-store - Prod: use hashfs for cache-busting URLs - Add LiveClock component to show SSE connection status - Update templates to use StaticPath for asset URLs --- assets/assets.go | 5 +++++ assets/static_dev.go | 22 +++++++++++++++++++++ assets/static_prod.go | 26 +++++++++++++++++++++++++ features/common/components/shared.templ | 4 ++-- features/common/layouts/base.templ | 5 +++-- go.mod | 1 + go.sum | 2 ++ main.go | 6 +----- router/router.go | 7 ++----- 9 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 assets/assets.go create mode 100644 assets/static_dev.go create mode 100644 assets/static_prod.go diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..837ab85 --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,5 @@ +// Package assets provides static file serving with build-tag switching +// between live filesystem (dev) and embedded hashfs (prod). +package assets + +const DirectoryPath = "assets" diff --git a/assets/static_dev.go b/assets/static_dev.go new file mode 100644 index 0000000..4b4776c --- /dev/null +++ b/assets/static_dev.go @@ -0,0 +1,22 @@ +//go:build dev + +package assets + +import ( + "net/http" + "os" + + "github.com/rs/zerolog/log" +) + +func Handler() http.Handler { + log.Debug().Str("path", DirectoryPath).Msg("static assets served from filesystem") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + http.StripPrefix("/assets/", http.FileServerFS(os.DirFS(DirectoryPath))).ServeHTTP(w, r) + }) +} + +func StaticPath(path string) string { + return "/assets/" + path +} diff --git a/assets/static_prod.go b/assets/static_prod.go new file mode 100644 index 0000000..6a17b54 --- /dev/null +++ b/assets/static_prod.go @@ -0,0 +1,26 @@ +//go:build !dev + +package assets + +import ( + "embed" + "net/http" + + "github.com/benbjohnson/hashfs" + "github.com/rs/zerolog/log" +) + +var ( + //go:embed css js + staticFiles embed.FS + staticSys = hashfs.NewFS(staticFiles) +) + +func Handler() http.Handler { + log.Debug().Msg("static assets are embedded with hashfs") + return hashfs.FileServer(staticSys) +} + +func StaticPath(path string) string { + return "/" + staticSys.HashName(path) +} diff --git a/features/common/components/shared.templ b/features/common/components/shared.templ index d310227..b9f9d2d 100644 --- a/features/common/components/shared.templ +++ b/features/common/components/shared.templ @@ -51,8 +51,8 @@ templ NicknamePrompt(returnPath string) { // LiveClock shows the current server time, updated with each SSE patch. // If the clock stops updating, users know the connection is stale. templ LiveClock() { -
-
+
+
{ time.Now().Format("15:04:05") }
} diff --git a/features/common/layouts/base.templ b/features/common/layouts/base.templ index 5029e7f..e779665 100644 --- a/features/common/layouts/base.templ +++ b/features/common/layouts/base.templ @@ -1,6 +1,7 @@ package layouts import ( + "github.com/ryanhamamura/games/assets" "github.com/ryanhamamura/games/config" "github.com/ryanhamamura/games/version" ) @@ -11,8 +12,8 @@ templ Base(title string) { { title } - - + + if config.Global.Environment == config.Dev { diff --git a/go.mod b/go.mod index ad1794a..e7ffb13 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect github.com/aws/smithy-go v1.24.0 // indirect + github.com/benbjohnson/hashfs v0.2.2 // indirect github.com/bep/godartsass/v2 v2.5.0 // indirect github.com/bep/golibsass v1.2.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect diff --git a/go.sum b/go.sum index 150f2e4..eeb1ae1 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/hashfs v0.2.2 h1:vFZtksphM5LcnMRFctj49jCUkCc7wp3NP6INyfjkse4= +github.com/benbjohnson/hashfs v0.2.2/go.mod h1:7OMXaMVo1YkfiIPxKrl7OXkUTUgWjmsAKyR+E6xDIRM= github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= diff --git a/main.go b/main.go index 34cde03..9ad9f0b 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "embed" "fmt" "log/slog" "net" @@ -29,9 +28,6 @@ import ( "github.com/ryanhamamura/games/version" ) -//go:embed assets -var assets embed.FS - func main() { ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() @@ -97,7 +93,7 @@ func run(ctx context.Context) error { sessionManager.LoadAndSave, ) - router.SetupRoutes(r, queries, sessionManager, nc, store, snakeStore, assets) + router.SetupRoutes(r, queries, sessionManager, nc, store, snakeStore) // HTTP server srv := &http.Server{ diff --git a/router/router.go b/router/router.go index 216509f..083c7ac 100644 --- a/router/router.go +++ b/router/router.go @@ -2,8 +2,6 @@ package router import ( - "embed" - "io/fs" "net/http" "sync" @@ -12,6 +10,7 @@ import ( "github.com/nats-io/nats.go" "github.com/starfederation/datastar-go/datastar" + "github.com/ryanhamamura/games/assets" "github.com/ryanhamamura/games/config" "github.com/ryanhamamura/games/connect4" "github.com/ryanhamamura/games/db/repository" @@ -31,11 +30,9 @@ func SetupRoutes( nc *nats.Conn, store *connect4.Store, snakeStore *snake.SnakeStore, - assets embed.FS, ) { // Static assets - subFS, _ := fs.Sub(assets, "assets") - router.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServerFS(subFS))) + router.Handle("/assets/*", assets.Handler()) // Hot-reload for development if config.Global.Environment == config.Dev {