Update 3d chart mode, add US heatmap, move kjol-web -> kjol-website
This commit is contained in:
117
go/cmd/kjol-website/internal/handlers/public.go
Normal file
117
go/cmd/kjol-website/internal/handlers/public.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Package handlers serves the server-rendered public pages of the Kjøl 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 // <title> 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 + `</title>
|
||||
` + webui.ThemeBootScript + `
|
||||
<link rel="stylesheet" href="/public.bundle.min.css" />
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<div id="page-root">` + body + `</div>` + serverData + `
|
||||
<script type="module" src="/public.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
Reference in New Issue
Block a user