host: blog SPA scaffolding (WASM kernel) — server side complete, boost blocked on bundle rebuild
Turn the blog into a SPA using the SX-htmx engine (web/engine.sx) booting the
WASM OCaml kernel (same evaluator as the server) in-browser, with sx-boost
fragment-swapping every link into #content.
Server side DONE + verified:
- lib/host/static.sx: GET /static/** serves shared/static via the file-read
primitive (ctype by ext, traversal-guarded, 404 on missing). Wired into
serve.sh (module + route group). Tested: kernel JS + .wasm binary-exact.
- host/blog--page is now the SPA shell: full page = WASM boot scripts +
sx-boost=#content wrapper + #content; on SX-Request:true returns ONLY the
inner content fragment for the engine to swap. All 13 handlers thread req.
- docker-compose mounts ./shared/static.
- lib/host/playwright/spa-check.{spec.js,run-spa-check.sh}: boot/boost/swap/back.
Client side: the WASM kernel BOOTS (SxKernel object, data-sx-ready=true, web
stack loads). BLOCKER: the bundled .sxbc throw 'VM: unknown opcode 0' vs this
worktree's kernel -> .sx source fallback -> boot.sx source fails 'Expected
list, got string' -> process-boosted never binds links (boosted 0/N). Fix =
rebuild a consistent WASM bundle (recompile .sxbc against the kernel via
scripts/sx-build-all.sh); the browser wasm target isn't built here yet. See
plans/host-spa.md. Live NOT redeployed (stays on pre-SPA process).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -404,14 +404,35 @@
|
||||
;; builds the tree (running any dynamic logic in the full evaluator, e.g. a posts
|
||||
;; loop) and render-page renders the static result — no embedded HTML strings,
|
||||
;; only the doctype prefix render-to-html doesn't emit. `body` is an SX node.
|
||||
;; SPA shell. The blog is a single-page app: the page boots the WASM OCaml kernel
|
||||
;; (the SAME evaluator as the server) + the SX-htmx engine (web/engine.sx), and
|
||||
;; `sx-boost="#content"` turns every in-page link/form into a fragment swap into
|
||||
;; #content — no full reloads, history handled. A boosted request carries the
|
||||
;; SX-Request:true header; we then return ONLY the inner content (so the engine
|
||||
;; swaps it straight into #content). A direct / no-JS request gets the full shell,
|
||||
;; so the blog degrades gracefully to plain server-rendered pages.
|
||||
(define host/blog--spa-req? (fn (req) (= (dream-header req "sx-request") "true")))
|
||||
|
||||
(define host/blog--page
|
||||
(fn (title body)
|
||||
(str "<!doctype html>"
|
||||
(render-page
|
||||
(quasiquote
|
||||
(html
|
||||
(head (meta :charset "utf-8") (title (unquote title)))
|
||||
(body (unquote body))))))))
|
||||
(fn (req title body)
|
||||
(if (host/blog--spa-req? req)
|
||||
;; fragment: inner content only — engine swaps it into #content
|
||||
(render-page body)
|
||||
;; full SPA shell: WASM kernel + platform + boosted #content
|
||||
(str "<!doctype html>"
|
||||
(render-page
|
||||
(quasiquote
|
||||
(html
|
||||
(head (meta :charset "utf-8") (title (unquote title))
|
||||
(script :src "/static/wasm/sx_browser.bc.wasm.js")
|
||||
(script :src "/static/wasm/sx-platform.js"))
|
||||
(body
|
||||
;; sx-boost must be on a DESCENDANT of <body> (process-boosted
|
||||
;; queries [sx-boost] WITHIN the body, so it can't sit on body
|
||||
;; itself). The wrapper boosts every link/form inside, targeting
|
||||
;; #content; #content is the swap target.
|
||||
(div :sx-boost "#content"
|
||||
(div :id "content" (unquote body)))))))))))
|
||||
|
||||
;; ── registry-driven relation rendering (post page) ──────────────────
|
||||
;; One labelled block of links from records ({:slug :title}), or "" when empty.
|
||||
@@ -576,7 +597,7 @@
|
||||
(relations (host/blog--relations-or-hint slug (not (nil? principal))))
|
||||
(auth-foot (host/auth-footer req)))
|
||||
(dream-html
|
||||
(host/blog--page (get r :title)
|
||||
(host/blog--page req (get r :title)
|
||||
(quasiquote
|
||||
(div
|
||||
(article (raw! (unquote body-html)))
|
||||
@@ -590,7 +611,7 @@
|
||||
" · "
|
||||
(unquote auth-foot))))))))
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(host/blog--page req "Not found"
|
||||
(quasiquote
|
||||
(div (h1 "404")
|
||||
(p (unquote (str "No published post: " slug))))))))))))
|
||||
@@ -612,7 +633,7 @@
|
||||
;; quasiquote (a perform during tree-build raises VmSuspended).
|
||||
(auth-foot (host/auth-footer req)))
|
||||
(dream-html
|
||||
(host/blog--page "Blog"
|
||||
(host/blog--page req "Blog"
|
||||
(quasiquote
|
||||
(div (h1 "Posts")
|
||||
(unquote listing)
|
||||
@@ -637,7 +658,7 @@
|
||||
(unquote (get p :title))))))
|
||||
recs)))
|
||||
(dream-html
|
||||
(host/blog--page "Tags"
|
||||
(host/blog--page req "Tags"
|
||||
(quasiquote
|
||||
(div (h1 "Tags")
|
||||
(unquote (if (> (len recs) 0)
|
||||
@@ -657,7 +678,7 @@
|
||||
(dream-response 200 {:content-type "text/plain; charset=utf-8"}
|
||||
(or (get r :sx-content) ""))
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(host/blog--page req "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug))))))))))))
|
||||
|
||||
;; ── create page (GET /new) — clean minimal form as an SX tree ───────
|
||||
@@ -666,7 +687,7 @@
|
||||
(define host/blog-new-form
|
||||
(fn (req)
|
||||
(dream-html
|
||||
(host/blog--page "New post"
|
||||
(host/blog--page req "New post"
|
||||
(quasiquote
|
||||
(div
|
||||
(h1 "New post")
|
||||
@@ -706,12 +727,12 @@
|
||||
(cond
|
||||
((or (nil? title) (= title ""))
|
||||
(dream-html-status 400
|
||||
(host/blog--page "Error"
|
||||
(host/blog--page req "Error"
|
||||
(quasiquote (div (h1 "Error") (p "Title is required.")
|
||||
(p (a :href "/new" "Back")))))))
|
||||
((not (host/blog-content-ok? sx-content))
|
||||
(dream-html-status 400
|
||||
(host/blog--page "Error"
|
||||
(host/blog--page req "Error"
|
||||
(quasiquote (div (h1 "Error") (p "Post body is not valid SX markup.")
|
||||
(p (a :href "/new" "Back")))))))
|
||||
(else
|
||||
@@ -793,7 +814,7 @@
|
||||
(kind (or (dream-form-field req "kind") "related")))
|
||||
(if (nil? (host/blog-get slug))
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(host/blog--page req "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||
(begin
|
||||
(when (and other (not (= other "")) (not (= other slug))
|
||||
@@ -823,7 +844,7 @@
|
||||
(let ((r (host/blog-get slug)))
|
||||
(if (nil? r)
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(host/blog--page req "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||
(let ((status (get r :status)))
|
||||
;; the relation editors + tag toggle do durable reads — compute them
|
||||
@@ -836,7 +857,7 @@
|
||||
(quasiquote (option :value (unquote val) :selected "selected" (unquote label)))
|
||||
(quasiquote (option :value (unquote val) (unquote label)))))))
|
||||
(dream-html
|
||||
(host/blog--page (str "Edit: " (get r :title))
|
||||
(host/blog--page req (str "Edit: " (get r :title))
|
||||
(quasiquote
|
||||
(div
|
||||
(h1 (unquote (str "Edit: " (get r :title))))
|
||||
@@ -868,7 +889,7 @@
|
||||
(let ((slug (dream-param req "slug")) (r (host/blog-get (dream-param req "slug"))))
|
||||
(if (nil? r)
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(host/blog--page req "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||
(let ((title (or (dream-form-field req "title") (get r :title)))
|
||||
(sx-content (or (dream-form-field req "sx_content") ""))
|
||||
@@ -884,7 +905,7 @@
|
||||
(dream-redirect (str "/" slug "/")))
|
||||
(let ((issue-items (map (fn (i) (quasiquote (li (unquote i)))) issues)))
|
||||
(dream-html-status 400
|
||||
(host/blog--page "Cannot save"
|
||||
(host/blog--page req "Cannot save"
|
||||
(quasiquote
|
||||
(div (h1 "Cannot save")
|
||||
(p "This post can't be saved yet:")
|
||||
|
||||
Reference in New Issue
Block a user