6.7 KiB
kjol-web — the kjol website
The landing page and documentation for the whole codebase. It is also the thing it documents: every page runs the code it describes, and there is not a screenshot of a component anywhere on the site.
It is one server running two entirely different front-ends, and that is the point of it. kjol has two web layers — one written in Go and compiled to WebAssembly, one written in Solid and bundled by a Go toolchain — and the only honest way to document both is to build the site out of both.
Run it
cd go/cmd/kjol-web
go run ./build # cold build: both halves
go run ./server # SSR + /rsc + hot reload at http://localhost:8085
go run ./server performs that same build on every save and hot-swaps the result into
the browser, so day to day it is the only command you need. A .go save rebuilds the
wasm and swaps it in without a reload or a flash, preserving page state; a .css save
recompiles only Tailwind; a compile error lands in a browser overlay rather than in a
terminal you were not looking at.
The shape of the site
| Path | Rendered by | What it is |
|---|---|---|
/, /about |
Go → WebAssembly, SSR'd | The landing page. The Layers menu is the site's primary navigation. |
/wasm/* |
Go → WebAssembly | Kjol Wasm Web — the gowasm engine: SSR + hydration, server components, the webui kit, overlays, AutoTable, charts, client fetching. |
/js/* |
Solid → esbuild, client-rendered | Kjol JS Web — the Solid kit: components, forms, AutoTable, theming. |
/js/ssr |
Solid → goja, at request time | A public page server-rendered with live data injected — the ISR path. |
Crossing between /wasm and /js is a real page load. They are different binaries, and
pretending otherwise would mean shipping both to every visitor.
Layout
app/ the Go/WASM half — pages as plain Go functions returning a *VNode
pages.go Deps + Shell + the public/app layouts + the landing page
layers.go the Layers, as data. MIRRORED in frontend/src/layers.ts.
docs.go the docs chrome; docsNav() is the sidebar AND the index
kit.go table.go overlays.go chart.go data.go server_counter.go
*.gen.go GENERATED by kjol/cmd/wasmgen (routes, layout dispatch, RSC stubs)
wasm/ the js/wasm client entry (main_native.go is a host stub)
css/app.css its Tailwind entry — kjol/tw scans the .go files for class names
frontend/ the Solid half — .tsx pages written against @ui/*
css/style.css brand ONLY. kjol's theme.css is prepended by the bundler, and it
is the one that does `@import "tailwindcss"`.
vendor/ pdf-lib + pdfjs-dist. @ui/AutoTable imports them at the TOP LEVEL,
so a bundle without them does not degrade — it fails to evaluate.
src/app.ts the SPA entry. It is .ts, not .tsx, because the bundler resolves
the entry as src/app.ts and nothing else — so it can hold no JSX.
src/layers.ts the Layers again. Keep in step with app/layers.go.
server/ ONE Go server: serves wwwroot, SSRs the wasm routes, hosts /rsc,
mounts the Solid SPA at /js/*, serves the SSR'd public pages.
internal/handlers/ the app side of the public-page inversion: kjol generates the
registry; this owns the type and the document shell.
buildsteps/ the build, in Go rather than a shell script, so the one-shot build and
the dev server's watch loop call the SAME functions and cannot drift.
wwwroot/ both halves write here. They never collide: app.css / app.wasm for one,
bundle.min.* for the other. One static dir, one server.
Theming
One theme, both halves. The kits are themed by semantic tokens — components say
bg-surface, text-ink, border-line and never name a colour — so dark mode
re-points about a dozen CSS variables and not one component knows it happened.
The choice is stored under a single kjol-theme key that both front-ends read, so
switching to dark in /wasm and walking over to /js keeps it dark. A ten-line boot
script in the document head applies the class before first paint; without it every
dark-mode reader would get a white page until the bundle landed, and then have it
snatched away.
The only places a dark: variant survives are the two a re-pointed token cannot fix: a
coloured tint (a red-50 wash is invisible on a near-black surface) and a fill that has
to invert (the neutral button, whose label must go dark when the fill goes pale).
Adding things
A Go/WASM page: write the function, mark it //gowasm:page /wasm/thing layout=app,
build. wasmgen regenerates the routing. Add static to have it server-rendered.
A Solid page: write the .tsx, add it to routes in frontend/src/app.ts and to
NAV in frontend/src/layout/Shell.tsx.
A layer: one entry in app/layers.go and one in frontend/src/layers.ts. They are
two files because nothing is upstream of both a WebAssembly binary and an esbuild bundle;
keeping each to a flat list of plain data is what makes that duplication survivable.
How it maps onto the engine
| Package | Role | This app's use |
|---|---|---|
kjol/vdom |
neutral virtual DOM (native + wasm): VNode, Signal, RenderHTML |
pages build *VNode; the server SSRs with vdom.RenderHTML |
kjol/wasmruntime |
wasm client runtime: reconcile, Run/Hydrate, router, fetch |
wasm/main.go calls Hydrate/Run |
kjol/rsc |
stateless server components over HTTP | //gowasm:server + its generated client stub |
kjol/wasmdevserver |
reusable dev server: SSR, /rsc, hot reload, error overlay |
server/main.go fills a wasmdevserver.Config |
kjol/webui |
the Go component kit | every /wasm/* page |
kjol/jsbundler |
TSX → Solid → esbuild, the Tailwind driver, the SSR bake | buildsteps.JS, and the ISR render at request time |
kjol/jsruntime |
the Solid kit, the vendored runtime, the icons, theme.css |
everything under /js/* |
kjol/tw |
the Tailwind v4 engine, in Go | both stylesheets — it scans .go for one and .tsx for the other |
The golden rule holds throughout: no kjol package imports application code. The app
injects Build, Render and Document into wasmdevserver; it owns the publicPage
type that kjol's generated registry is written against. The dependency only ever points
one way.
Its own module
go.mod declares module kjolweb with replace kjol => ../.., so its dependencies —
go-chart for the server-drawn charts, plus esbuild and goja by way of the bundler — stay
out of kjol, whose engine packages are stdlib-only. go build ./... at the kjol root
does not descend into this nested module; build it from here.