Static asset pipeline: embed.FS + AssetVersion (bookshelf-qqz.8) #9

Merged
zombor merged 4 commits from bd-bookshelf-qqz.8 into main 2026-05-20 18:08:49 +00:00
Owner

Summary

  • Adds static/ package with //go:embed declaration covering css/*.css, js/app.js, js/controllers/, js/vendor/, fonts/, and img/
  • Version(fs.FS) string: SHA-256 walk in sorted-path order, returns first 8 hex chars; deterministic and sensitive to any file change
  • Handler(embed.FS) http.Handler: strips /static/ prefix, sets Cache-Control: max-age=31536000, immutable on every response (including 404s) via a cacheWriter wrapper that re-applies the header at WriteHeader time (Go stdlib serveError intentionally clears caller-set headers on error paths)
  • static/css/main.css: hand-written, ~110 lines, :root CSS variables (--accent, --bg, --fg, --bg-card, --border, --radius, font stacks), base styles for body/nav/main/links/headings/.card
  • static/js/app.js: placeholder comment; Stimulus bootstrap deferred to F-09
  • static/fonts/.gitkeep, static/img/.gitkeep: track dirs before real assets land

Coverage gate

scripts/check-coverage.sh updated to include ./static/... in both -coverpkg and the test run so the 100% gate applies to static/ too. Makefile test target also updated.

Test plan

  • make test — all 12 Static Suite specs pass, all other suites unchanged
  • make coverage — 100% on both internal/ and static/
  • Handler: GET /static/css/main.css → 200 + Cache-Control set
  • Handler: GET /static/missing.txt → 404 + Cache-Control set
  • cacheWriter.Write without prior WriteHeader correctly sets Cache-Control
  • Version(): deterministic (same FS → same hash twice)
  • Version(): sensitive (different content → different hash, different path → different hash)
  • Version(): skips files whose Open returns an error without panicking
  • Version(): handles empty FS

Closes bead bookshelf-qqz.8 on merge.

## Summary - Adds `static/` package with `//go:embed` declaration covering `css/*.css`, `js/app.js`, `js/controllers/`, `js/vendor/`, `fonts/`, and `img/` - `Version(fs.FS) string`: SHA-256 walk in sorted-path order, returns first 8 hex chars; deterministic and sensitive to any file change - `Handler(embed.FS) http.Handler`: strips `/static/` prefix, sets `Cache-Control: max-age=31536000, immutable` on every response (including 404s) via a `cacheWriter` wrapper that re-applies the header at `WriteHeader` time (Go stdlib `serveError` intentionally clears caller-set headers on error paths) - `static/css/main.css`: hand-written, ~110 lines, `:root` CSS variables (`--accent`, `--bg`, `--fg`, `--bg-card`, `--border`, `--radius`, font stacks), base styles for `body`/`nav`/`main`/links/headings/`.card` - `static/js/app.js`: placeholder comment; Stimulus bootstrap deferred to F-09 - `static/fonts/.gitkeep`, `static/img/.gitkeep`: track dirs before real assets land ## Coverage gate `scripts/check-coverage.sh` updated to include `./static/...` in both `-coverpkg` and the test run so the 100% gate applies to `static/` too. `Makefile` `test` target also updated. ## Test plan - [x] `make test` — all 12 Static Suite specs pass, all other suites unchanged - [x] `make coverage` — 100% on both `internal/` and `static/` - [x] Handler: GET /static/css/main.css → 200 + Cache-Control set - [x] Handler: GET /static/missing.txt → 404 + Cache-Control set - [x] cacheWriter.Write without prior WriteHeader correctly sets Cache-Control - [x] Version(): deterministic (same FS → same hash twice) - [x] Version(): sensitive (different content → different hash, different path → different hash) - [x] Version(): skips files whose Open returns an error without panicking - [x] Version(): handles empty FS Closes bead bookshelf-qqz.8 on merge.
Implements F-08 of the foundation epic:
- static/static.go: //go:embed declaration covering css/*.css, js/app.js,
  js/controllers/, js/vendor/, fonts/, and img/ via all: prefix for dirs
  that only carry .gitkeep until F-09 lands.
- static/version.go: Version(fs.FS) walks files in sorted-path order and
  returns the first 8 hex chars of SHA-256(path + content) per file.
  Deterministic; any change to path or bytes changes the hash.
- static/handler.go: Handler(embed.FS) wraps http.FileServer with
  StripPrefix("/static/") and a cacheWriter that injects
  Cache-Control: max-age=31536000, immutable on every response,
  including 404s (Go stdlib serveError clears caller-set headers; the
  wrapper re-applies the header at WriteHeader time).
- static/css/main.css: hand-written with :root CSS variables, minimal
  base styles for body/nav/main/links/headings/.card. ~110 lines.
- static/js/app.js: placeholder comment; Stimulus bootstrap comes in F-09.
- static/fonts/.gitkeep, static/img/.gitkeep: track dirs before real assets.
- 100% test coverage on static/ package (ginkgo v2 + gomega).
- scripts/check-coverage.sh: extended -coverpkg and test run to include
  ./static/... so the coverage gate enforces 100% on static/ too.
- Makefile: test target now includes ./static/... alongside ./internal/...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zombor force-pushed bd-bookshelf-qqz.8 from 1b391e64e9 to ae77531fc4
Some checks failed
/ Test (pull_request) Failing after 2s
/ Coverage (pull_request) Has been skipped
/ Lint (pull_request) Successful in 1m12s
2026-05-20 17:58:02 +00:00
Compare
ci: re-trigger after runner port conflict (bookshelf-qqz.8)
All checks were successful
/ Lint (pull_request) Successful in 49s
/ Test (pull_request) Successful in 1m24s
/ Coverage (pull_request) Successful in 1m18s
66e9863fee
The previous CI run failed because port 3307 was already allocated by
a concurrent MySQL service container from another PR's test run.
This empty commit re-triggers CI after the runner has freed the port.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore: gitignore coverage_static.out alongside coverage.out
Some checks failed
/ Lint (pull_request) Has been cancelled
/ Coverage (pull_request) Has been cancelled
/ Test (pull_request) Has been cancelled
980f9a704a
The static package coverage run produces coverage_static.out; broaden
the .gitignore glob from coverage.out to coverage*.out so all ad-hoc
coverage profiles are ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ci): remove static host port mapping for MySQL service
All checks were successful
/ Lint (pull_request) Successful in 1m12s
/ Test (pull_request) Successful in 1m31s
/ Coverage (pull_request) Successful in 1m7s
0d098e9aa8
Port 3307:3306 caused 'port already allocated' failures when multiple CI
jobs ran concurrently. Service containers are accessible via service name
(mysql:3306) within the job's Docker network; host port mapping is not needed.
zombor merged commit 7bfa9ce50c into main 2026-05-20 18:08:49 +00:00
zombor referenced this pull request from a commit 2026-05-20 18:08:50 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/pergamum!9
No description provided.