// Merge class lists through the configured merger (plain join by
// default; conflict-aware once you call WithClassMerger).
```
| Function | Description |
|----------|-------------|
| `attrs` | Renders a `gastro.Attrs` bag as HTML attributes. Optional second `dict` of base defaults; `class` is merged, other keys are overridable defaults. Escapes values (safe-typed values pass through), `bool` → bare/omitted, names validated. |
| `twJoin` | Concatenates class lists with single spaces, dropping empties. Never resolves conflicts. |
| `twMerge` | Same as `twJoin` until you call `gastro.WithClassMerger` — then it delegates to your merger (e.g. tailwind-merge-go) for conflict-aware merging. |
By default `twMerge` and `attrs`'s class handling only concatenate. Plug a
Tailwind-aware merger to get conflict resolution; the dependency lives in
your module, not gastro's:
```go
import twmerge "github.com/Oudwins/tailwind-merge-go"
router := gastro.New(gastro.WithClassMerger(twmerge.Merge))
```
## Custom Helpers
Register custom template functions in your `main.go` using `gastro.WithFuncs()`:
```go
router := gastro.New(
gastro.WithFuncs(template.FuncMap{
"formatEUR": func(cents int) string {
return fmt.Sprintf("%.2f EUR", float64(cents)/100)
},
"slugify": func(s string) string {
return strings.ToLower(strings.ReplaceAll(s, " ", "-"))
},
}),
)
http.ListenAndServe(":4242", router.Handler())
```
Custom functions are available in all pages and components, just like the built-in helpers.
## Request-aware Helpers (`WithRequestFuncs`)
`WithFuncs` registers helpers at template-parse time — their bodies are
fixed for the lifetime of the router. **Request-aware helpers** are
different: their bodies close over a `*http.Request` and can read
request state. That makes the same helper name (`t`, `csrfField`,
`cspNonce`, …) return different values on different requests.
Use `gastro.WithRequestFuncs(binder)` to register them:
```go
router := gastro.New(
gastro.WithMiddleware("/", i18n.Middleware),
gastro.WithRequestFuncs(func(r *http.Request) template.FuncMap {
l := i18n.FromCtx(r.Context())
return template.FuncMap{
"t": l.T,
"tn": l.TN,
"tc": l.TC,
}
}),
)
```
In a `.gastro` template:
```gastro
---
---
{{ t "Welcome" }}
{{ tn "1 item" "%d items" .Count }}
{{ tc "button" "Open" }}
```
The binder runs once per request. The closures it returns capture `r`,
so `{{ t "Welcome" }}` resolves against the request's locale, CSRF
cookie, CSP nonce, or whatever else your middleware attached to the
request context.
### When to use it
| Pattern | Library | Helpers registered |
|---|---|---|
| Internationalisation (gettext-style) | `gotext`, `go-i18n`, hand-rolled | `t`, `tn`, `tc` |
| CSRF protection | `gorilla/csrf`, custom | `csrfToken`, `csrfField` |
| CSP nonces | custom (a few lines of `crypto/rand`) | `cspNonce` |
| Named-route reversal | custom | `routePath` |
| Asset hashing | custom | `asset` |
| Feature flags | flag library of choice | `flag` |
The common thread: anything that needs to read **per-request state**
at template time, where pre-computing in frontmatter would be
repetitive across many pages.
### Rendering from handlers and SSE
When you call `gastro.Render.X(props)` from a Go handler, the static
FuncMap is used — binders are *not* invoked, so request-aware helpers
resolve to placeholders (typically the empty string). To bind a render
call to a specific request, use `Render.With(r)`:
```go
func handleUpdate(w http.ResponseWriter, r *http.Request) {
html, _ := gastro.Render.With(r).Card(gastro.CardProps{Title: "Hello"})
datastar.NewSSE(w, r).PatchElements(html)
}
```
The returned `*renderAPI` is reusable within a single request — store
it in a local and render multiple components from it. It is **not**
goroutine-safe and must not be retained beyond the request.
### Multiple binders compose
You can register `WithRequestFuncs` multiple times — e.g. one for i18n,
one for CSRF — and the helper sets are merged. The only constraint is
that helper names must be unique across the union of:
- Gastro built-ins (`upper`, `lower`, `dict`, … — see top of this page)
- `WithFuncs` registrations
- All `WithRequestFuncs` binders
A collision panics at `gastro.New()` with both sources named:
```
gastro: helper name "t" registered twice
- WithFuncs
- WithRequestFuncs[1]
```
This is intentional — silent shadowing of a built-in or another binder
would make template behaviour depend on registration order, which is
brittle and hard to debug.
### The binder contract
A `WithRequestFuncs` binder is a `func(*http.Request) template.FuncMap`.
It MUST:
- Return a **stable key set** — the *names* returned must not depend on
request state. (Closure *bodies* may read request state freely; that's
the whole point.) Gastro probes each binder once at `New()` with a
synthetic request to discover its key set for collision detection.
- Not panic during top-level execution when fed a probe request whose
context carries no adopter-installed values. In particular, your
`FromCtx`-style accessors must tolerate a missing locale / cookie /
nonce and return safe zero defaults. The probe never invokes the
*closures* inside the FuncMap — only the map's keys are read — but
top-level statements in the binder body do run.
A binder SHOULD:
- Be cheap. It runs on every request.
- Return a **literal `template.FuncMap{...}`** so the Gastro LSP can
extract helper names via static analysis and surface them in
completion / hover / go-to-definition. Dynamically constructed maps
(`m := make(template.FuncMap); m["t"] = …; return m`) work at runtime
but degrade the editor experience for those helpers.
### Runtime panic recovery
If a binder or any helper it returned panics during request handling,
Gastro recovers the panic, logs it with the panicking binder's
registration index, and dispatches to your `WithErrorHandler` (default:
`500 Internal Server Error`). One bad binder cannot crash the server.
### Components, slots, and wrap blocks
Request-aware helpers propagate through every layer of a page render:
- Page templates (`pages/foo.gastro`).
- Components invoked from a page via `{{ Component . }}` or
`{{ wrap Component (dict ...) }}`.
- Slot content rendered inside a wrap block.
- Components rendered programmatically via
`gastro.Render.With(r).Component(props)`.
In every case, helpers like `{{ t "…" }}`, `{{ csrfField }}`, and
`{{ cspNonce }}` resolve against the right per-request state. You don't
need to translate strings in the page's frontmatter and pass them as
props — the helper just works inside the component template body.
Under the hood, each request Clones the page's parsed template (or, in
dev mode, re-parses it) and applies the per-request FuncMap to the
clone. Bare component invocations are then dispatched through closures
that thread the request all the way down the component tree. Cost is
proportional to nesting depth; on an Apple M3 a typical component
template clones in ~1.2 µs, so a 5-deep tree adds ~6 µs per request —
well below the per-request budget of any real handler. See the
`BenchmarkNestedClone` suite in `internal/compiler/` for the per-depth
roll-up.
### Editor support
The Gastro LSP discovers `WithRequestFuncs` binder helpers by AST-
parsing your project's `main.go`. As long as the binder returns a
literal `template.FuncMap{…}` (either inline or via a one-hop named
function reference in the same file), helper names show up in:
- Template completion (`{{ t
` suggests `t` with detail
*"request-aware helper"*).
- Template parse — no spurious *"function not defined"* diagnostic.
- Hover on `{{ t "…" }}` shows the binder index and a source link
pointing at the FuncMap key in `main.go`.
- Go-to-definition on a helper name jumps to that same FuncMap entry.
Binders that build their FuncMap dynamically (e.g. by ranging over a
slice, or returning a `template.FuncMap` constructed in another
package) still work at runtime, but the LSP can't statically extract
their keys — so completion / hover / go-to-def don't list them. To
make the trade-off visible, the LSP publishes an **info-level
diagnostic** on the `gastro.WithRequestFuncs(...)` call site explaining
the situation and pointing at the literal-`FuncMap` workaround.
### Worked examples
Three example apps in `examples/` exercise `WithRequestFuncs` along
different axes:
| Example | What it stresses |
|---|---|
| `examples/i18n/` | The motivating case. Three helpers (`t`, `tn`, `tc`) from one binder, locale detection middleware, gettext-style PO catalogues. |
| `examples/csrf/` | Mixed return types in one binder (`csrfToken` returns `string`, `csrfField` returns `template.HTML`). Middleware mints + verifies tokens; helpers only read. |
| `examples/csp/` | Helper-to-middleware coordination: the middleware writes a `Content-Security-Policy: nonce-X` header and the `cspNonce` helper renders the matching `