boardwalk
HTTP handler that serves the embedded admin SPA build under a configurable path prefix.
import "git.golem15.com/golem15/summercms/modules/boardwalk"
Name: The walkway along the beach front; it is the path admins take into the admin SPA.
Overview
boardwalk embeds the compiled admin SPA (dist/, produced by npm --prefix admin run build) into the binary and serves it. The build is path-agnostic: index.html carries a placeholder token that the handler replaces once, at construction, with the prefix the admin is mounted under, so one build works at any backend URI. The handler also computes a build id, so an open admin tab can tell that a new version was deployed. cabana mounts it when it activates the admin routes. It stands in for the server-rendered backend layouts of WinterCMS, which the Go port replaces with a single-page app.
Role in the framework
A small leaf with no framework dependencies; cabana is its only importer. It exists on its own because it owns the generated admin SPA bundle in dist/, which is rebuilt and drift-checked separately from Go code, and because its handler is easy to test against an in-memory file system. Application code normally does not import it: cabana mounts it under the backend prefix.
Use cases:
- Serve the admin SPA from the application binary with no separate frontend deployment.
- Move the admin to another path by changing the backend URI; the bundle is rewritten for the prefix at boot.
- Serve as the pattern to copy when a plugin embeds its own static site (the embedding and cache headers, not the admin-only security headers).
Features
- Serves the embedded build under any prefix, rewriting relative asset URLs and the admin base meta in
index.htmlfor that prefix (boardwalk.RewriteIndex). - Fails at boot when
index.htmllacks theboardwalk.BaseTokenplaceholder, which catches a stale or hand-edited build. - Falls back to
index.htmlfor client-side routes and directories; missing files with an extension get a plain 404. - Hands every request whose path under the prefix is
apior starts withapi/to a caller-supplied handler, so admin API misses stay JSON instead of returning the SPA. - Long-lived immutable caching for hashed files under
assets/,no-cachefor other files andno-storeforindex.html. - Security headers on every response: a restrictive Content-Security-Policy (
frame-ancestors 'none',base-uri 'none',object-src 'none',script-src 'self',form-action 'self',frame-src 'none'), frame denial,nosniff, a same-origin referrer policy andnoindex, nofollow. - Explicit content types for scripts, styles, fonts, SVG and JSON, with a MIME lookup fallback.
- A build id (
boardwalk.BuildID), computed once when the handler is built: 12 hex characters of a sha256 over the raw dist tree plus the binary's build information fromruntime/debug.ReadBuildInfo(the Go version, the main module path, version and sum,vcs.revisionandvcs.modified, and every dependency's path, version and sum, replacements included). - The build id in the
X-Summer-Buildheader (boardwalk.BuildHeader) on every response the handler writes, theapi/hand-off included. {prefix}/build.json(boardwalk.BuildPath) answers{"build":"<id>"}asno-storeJSON.- The
summer-admin-buildmeta inindex.htmlgets the build id in place ofboardwalk.BuildToken; an index without the token is served unchanged and the SPA's version check stays off.
Usage
notFoundAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"not found"}`))
})
spa, err := boardwalk.Handler("/backend", notFoundAPI)
if err != nil {
return err
}
mux.Handle("/backend/", spa)
The build id changes when any file of the admin build changes and on any redeploy of a different binary: a new commit (vcs.revision), a change of vcs.modified, a dependency version bump (a plugin release included) or a toolchain change. Without build information the id is the dist hash alone. Plugin admin asset files are not hashed directly; they reach the id through their module versions and the application's revision. A build with -buildvcs=false that takes plugins only from local replace directives does not see edits inside the replaced modules. An identical rebuild may still produce a new id once, which only shows the reload banner.
API reference
| Identifier | Description |
|---|---|
boardwalk.Handler |
Builds the SPA handler for a prefix; the second argument answers unmatched api/ paths. |
boardwalk.Dist |
Returns the embedded build as an fs.FS rooted at dist/. |
boardwalk.RewriteIndex |
Rewrites raw index.html bytes for a prefix; errors when the placeholder token is missing. |
boardwalk.BaseToken |
The placeholder in dist/index.html that is replaced by the prefix. |
boardwalk.BuildID |
Computes the build id from a dist tree and the binary's build information (nil for the dist alone). |
boardwalk.BuildHeader |
The response header, X-Summer-Build, that carries the build id on every response. |
boardwalk.BuildToken |
The placeholder in the summer-admin-build meta of dist/index.html that is replaced by the build id. |
boardwalk.BuildPath |
The path under the prefix, build.json, that answers the current build id. |
boardwalk.ContentType |
The Content-Type served for a file name, with explicit UTF-8 JavaScript and CSS types. Shared with the plugin asset route in cabana. |
boardwalk.SetSecurityHeaders |
Sets the admin security headers (nosniff, referrer policy, no framing, the CSP with script-src 'self', form-action 'self' and frame-src 'none', noindex) on a response. |
Dependencies
- SummerCMS modules: none.
- Third-party: none.
- Standard library:
bytes,crypto/sha256,embed,encoding/hex,errors,fmt,html,io,io/fs,mime,net/http,path,runtime/debug,slices,strconv,strings,time.
The embedded dist/ tree is generated from the admin/ Vite project, whose build writes to modules/boardwalk/dist.
Testing
go test ./modules/boardwalk/...
The tests run the handler through net/http/httptest against the embedded build and in-memory file systems; they need no external services.