go-wasm-web — example app for the gowasm engine
A runnable example of kjol's gowasm engine: author UI components in pure
Go, compiled to WebAssembly, with SSR + hydration, Next.js-style
server components, layouts, the kjol/webui component kit, Tailwind CSS
(compiled by kjol's own engine), and a flash-free, state-preserving hot reload.
No custom markup, no JSX — just Go. The engine lives in top-level kjol packages
(kjol/go/{vdom,wasmruntime,rsc,wasmdevserver,webui}); this directory is only the
app that consumes them.
Run it
cd cmd/examples/go-wasm-web
go run ./server # codegen + SSR + hot reload at http://localhost:8085
Open http://localhost:8085. / and /about use the light public layout;
/chart, /server, /data (client-side fetching), and /kit (a UI-kit
"kitchen-sink" demo of the webui components) use the dark app layout. Edit
any .go file and the browser hot-swaps the new wasm without a full reload or
a flash, preserving page state; a build failure shows the Go compiler output
as an overlay.
Data fetching (/data) shows both directions of kjol/httputil: the server
answers /api/quotes with httputil.RespondGob([]Quote) and the client decodes
it straight back into []Quote with httputil.FetchGob (the same Go type on
both ends — no JSON); and a user-entered GitHub repo (owner/name) is
fetched with httputil.FetchJSON into a tagged Go struct. The client HTTP
transport is wasmruntime.FetchBytes, installed by the runtime itself (override
it with httputil.SetClientTransport for auth headers or a base URL). /data is
a static route, and fetching only exists on the client, so the fetches no-op
during SSR: the server pre-renders the page's spinner, and the client runs
them for real after hydration.
Styling is Tailwind: the dev server (and build.sh) run kjol/cmd/twcss,
which scans the Go markup + the webui kit for utility classes and compiles
css/app.css → wwwroot/app.css with kjol's native Tailwind v4 engine. There is
no Bootstrap and no hand-written CSS. (build.sh does a one-off build.)
This is a separate module
go.mod here declares its own module (gowasmweb) with replace kjol => ../../..,
so the app's go-chart dependency (and freetype / x/image) stays out of kjol —
the engine packages (vdom, wasmruntime, rsc, wasmdevserver) are
stdlib-only. go build ./... at the kjol root does not descend into this
nested module; build it from this directory.
Layout
app/ the application — neutral, standalone functions (no central struct)
pages.go Deps + Shell + App/Public layouts + nav + Counter + pages
chart.go Chart page (go-chart, renders on both sides)
kit.go /kit — UI-kit demo page showcasing kjol/webui components
data.go /data — client fetch: gob from /api/quotes + third-party JSON
server_counter.go //gowasm:server component (server-only; clicks-over-time chart)
*.gen.go GENERATED by kjol/cmd/wasmgen (routes, layout dispatch, stubs)
css/app.css Tailwind entry (@import "tailwindcss" + @theme tokens)
wasm/ the js/wasm client entry point (main_native.go is a host stub)
server/ the dev-server main: injects Build/Render/Document into wasmdevserver
wwwroot/ wasmboot.js (+ generated app.css, wasm_exec.js, app.wasm)
How it maps onto the engine (top-level kjol packages)
| Engine package | Role | This app's use |
|---|---|---|
kjol/vdom |
neutral virtual DOM (native + wasm): VNode, builders, Signal, RenderHTML |
pages build *VNode; server SSRs with vdom.RenderHTML |
kjol/wasmruntime |
wasm client runtime: reconcile, Run/Hydrate, router, fetch, HMR state |
wasm/main.go calls Hydrate/Run |
kjol/rsc |
stateless server components over HTTP (gob) | //gowasm:server + the generated client stub |
kjol/wasmdevserver |
reusable dev server: SSR, /rsc, hot reload, error overlay |
server/main.go fills a wasmdevserver.Config |
kjol/httputil |
gob/JSON responders + typed client fetch (RespondGob, FetchGob, FetchJSON) |
the /data page + the /api/quotes handler |
kjol/cmd/wasmgen |
directive codegen → app/*.gen.go |
run by buildWasm and //go:generate |
The golden rule holds: wasmdevserver imports no app code. The app injects
Build (how to compile the wasm), Render (SSR a route → HTML), and Document
(wrap it in a page) via wasmdevserver.Config — the same coupling inversion kjol
uses elsewhere.
Directives (expanded by wasmgen at build time)
//gowasm:page / static layout=public // a route; `static` SSRs it, `layout=` wraps it
func HomePage(d Deps) func() *VNode { ... }
//gowasm:layout public // chrome for pages that opt into layout=public
func PublicLayout(d Deps, content *VNode) *VNode { ... }
//gowasm:server // runs on the server; calling it looks identical
func ServerCounter() func() *VNode { count := NewSignal(0); ... }