A widget’s Go code lives in the source tree; its CSS, JavaScript, fonts, images, and any other static assets need to ship to the browser somehow. BESPA does this through a single global registry that every page emits links to in its <head>.
Each widget package embeds its assets at compile time with //go:embed, hands the resulting embed.FS to RegisterFS, and the framework picks up everything it knows how to serve:
package mywidget
import (
"embed"
"github.com/microbus-io/bespa/widget"
)
// Globbing by extension keeps .go source files (and READMEs, .txt, etc.)
// out of the binary. List every extension the package actually ships.
//go:embed *.css *.js *.woff2
var bundle embed.FS
func init() {
widget.AssetRegistry.RegisterFS(bundle)
}
That’s the whole thing. RegisterFS walks the FS and routes each file by extension — .css joins /bespa/style.css, .js joins /bespa/script.js (or is isolated if it carries a sourcemap reference), .woff2 / .png / .jpg / .svg are served as static files at /bespa/<name>. Unknown extensions are ignored.
List every extension the package actually ships in the directive. //go:embed * would also work but would bundle the package’s .go source files into the binary — wasted bytes, since RegisterFS ignores them. Each pattern must match at least one file or the build fails, so drop extensions that the package doesn’t yet have.
When you need a non-default key, asset content built at init time, or just one file in the package, register directly:
// For a single asset with a non-default key, register it explicitly:
//go:embed mywidget.css
var mywidgetCSS string
func init() {
widget.AssetRegistry.RegisterStyle("mywidget", mywidgetCSS)
}
RegisterStyle / RegisterScript take a key — the filename stem in the RegisterFS path — and the asset content. Use them when the auto-derived key isn’t what you want.
For anything sizeable enough that you don’t want to inflate the main bundle (Quill, ECharts, Chroma — anything in the hundreds of kilobytes or more), register it as an isolated script:
//go:embed chart-big-lib.js
var chartLib string
func init() {
// Served at /bespa/chart-big-lib.js with its own <script> tag and
// its own cache key — does NOT join the aggregated /bespa/script.js bundle.
widget.AssetRegistry.RegisterIsolatedScript("chart-big-lib", chartLib)
}
It gets its own <script> tag, its own cache entry, and its own cache-buster query string. Pages that don’t use the widget don’t pay for it (apart from the empty <script> tag, which the browser short-circuits on second load).
PNG, WOFF2, JSON, anything else — use RegisterFile:
//go:embed flags.png
var flags []byte
func init() {
widget.AssetRegistry.RegisterFile("flags.png", flags)
}
// Served at /bespa/flags.png with content type sniffed from the bytes.
The Content-Type is detected from the leading bytes via http.DetectContentType. A 30-day cache header is set automatically — the cache-buster query string on the URL handles invalidation across deploys.
Sometimes the asset is too big to embed wholesale and too varied to pre-compute. Register an http.Handler that owns a sub-path:
widget.AssetRegistry.RegisterHandler("maps/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// r.URL.Path is "/bespa/maps/<spec>.json"; serve whatever the spec asks for.
spec := strings.TrimPrefix(r.URL.Path, "/bespa/maps/")
spec = strings.TrimSuffix(spec, ".json")
// ... compute the JSON for spec, write it ...
}))
Any request under /bespa/maps/ routes to this handler. chart/maps does exactly this to serve 200+ country / subdivision GeoJSON slices out of two embedded master files.
Everything the registry serves lives under /bespa/. Grouped assets use sub-paths: /bespa/maps/usa.json, /bespa/icons/heart.svg. When you mount the registry on your mux, use the trailing-slash pattern so all those paths route to it:
mux.HandleFunc("/bespa/", widget.AssetRegistry.ServeHTTP)Packaging as a library — the full file layout for a publishable widget package.
Read widget/assets.go for the registry implementation and chart/maps/maps.go for the dynamic-handler pattern.