// Package handlers serves the server-rendered public pages of the Kjol JS Web // section. // // This file is the APP side of a coupling inversion. kjol's bundler renders each // public page at build time and generates public_pages.gen.go — a list of routes, // titles, baked HTML, and (for dynamic pages) the render bundle. It does not know // what a page is served as: no document shell, no stylesheet paths, no data. That // is all here, because all of it is the application's business. // // The generated file declares `var publicPages = []publicPage{...}` and nothing // else. The TYPE is ours — which is what lets the shape of a page be an app concern // while the rendering of one stays the framework's. package handlers import ( "encoding/json" "fmt" "log" "net/http" "time" "kjol/jsbundler" "kjol/webui" ) // publicPage is the app-side shape the generated registry is written against. // Field names and order are the generator's contract (jsbundler/genssr.go). type publicPage struct { route string // URL path, e.g. "/js/ssr" title string // text module string // page module relative to frontend/src (informational) component string // exported body component name (informational) html string // pre-rendered, data-free page body (PublicLayout + page content) renderJS string // bundled render entry; baked ONLY for dynamic (ISR) pages } // buildInfo is the payload injected into the /js/ssr page. It mirrors the // `BuildInfo` interface the component reads via serverData<T>() — the two have to // agree, and the JSON tags are the whole of that agreement. type buildInfo struct { RenderedAt string `json:"renderedAt"` Stage string `json:"stage"` } // RegisterPublicPages binds every generated public page to its route. // // A page with a render bundle is rendered PER REQUEST with live data (the ISR // path). A page without one serves the skeleton that was baked at build time. Both // ship complete HTML; the difference is only whether the numbers in it are fresh. func RegisterPublicPages(mux *http.ServeMux) { for _, p := range publicPages { mux.HandleFunc("GET "+p.route, servePublicPage(p)) } } func servePublicPage(p publicPage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := p.html data := "" // The ISR path. The SAME Solid component that was baked at build time is run // again here, in goja, with data injected — so the server's markup is not a // template with holes punched in it, it is the component's own output. if p.renderJS != "" { payload, err := json.Marshal(buildInfo{ RenderedAt: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), Stage: "request time, in goja", }) if err != nil { log.Printf("public page %s: marshalling data: %v", p.route, err) } else if rendered, err := jsbundler.RenderBundleWithData(p.renderJS, string(payload)); err != nil { // Fall through to the baked skeleton rather than 500. A page that cannot // render with data is still a page; serving nothing helps no one. log.Printf("public page %s: ISR render failed, serving skeleton: %v", p.route, err) } else { body, data = rendered, string(payload) } } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, document(p.title, body, data)) } } // document wraps a rendered body in the page shell. // // __SERVER_DATA__ is inlined BEFORE the bundle, and it is the same JSON the server // just rendered with. That is what makes the client takeover silent: public.tsx // re-renders the identical component against the identical data and produces the // identical markup, so the swap is invisible. Omit it and the page would render, then // visibly collapse back to its loading skeleton the moment the bundle loaded. func document(title, body, data string) string { serverData := "" if data != "" { serverData = "\n<script>window.__SERVER_DATA__ = " + data + ";</script>" } // webui.ThemeBootScript is the Go/WASM kit's — reused verbatim, because it reads the // same "kjol-theme" key the Solid kit's controller writes. One script, one key, and a // reader's choice of theme survives crossing between two front-ends that share // nothing else. It goes BEFORE the stylesheet, or a dark-mode reader gets a white // page until the CSS lands. return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>` + title + ` ` + webui.ThemeBootScript + `
` + body + `
` + serverData + ` ` }