{}

Handlers & routing

A BESPA page is an http.Handler. The framework has no router of its own — you register handlers against whatever mux you already use, and Page().Draw(w, r) writes HTML to the response. This page is the wiring contract.

The minimum

package main

import (
    "net/http"

    "github.com/microbus-io/bespa"
    "github.com/microbus-io/bespa/widget"
)

var wf = bespa.DefaultFactory{}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/bespa/", widget.AssetRegistry.ServeHTTP)
    mux.HandleFunc("/", handleHome)
    http.ListenAndServe(":8080", mux)
}

func handleHome(w http.ResponseWriter, r *http.Request) {
    wf.Page().Add("Hello, world!").Draw(w, r)
}

Two things are required:

With another router

Anything that speaks http.Handler works:

// chi
r := chi.NewRouter()
r.Handle("/bespa/*", widget.AssetRegistry)
r.Get("/orders/{id}", handleOrder)

// gorilla/mux
m := mux.NewRouter()
m.PathPrefix("/bespa/").Handler(widget.AssetRegistry)
m.HandleFunc("/orders/{id}", handleOrder)

// stdlib (Go 1.22+) — method + pattern + path variables
mux := http.NewServeMux()
mux.HandleFunc("/bespa/", widget.AssetRegistry.ServeHTTP)
mux.HandleFunc("GET /orders/{id}", handleOrder)

The path-parameter accessor changes per router; the BESPA-side code does not.

Middleware

Standard net/http middleware composes the way it always does. Wrap individual handlers, or wrap the whole mux. BESPA is unaware:

// Wrap a handler:
mux.Handle("/admin/", requireRole("admin", adminHandler))

// Wrap the whole mux:
http.ListenAndServe(":8080", logging(compress(mux)))

Compression middleware (brotli, gzip, deflate) is the most common outer wrapper. EmbedHandler notices the Content-Encoding header on the response from your handler and decompresses transparently before parsing — so a mux wrapped in compression can still be passed to EmbedHandler directly. You don’t have to hand it the inner unwrapped mux to avoid double-encoding.

EmbedHandler — the integration point

EmbedHandler is how one page renders another inside itself. It takes a handler function (typically mux.ServeHTTP), the current request, an HTTP method, a path, and a body. It invokes the handler against an in-memory recorder, extracts the <body> content, and returns it as a widget you can drop anywhere in your page tree:

// mux is your *http.ServeMux (or whatever router you wired above).
wf.Modal("modal").Add(
    wf.EmbedHandler(mux.ServeHTTP, r, "GET",
        wf.StateOf(r).Get("modal")+"?_back=^?modal=", nil),
)

Three things to know:

`mux` in code samples

Throughout these docs, mux is your *http.ServeMux (or whatever router you used) — the one that knows how to resolve a path back to a handler. You’re free to name it anything; the pages use mux because that’s what EmbedHandler typically receives.

CSP

BESPA emits a small amount of inline <script> (the client bootstrap) and inline <style> (the resolved theme tokens). A strict Content-Security-Policy needs 'unsafe-inline' for both — or a nonce/hash plumbing layer, which the framework does not currently provide:

Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'unsafe-inline';
  style-src   'self' 'unsafe-inline';
  font-src    'self' data:;
  img-src     'self' data:;

If your deployment requires nonce-based CSP, the inline emissions live in widget/page.go — wrap or fork.

CORS

BESPA renders HTML, not an API. CORS only matters if you embed a BESPA page in a cross-origin iframe or accept cross-origin POSTs. Add the headers at the middleware layer; nothing in BESPA opts in or out.

See also

Basics → Nesting pages — what EmbedHandler actually does under the hood.

Modals & side panels — the most common reason to embed.

website/main.go in the bespa repo — the canonical wiring this site uses: a logging + brotli wrapper around *http.ServeMux, AssetRegistry at /bespa/, and sub-packages each calling Init(mux.ServeMux) to register on the inner mux.