feat: add hashfs for static asset cache busting and live clock
All checks were successful
CI / Deploy / test (pull_request) Successful in 32s
CI / Deploy / lint (pull_request) Successful in 42s
CI / Deploy / deploy (pull_request) Has been skipped

- 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
This commit is contained in:
Ryan Hamamura
2026-03-03 13:26:52 -10:00
parent e0f5d555fb
commit 9a8fe4534d
9 changed files with 64 additions and 14 deletions

5
assets/assets.go Normal file
View File

@@ -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"

22
assets/static_dev.go Normal file
View File

@@ -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
}

26
assets/static_prod.go Normal file
View File

@@ -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)
}

View File

@@ -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() {
<div class="fixed top-2 right-2 flex items-center gap-1 text-xs opacity-50 font-mono">
<div class="status status-xs status-success"></div>
<div class="fixed top-2 right-2 flex items-center gap-1.5 text-xs opacity-60 font-mono">
<div style="width: 6px; height: 6px; border-radius: 50%; background-color: #22c55e;"></div>
{ time.Now().Format("15:04:05") }
</div>
}

View File

@@ -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) {
<head>
<title>{ title }</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<script defer type="module" src="/assets/js/datastar.js"></script>
<link href="/assets/css/output.css" rel="stylesheet" type="text/css"/>
<script defer type="module" src={ assets.StaticPath("js/datastar.js") }></script>
<link href={ assets.StaticPath("css/output.css") } rel="stylesheet" type="text/css"/>
</head>
<body class="flex flex-col h-screen">
if config.Global.Environment == config.Dev {

1
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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{

View File

@@ -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 {