Files
via/docs/getting-started.md

180 lines
5.9 KiB
Markdown

# 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:
```go
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:
1. `via.New()` creates the app, starts an embedded NATS server, and registers internal routes (`/_sse`, `/_action/{id}`, `/_navigate`, `/_session/close`).
2. `v.Config()` applies settings.
3. `v.Page()` registers a route. The init function receives a `*Context` where you define signals, actions, and the view.
4. `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
```go
v.Config(via.Options{
DevMode: true,
ServerAddress: ":8080",
LogLevel: via.LogLevelDebug,
DocumentTitle: "My App",
Plugins: []via.Plugin{MyPlugin},
SessionManager: sm,
PubSub: customBackend,
ContextTTL: 60 * time.Second,
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](pubsub-and-sessions.md) |
| `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](pubsub-and-sessions.md) |
| `ContextTTL` | `30s` | Max time a context survives without an SSE connection before cleanup. Negative value disables the reaper |
| `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:
```go
v.Static("/assets/", "./static")
```
Or from an embedded filesystem:
```go
//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>`:
```go
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.
```go
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:
```go
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](https://github.com/air-verse/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:
```go
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](state-and-interactivity.md) — Signals, actions, components, validation
- [Routing and Navigation](routing-and-navigation.md) — Multi-page apps, middleware, SPA navigation
- [PubSub and Sessions](pubsub-and-sessions.md) — Real-time messaging, persistent sessions
- [HTML DSL](html-dsl.md) — The `h` package reference
- [Project Structure](project-structure.md) — Organizing files as your app grows