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>
53 lines
2.3 KiB
Plaintext
53 lines
2.3 KiB
Plaintext
;; lib/host/static.sx — serve the client kernel + assets so the blog can boot the
|
|
;; SX-htmx hypermedia engine (web/engine.sx) and run as a SPA. The native
|
|
;; http-listen host reads files with the `file-read` primitive (no perform), so
|
|
;; GET /static/** maps to a file under the static root (default "shared/static",
|
|
;; resolved against the server cwd — mount ./shared/static there in the container).
|
|
;; Depends on lib/dream/types.sx (dream-response/-html-status/-param) + router.
|
|
|
|
(define host/static-root "shared/static")
|
|
(define host/static-use-root! (fn (r) (set! host/static-root r)))
|
|
|
|
;; content-type by file extension; default to octet-stream.
|
|
(define host/static--ctype
|
|
(fn (path)
|
|
(cond
|
|
((ends-with? path ".js") "application/javascript; charset=utf-8")
|
|
((ends-with? path ".mjs") "application/javascript; charset=utf-8")
|
|
((ends-with? path ".css") "text/css; charset=utf-8")
|
|
((ends-with? path ".json") "application/json; charset=utf-8")
|
|
((ends-with? path ".map") "application/json; charset=utf-8")
|
|
((ends-with? path ".svg") "image/svg+xml")
|
|
((ends-with? path ".png") "image/png")
|
|
((ends-with? path ".woff2") "font/woff2")
|
|
((ends-with? path ".wasm") "application/wasm")
|
|
(true "application/octet-stream"))))
|
|
|
|
;; reject empty, absolute, or traversal paths.
|
|
(define host/static--safe?
|
|
(fn (rel)
|
|
(and (> (len rel) 0)
|
|
(not (starts-with? rel "/"))
|
|
(not (string-contains? rel "..")))))
|
|
|
|
;; Serve one asset by its path relative to the static root. file-read THROWS on a
|
|
;; missing file, so gate on file-exists? first and return a 404 instead.
|
|
(define host/static-serve
|
|
(fn (rel)
|
|
(if (not (host/static--safe? rel))
|
|
(dream-html-status 403 "Forbidden")
|
|
(let ((path (str host/static-root "/" rel)))
|
|
(if (not (file-exists? path))
|
|
(dream-html-status 404 "Not Found")
|
|
(dream-response 200
|
|
{:content-type (host/static--ctype rel)
|
|
:cache-control "public, max-age=3600"}
|
|
(file-read path)))))))
|
|
|
|
;; Route group: GET /static/** -> file under the static root. A plain route LIST
|
|
;; (like host/feed-routes); host/serve combines + flattens the groups itself.
|
|
(define host/static-routes
|
|
(list
|
|
(dream-get "/static/**"
|
|
(fn (req) (host/static-serve (dream-param req "**"))))))
|