118 lines
4.7 KiB
Go
118 lines
4.7 KiB
Go
// 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>`
|
|
}
|