{}

Assets & CSS

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

The pattern

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.

Explicit registration

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.

Large libraries: isolated scripts

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).

Static binary assets

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.

Dynamic assets: handlers

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.

URL conventions

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)

See also

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.