Plugins
app := via.New(via.WithPlugins(
picocss.Plugin(picocss.WithThemes(picocss.AllPicoThemes)),
echarts.Plugin(),
))
Plugins implement Register(*via.App) and call any of AppendToHead,
AppendToFoot, AppendAttrToHTML, HandleFunc, or RegisterAppSignal
during boot to inject document fragments, asset routes, and client-driven
signals.
Call these only from Register — the document-mutation slices are not
lock-guarded against concurrent appends after the server starts.
Plugin packages expose Plugin(...) as the canonical constructor (never
New(...)) so via.WithPlugins(...) call sites stay uniform.
Asset delivery
The bundled plugins embed their pinned client builds with go:embed and
serve them from a content-hashed, immutably-cached same-origin path. Plugin
registration does zero network I/O — there is no boot-time CDN fetch, no
third-party origin in the page, and an air-gapped or offline deploy works out
of the box. The embedded version is fixed; WithVersion(v) only guards the
pin (restating the embedded version is a no-op; any other version panics,
because there is no embedded asset or SRI hash to back it).
Two opt-outs, on the echarts and maplibre plugins (Pico ships only the
embedded build):
WithSource(url)— serve the script from a same-origin path you host yourself (a custom build or internal mirror). Cross-origin URLs are rejected; useWithCDNfor those.WithCDN(url, integrity)— load from a CDN. TheintegritySRI hash (sha256-/sha384-/sha512-+ base64 digest of that exact body) is mandatory — the emitted<script>carries it pluscrossorigin="anonymous", so a tampered response is refused by the browser. There is no way to opt out of SRI; a version bump means supplying the new build’s hash. MapLibre also hasWithCDNStylesheet(url, integrity).
echarts.Plugin(echarts.WithCDN(
"https://cdn.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.min.js",
"sha384-…",
))
Bundled plugins
picocss
picocss.Plugin() wires the Pico CSS framework:
theme + dark-mode switching driven by client signals (no full reload),
served from a plugin asset route with ETag revalidation and gzip
negotiation. Options include WithThemes(...), WithDefaultTheme(...),
WithClassless(), WithColorClasses(), and WithDarkMode() /
WithLightMode(). picocss.ThemeRef() / DarkModeRef() return the Datastar
signal references for inline expressions.
h.Button(h.Text("Blue"),
h.DataOnClick("%s = %q", picocss.ThemeRef(), picocss.PicoThemeBlue))
See internal/examples/picocss for client-side theme switching.
echarts
echarts.Plugin() integrates Apache ECharts.
Hold a *echarts.Chart on the page, build it in OnInit, mount it in
View, and update it from actions or a via.Stream ticker:
type Page struct {
Chart *echarts.Chart
}
func (p *Page) OnInit(ctx *via.Ctx) error {
if p.Chart == nil {
p.Chart = echarts.NewChart(
echarts.WithElementID("cpu"),
echarts.WithTitle("CPU"),
echarts.WithDimensions("100%", "300px"),
)
}
return nil
}
func (p *Page) Refresh(ctx *via.Ctx) error {
return p.Chart.SetSeries(ctx, echarts.Line("CPU", [][]any{ {0, 12}, {1, 18} }))
}
See internal/examples/sysmon for a live system monitor streaming into
ECharts.
maplibre
maplibre.Plugin() integrates MapLibre GL JS —
interactive vector maps whose camera, markers, and data layers are all driven
from Go and pushed over SSE. Hold a *maplibre.Map, build it in OnInit,
mount it in View, then move it from actions or a via.Stream ticker:
type Page struct {
Map *maplibre.Map
}
func (p *Page) OnInit(ctx *via.Ctx) error {
if p.Map == nil {
p.Map = maplibre.NewMap(
maplibre.WithCenter(maplibre.At(-122.42, 37.77)), // At(lng, lat)
maplibre.WithZoom(11),
maplibre.WithNavigationControl(),
)
}
return nil
}
func (p *Page) View(ctx *via.CtxR) h.H { return p.Map.Mount() }
func (p *Page) GoToTokyo(ctx *via.Ctx) { p.Map.FlyTo(ctx, maplibre.At(139.69, 35.69), 10) }
Coordinates are [lng, lat] — longitude first — the inverse of the lat/lng most
map UIs print, and the single most common MapLibre mistake. The camera, marker,
and center APIs take a typed LngLat whose named fields defuse the swap: build
it with maplibre.LngLat{Lng: …, Lat: …} (order-independent) or the
maplibre.At(lng, lat) shorthand; box APIs take a Bounds{West, South, East,
North}. GeoJSON geometry (Point, LineString, Polygon) stays as raw
[lng, lat] arrays.
The runtime surface, all delivered over SSE:
- Camera —
FlyTo(curved flight),EaseTo,JumpTo,SetCenter(all take aLngLat),SetZoom,SetPitch,SetBearing, andFitBounds(ctx, Bounds). - Markers —
AddMarker(ctx, id, At(lng, lat), opts…)places a keyed pin;WithMarkerdeclares a static one at construction;MoveMarkerrepositions it live (vehicle tracking),RemoveMarker/ClearMarkerstear down. Options:Color,Draggable,Scale,PopupText(XSS-safe, for user content),PopupHTML(anh.Hbody —h.Tescapes,h.Rawis trusted markup only). - Data — declare sources/layers with
WithGeoJSONSource+WithLayer, or add them at runtime withAddGeoJSONSource/AddLayer; push live data withSetGeoJSON(ctx, sourceID, fc). Build GeoJSON withPoint,LineString,Polygon,Feature,FeatureCollection; build layers withCircleLayer/LineLayer/FillLayer/SymbolLayer. - Events — drive Go from user gestures:
OnClick,OnMoveEnd,OnMarkerClick,OnMarkerDragEnd,OnFeatureClick, and a genericOnMapEventescape hatch (right-click, double-click, …). Each takes a bound method that reads a typedMapEvent— the clickedLngLat, theMarkerID/FeatureID, and the live camera — viap.Map.Event(ctx).WithFeatureHoverhighlights the hovered feature client-side, with no round-trip. - Styling — compose data-driven paint/layout values with typed expression
builders instead of raw
[]any:Get,FeatureState,Zoom,Boolean,Case,Interpolate,Step, plusWhenHovered/WhenStatesugar (e.g.Paint("fill-color", WhenHovered("#ffcc00", "#5856d6"))). - Dialogs —
ShowPopup(ctx, id, at, text)/ShowPopupHTML(anh.Hbody) /ClosePopupshow keyed, server-driven popups; the “open a popup at the clicked feature” pattern pairs naturally withOnFeatureClick. - Lifecycle —
SetStyle,Resize,Dispose, and theCall(ctx, method, args…)escape hatch for any Map method the typed API misses.
The default style is MapLibre’s no-key demo style, meant for demos and CI,
not a production SLA. Supply your own with WithStyle(url) (e.g. a MapTiler
or Stadia style) for real use. Pin a v5 release — v6 is ESM-only and drops the
maplibregl global the <script> include relies on.
Self-host or harden with WithSource / WithStylesheet (offline, air-gapped),
or WithCSPBuild() to use the inline-worker bundle under a strict worker-src
policy.
See internal/examples/maps for a server-driven world map: city buttons fly
the camera and a drone marker glides along a route, live, over SSE.