Background windows stopped updating because the reaper suspended contexts after ContextSuspendAfter and fully reaped them after ContextTTL. Suspended contexts had to re-run the page init function from scratch on reconnect, losing the live-updating experience. Contexts now live until the browser tab closes (beforeunload beacon) or the server shuts down. The context map grows indefinitely — no background reaper. Removes: startReaper, reapOrphanedContexts, suspend/resume logic, ContextSuspendAfter/ContextTTL config fields, lastSeenAt/suspended context fields, and all associated tests.
5.7 KiB
Getting Started
Via is a server-side reactive web framework for Go. The browser connects over SSE (Server-Sent Events), and all state lives on the server — signals, actions, and view rendering happen in Go. The browser is a thin display layer that Datastar keeps in sync via DOM morphing.
Core Loop
Every Via app follows the same pattern:
package main
import (
"github.com/ryanhamamura/via"
"github.com/ryanhamamura/via/h"
)
func main() {
v := via.New()
v.Config(via.Options{
DocumentTitle: "My App",
})
v.Page("/", func(c *via.Context) {
count := 0
step := c.Signal(1)
increment := c.Action(func() {
count += step.Int()
c.Sync()
})
c.View(func() h.H {
return h.Div(
h.P(h.Textf("Count: %d", count)),
h.Label(
h.Text("Step: "),
h.Input(h.Type("number"), step.Bind()),
),
h.Button(h.Text("+"), increment.OnClick()),
)
})
})
v.Start()
}
What happens:
via.New()creates the app, starts an embedded NATS server, and registers internal routes (/_sse,/_action/{id},/_navigate,/_session/close).v.Config()applies settings.v.Page()registers a route. The init function receives a*Contextwhere you define signals, actions, and the view.v.Start()starts the HTTP server and blocks until SIGINT/SIGTERM.
When a browser hits the page, Via creates a new Context, runs the init function, renders the full HTML document, and opens an SSE connection. From that point, every c.Sync() re-renders the view and pushes a DOM patch to the browser.
Configuration
v.Config(via.Options{
DevMode: true,
ServerAddress: ":8080",
LogLevel: via.LogLevelDebug,
DocumentTitle: "My App",
Plugins: []via.Plugin{MyPlugin},
SessionManager: sm,
PubSub: customBackend,
ActionRateLimit: via.RateLimitConfig{Rate: 20, Burst: 40},
})
| Field | Default | Description |
|---|---|---|
DevMode |
false |
Enables context persistence across restarts, console logger, and Datastar inspector widget |
ServerAddress |
":3000" |
HTTP listen address |
LogLevel |
InfoLevel |
Minimum log level. Use via.LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError |
Logger |
(auto) | Replace the default logger entirely. When set, LogLevel and DevMode have no effect on logging |
DocumentTitle |
"⚡ Via" |
The <title> of the HTML document |
Plugins |
nil |
Slice of plugin functions executed during Config() |
SessionManager |
in-memory | Cookie-based session manager. See PubSub and Sessions |
DatastarContent |
(embedded) | Custom Datastar JS bytes |
DatastarPath |
"/_datastar.js" |
URL path for the Datastar script |
PubSub |
embedded NATS | Custom PubSub backend. Replaces the default NATS. See PubSub and Sessions |
ActionRateLimit |
10 req/s, burst 20 |
Default token-bucket rate limiter for action endpoints. Rate of -1 disables limiting |
Static Files
Serve files from a directory:
v.Static("/assets/", "./static")
Or from an embedded filesystem:
//go:embed static
var staticFS embed.FS
v.StaticFS("/assets/", staticFS)
Both disable directory listing and return 404 for directory paths.
Head and Foot Injection
Add elements to every page's <head> or end of <body>:
v.AppendToHead(
h.Link(h.Rel("stylesheet"), h.Href("/assets/style.css")),
h.Meta(h.Attr("name", "viewport"), h.Attr("content", "width=device-width, initial-scale=1")),
)
v.AppendToFoot(
h.Script(h.Src("/assets/app.js")),
)
These are additive and affect all pages globally.
Plugins
A plugin is a func(v *via.V) that mutates the app during configuration — registering routes, injecting assets, or applying middleware.
func PicoCSSPlugin(v *via.V) {
v.HTTPServeMux().HandleFunc("GET /css/pico.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Write(picoCSSBytes)
})
v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/css/pico.css")))
}
// Usage:
v.Config(via.Options{
Plugins: []via.Plugin{PicoCSSPlugin},
})
Plugins have full access to the *V public API: HTTPServeMux(), AppendToHead(), AppendToFoot(), Config(), etc.
DevMode
Enable during development for a better feedback loop:
v.Config(via.Options{DevMode: true})
What it does:
- Console logger — Human-readable log output with timestamps.
- Context persistence — Saves context-to-route mappings to
.via/devmode/ctx.json. On server restart, reconnecting browsers restore their state instead of getting a blank page. Pair with Air for hot-reloading. - Datastar inspector — Injects a widget showing live signal values and SSE activity.
Custom HTTP Handlers
Access the underlying *http.ServeMux for custom routes:
mux := v.HTTPServeMux()
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
Register custom handlers before calling v.Start().
Next Steps
- State and Interactivity — Signals, actions, components, validation
- Routing and Navigation — Multi-page apps, middleware, SPA navigation
- PubSub and Sessions — Real-time messaging, persistent sessions
- HTML DSL — The
hpackage reference - Project Structure — Organizing files as your app grows