// Command server runs the kjol-web site on kjol's reusable wasmdevserver: // it SSRs the app's static routes, hosts the /rsc server-component endpoint, and // hot-swaps the wasm into the browser on change. It shows the coupling // inversion — the framework (wasmdevserver) imports no app code; the app injects // Build/Render/Document here. // // Run it from THIS directory (the relative paths below are resolved against it): // // go run ./server # from go/cmd/kjol-web package main import ( "flag" "fmt" "log" "net/http" "kjol/httputil" "kjol/vdom" "kjol/wasmdevserver" "kjol/webui" "kjolweb/app" "kjolweb/buildsteps" "kjolweb/internal/handlers" ) func main() { addr := flag.String("addr", ":8085", "listen address") watch := flag.Bool("watch", true, "watch sources, rebuild wasm, hot-reload") flag.Parse() log.Fatal(wasmdevserver.Serve(wasmdevserver.Config{ Addr: *addr, Dir: "./wwwroot", Watch: *watch, WatchDirs: []string{ "app", "wasm", "css", // this app's Go/WASM half "frontend", // its Solid half — a .tsx save rebuilds the JS bundle "../../webui", "../../vdom", "../../wasmruntime", "../../rsc", // the wasm engine "../../jsruntime/uikit", "../../jsruntime/styles", // the Solid kit + the shared theme }, Build: buildsteps.All, BuildCSS: buildsteps.Tailwind, // a .css save skips codegen+wasm and hot-swaps the stylesheet Render: render, Document: document, Handle: routes, })) } // routes registers everything the WASM app does not own. // // Order does not matter here — Go's ServeMux picks the most specific pattern, not the // first — but the shape does: /js/* belongs to a completely different front-end, and it // is claimed BEFORE the wasm app's "/" catch-all ever sees it. Two SPAs, one server, no // argument about who owns a URL. func routes(mux *http.ServeMux) { handlers.RegisterPublicPages(mux) // the SSR'd public pages (/js/ssr) mux.HandleFunc("GET /js/", serveJSApp) mux.HandleFunc("GET /js", serveJSApp) // /api/quotes responds with a gob-encoded []app.Quote (via httputil.RespondGob) — // the /wasm/data page fetches and decodes it on the client with encoding/gob (Go // types end to end, no JSON). mux.HandleFunc("GET /api/quotes", func(w http.ResponseWriter, r *http.Request) { httputil.RespondGob(w, http.StatusOK, sampleQuotes()) }) } // serveJSApp ships the shell for the Solid SPA. Every /js/* route gets the SAME empty // document — the client router reads the URL and decides what to render, which is what // makes it a single-page app. // // It carries no server-rendered markup, and that is a real difference from the Go/WASM // half rather than an oversight: this section is a docs section behind a click, where a // blank first frame costs nothing. Where it WOULD cost something, the public-page path // (see internal/handlers) renders on the server instead — /js/ssr is that, and it is // registered above, so it never reaches this handler. func serveJSApp(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, ` Kjol JS Web `+webui.ThemeBootScript+`
`) } func sampleQuotes() []app.Quote { return []app.Quote{ {Author: "Rob Pike", Text: "A little copying is better than a little dependency."}, {Author: "Rob Pike", Text: "Don't communicate by sharing memory; share memory by communicating."}, {Author: "Ken Thompson", Text: "When in doubt, use brute force."}, {Author: "Alan Kay", Text: "The best way to predict the future is to invent it."}, } } // render SSRs a static route's #app inner HTML; ok=false ships an empty #app // (client-rendered). It is the same neutral render the client runs, so the client // hydrates it. func render(path string) (string, bool) { if !app.StaticPaths[path] { return "", false } deps := app.Deps{Path: func() string { return path }} // Navigate is nil on the server return vdom.RenderHTML(app.Shell(deps, app.Routes(deps))), true } // document wraps the server-rendered inner HTML in the page shell. No whitespace // between
and the markup, so hydration's childNodes line up. The // dev server injects the livereload script before in watch mode. func document(inner string) string { // The theme boot script comes FIRST — before the stylesheet, before any markup. // // The server cannot read localStorage, so it cannot know which theme to render. If // the dark class were applied by the WebAssembly once it loads, a dark-mode user // would be shown a white page for as long as the binary takes to download and then // have it snatched away. This runs synchronously, before the first paint, so the // first paint is already right. It is the only JavaScript in the project. return ` kjol — a shared base layer ` + webui.ThemeBootScript + `
` + inner + `
` }