# 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 ```sh 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 build runs `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 (`kjol/tw`). There is **no Bootstrap and no hand-written CSS**. Saving a `.css` file recompiles **only** Tailwind and swaps the stylesheet into the live page — no wasm rebuild, no reload, no lost state. Saving a `.go` file does the full rebuild and hot-swaps the wasm. ## Building The build is Go, not a shell script — `buildsteps/` holds the four steps (codegen → Tailwind → wasm → `wasm_exec.js` shim), and both the one-off build and the dev server's watch loop call the *same* functions, so they cannot drift apart. ``` go run ./build # one-off: codegen + Tailwind + wasm + shim go run ./server # dev server: does the same build, then watches and hot-reloads ``` In VS Code these are the `gowasm: build` and `gowasm: dev server (hot reload)` tasks; both run through `gowasm: prebuild` (codegen + Tailwind), which is also the `preLaunchTask` of the debug configs — under the debugger the binary is built by Delve, so nothing else would generate `app/*.gen.go`. ## 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) ```go //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); ... } ```