From 41f3e9b27612d04886ca89145d4ea3ffdab88ddc Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 29 Jun 2026 13:03:48 +0000 Subject: [PATCH] host: SPA fragments are SX wire format (text/sx), rendered client-side by the WASM kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boosted (SPA) requests now return the SX source of the content (serialize) with content-type text/sx, so the engine's handle-sx-response parses + sx-renders it client-side on the WASM OCaml kernel — instead of server-rendered HTML. Direct / no-JS requests still get the full HTML shell (SEO + first paint). - host/blog--page: fragment branch serializes the body tree to SX wire format (was render-page -> HTML); full branch unchanged (HTML shell). - host/blog--resp: new content-type-aware wrapper (text/sx for boosted, text/html otherwise); replaced the 13 dream-html/dream-html-status call-site wrappers. - listings built with (cons (quote ul) items) not (list (quote ul) items): the list form nests children as one list and relied on render-to-html flattening it; sx-render (client) treats (li ...) as a call -> 'Not callable'. cons splices them into canonical (ul li1 li2 ...) that renders identically on both sides. Verified: native host conformance 271/271; SX-Request returns text/sx SX source, direct request text/html; lib/host/playwright/spa-check 4/4 (boot, boost, SX fragment swap, back button) in chromium. Co-Authored-By: Claude Opus 4.8 --- lib/host/blog.sx | 56 ++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/lib/host/blog.sx b/lib/host/blog.sx index 8a668999..ff5de42b 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -416,9 +416,12 @@ (define host/blog--page (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 + ;; SPA fragment: SX WIRE FORMAT (text/sx), not HTML. The WASM kernel parses + ;; + renders it client-side into #content (the engine's handle-sx-response). + ;; No server-side HTML render on the boosted path. + (serialize body) + ;; full SPA shell: WASM kernel + platform + boosted #content (server HTML + ;; for first load / no-JS / SEO) (str "" (render-page (quasiquote @@ -434,6 +437,17 @@ (div :sx-boost "#content" (div :id "content" (unquote body))))))))))) +;; Wrap a host/blog--page result in a response with the matching content-type: +;; text/sx for a boosted (SPA) request (the WASM kernel renders it), text/html +;; for a full-page request. Replaces the old dream-html/-status wrappers so the +;; boosted path ships SX instead of server-rendered HTML. +(define host/blog--resp + (fn (req status str) + (dream-response status + {:content-type + (if (host/blog--spa-req? req) "text/sx; charset=utf-8" "text/html; charset=utf-8")} + str))) + ;; ── registry-driven relation rendering (post page) ────────────────── ;; One labelled block of links from records ({:slug :title}), or "" when empty. ;; Records are pre-fetched, so the tree is built from in-memory data only. @@ -448,7 +462,7 @@ (quasiquote (div :style "margin-top:2em" (h3 (unquote label)) - (unquote (list (quote ul) items))))) + (unquote (cons (quote ul) items))))) ""))) ;; nodes -> {:slug :title} records, existence-filtered against a shared key set. @@ -536,7 +550,7 @@ (h3 (unquote (get spec :label))) (unquote (if (> (len current) 0) - (list (quote ul) + (cons (quote ul) (map (fn (s) (quasiquote (li (a :href (unquote (str "/" s "/")) (unquote s)) " " @@ -596,7 +610,7 @@ ;; come from iterating the registry — one section, registry-driven. (relations (host/blog--relations-or-hint slug (not (nil? principal)))) (auth-foot (host/auth-footer req))) - (dream-html + (host/blog--resp req 200 (host/blog--page req (get r :title) (quasiquote (div @@ -610,7 +624,7 @@ (a :href "/" "all posts") " · " (unquote auth-foot)))))))) - (dream-html-status 404 + (host/blog--resp req 404 (host/blog--page req "Not found" (quasiquote (div (h1 "404") @@ -627,12 +641,12 @@ (unquote (get p :title)))))) posts))) (let ((listing (if (> (len posts) 0) - (list (quote ul) items) + (cons (quote ul) items) (quote (p "No posts yet.")))) ;; auth-footer does a durable session read — bind it BEFORE the ;; quasiquote (a perform during tree-build raises VmSuspended). (auth-foot (host/auth-footer req))) - (dream-html + (host/blog--resp req 200 (host/blog--page req "Blog" (quasiquote (div (h1 "Posts") @@ -657,12 +671,12 @@ (li (a :href (unquote (str "/" (get p :slug) "/")) (unquote (get p :title)))))) recs))) - (dream-html + (host/blog--resp req 200 (host/blog--page req "Tags" (quasiquote (div (h1 "Tags") (unquote (if (> (len recs) 0) - (list (quote ul) items) + (cons (quote ul) items) (quote (p "No tags yet.")))) (p :style "margin-top:2em;font-size:0.9em;opacity:0.8" (a :href "/" "all posts") " · " (unquote auth-foot)))))))))) @@ -677,7 +691,7 @@ (if r (dream-response 200 {:content-type "text/plain; charset=utf-8"} (or (get r :sx-content) "")) - (dream-html-status 404 + (host/blog--resp req 404 (host/blog--page req "Not found" (quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))))))) @@ -686,7 +700,7 @@ ;; future native SX-island editor (Phase 5.2+). Posts to /new. (define host/blog-new-form (fn (req) - (dream-html + (host/blog--resp req 200 (host/blog--page req "New post" (quasiquote (div @@ -726,12 +740,12 @@ (status (or (dream-form-field req "status") "published"))) (cond ((or (nil? title) (= title "")) - (dream-html-status 400 + (host/blog--resp req 400 (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--resp req 400 (host/blog--page req "Error" (quasiquote (div (h1 "Error") (p "Post body is not valid SX markup.") (p (a :href "/new" "Back"))))))) @@ -813,7 +827,7 @@ (other (dream-form-field req "other")) (kind (or (dream-form-field req "kind") "related"))) (if (nil? (host/blog-get slug)) - (dream-html-status 404 + (host/blog--resp req 404 (host/blog--page req "Not found" (quasiquote (div (h1 "404") (p (unquote (str "No post: " slug))))))) (begin @@ -843,7 +857,7 @@ (let ((slug (dream-param req "slug"))) (let ((r (host/blog-get slug))) (if (nil? r) - (dream-html-status 404 + (host/blog--resp req 404 (host/blog--page req "Not found" (quasiquote (div (h1 "404") (p (unquote (str "No post: " slug))))))) (let ((status (get r :status))) @@ -856,7 +870,7 @@ (if (= val status) (quasiquote (option :value (unquote val) :selected "selected" (unquote label))) (quasiquote (option :value (unquote val) (unquote label))))))) - (dream-html + (host/blog--resp req 200 (host/blog--page req (str "Edit: " (get r :title)) (quasiquote (div @@ -888,7 +902,7 @@ (fn (req) (let ((slug (dream-param req "slug")) (r (host/blog-get (dream-param req "slug")))) (if (nil? r) - (dream-html-status 404 + (host/blog--resp req 404 (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))) @@ -904,12 +918,12 @@ (host/blog-put! slug title sx-content status) (dream-redirect (str "/" slug "/"))) (let ((issue-items (map (fn (i) (quasiquote (li (unquote i)))) issues))) - (dream-html-status 400 + (host/blog--resp req 400 (host/blog--page req "Cannot save" (quasiquote (div (h1 "Cannot save") (p "This post can't be saved yet:") - (unquote (list (quote ul) issue-items)) + (unquote (cons (quote ul) issue-items)) (p (a :href (unquote (str "/" slug "/edit")) "Back")))))))))))))) ;; ── routes ──────────────────────────────────────────────────────────