Files
summercms/modules/boardwalk/README.md
Jakub Zych 291b18fb1e feat(boardwalk): expose the admin build id and show a reload banner when a new build is deployed (quick-261028-rqb)
- boardwalk.BuildID hashes the dist tree plus the binary's build info once
  per handler; X-Summer-Build on every response, {prefix}/build.json and the
  summer-admin-build meta carry it
- the SPA reads the booted build, re-checks on focus, visibility and every
  5 minutes, and shows UpdateBanner on a different build
2026-10-28 21:19:36 +01:00

6.3 KiB

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.html for that prefix (boardwalk.RewriteIndex).
  • Fails at boot when index.html lacks the boardwalk.BaseToken placeholder, which catches a stale or hand-edited build.
  • Falls back to index.html for client-side routes and directories; missing files with an extension get a plain 404.
  • Hands every request whose path under the prefix is api or starts with api/ 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-cache for other files and no-store for index.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 and noindex, 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 from runtime/debug.ReadBuildInfo (the Go version, the main module path, version and sum, vcs.revision and vcs.modified, and every dependency's path, version and sum, replacements included).
  • The build id in the X-Summer-Build header (boardwalk.BuildHeader) on every response the handler writes, the api/ hand-off included.
  • {prefix}/build.json (boardwalk.BuildPath) answers {"build":"<id>"} as no-store JSON.
  • The summer-admin-build meta in index.html gets the build id in place of boardwalk.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.