diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml
index 9e4ab1b6..66816226 100644
--- a/hosts/ocaml/bin/sx_server.ml
+++ b/hosts/ocaml/bin/sx_server.ml
@@ -1499,9 +1499,7 @@ let http_render_page env path =
| [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: body_exprs) in
Sx_render.render_to_html_streaming body_expr env
with e ->
- Printf.eprintf "[http-ssr] failed: %s\n%!" (Printexc.to_string e);
- (* Fallback: minimal layout structure so client can mount *)
- "
"
+ Printf.eprintf "[http-ssr] failed: %s\n%!" (Printexc.to_string e); ""
in
let t3 = Unix.gettimeofday () in
(* Phase 3: Shell — render directly to buffer for zero-copy output *)
@@ -1621,25 +1619,14 @@ let http_inject_shell_statics env static_dir sx_sxc =
| _ -> ()
) env.bindings;
let raw_defs = Buffer.contents buf in
- (* Escape scans for (case-insensitive) to
- close the tag. Replace with <\/ when followed by 's' or 'S'.
- The \/ is treated as / by the SX parser. *)
- let component_defs =
- let len = String.length raw_defs in
- let out = Buffer.create (len + 256) in
- let i = ref 0 in
- while !i < len do
- if !i + 2 < len && raw_defs.[!i] = '<' && raw_defs.[!i + 1] = '/'
- && (raw_defs.[!i + 2] = 's' || raw_defs.[!i + 2] = 'S') then begin
- Buffer.add_string out "<\\/";
- i := !i + 2
- end else begin
- Buffer.add_char out raw_defs.[!i];
- i := !i + 1
- end
- done;
- Buffer.contents out in
+ (* Don't inline component-defs — serve from /static/sx-components.sx.
+ Inlining breaks because SX source can contain literal which
+ the HTML parser uses to close the tag prematurely. *)
+ let component_defs = "" in
+ (* Cache the raw defs for the /static/sx-components.sx endpoint *)
+ Hashtbl.replace static_cache "/static/sx-components.sx"
+ (Printf.sprintf "HTTP/1.1 200 OK\r\nContent-Type: text/sx; charset=utf-8\r\nContent-Length: %d\r\nCache-Control: public, max-age=31536000, immutable\r\n\r\n%s"
+ (String.length raw_defs) raw_defs);
let component_hash = Digest.string component_defs |> Digest.to_hex in
(* Compute file hashes for cache busting *)
let sx_js_hash = file_hash (static_dir ^ "/scripts/sx-browser.js") in
@@ -1704,7 +1691,16 @@ let http_inject_shell_statics env static_dir sx_sxc =
ignore (env_bind env "__shell-body-scripts" Nil);
ignore (env_bind env "__shell-inline-css" Nil);
ignore (env_bind env "__shell-inline-head-js" Nil);
- ignore (env_bind env "__shell-init-sx" Nil);
+ (* init-sx: trigger client-side render when sx-root is empty (SSR failed).
+ The boot code hydrates existing islands but doesn't do fresh render.
+ This script forces a render from page-sx after boot completes. *)
+ ignore (env_bind env "__shell-init-sx" (String
+ "document.addEventListener('sx:boot-done', function() { \
+ var root = document.getElementById('sx-root'); \
+ if (root && !root.innerHTML.trim() && typeof SX !== 'undefined' && SX.renderPage) { \
+ SX.renderPage(); \
+ } \
+ });"));
Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d js=%s wasm=%s\n%!"
(String.length component_defs) component_hash (String.length sx_css) sx_js_hash wasm_hash
diff --git a/hosts/ocaml/lib/sx_render.ml b/hosts/ocaml/lib/sx_render.ml
index 92bc351a..d8201501 100644
--- a/hosts/ocaml/lib/sx_render.ml
+++ b/hosts/ocaml/lib/sx_render.ml
@@ -258,21 +258,8 @@ and render_list_to_html head args env =
| Component c when c.c_affinity = "client" -> "" (* skip client-only *)
| Component _ -> render_component v args env
| Island _i ->
- (* Islands: try SSR via the SX render-to-html from adapter-html.sx.
- It handles deref/signal/computed through the CEK correctly,
- and renders island bodies with hydration markers.
- On failure, emit a placeholder — client hydrates from SX source. *)
- (try
- let call_expr = List (Symbol name :: args) in
- let quoted = List [Symbol "quote"; call_expr] in
- let render_call = List [Symbol "render-to-html"; quoted; Env env] in
- let result = Sx_ref.eval_expr render_call (Env env) in
- (match result with
- | String s | RawHTML s -> s
- | _ -> value_to_string result)
- with _e ->
- (* Placeholder — client will hydrate this island *)
- Printf.sprintf "" _i.i_name)
+ (* Islands are client-rendered — emit placeholder for hydration *)
+ Printf.sprintf "" _i.i_name
| Macro m ->
let expanded = expand_macro m args env in
do_render_to_html expanded env
@@ -516,16 +503,10 @@ and render_list_buf buf head args env =
| Component c when c.c_affinity = "client" -> ()
| Component _ -> render_component_buf buf v args env
| Island _i ->
- (try
- let call_expr = List (Symbol name :: args) in
- let quoted = List [Symbol "quote"; call_expr] in
- let render_call = List [Symbol "render-to-html"; quoted; Env env] in
- let result = Sx_ref.eval_expr render_call (Env env) in
- (match result with
- | String s | RawHTML s -> Buffer.add_string buf s
- | _ -> Buffer.add_string buf (value_to_string result))
- with _e ->
- Buffer.add_string buf (Printf.sprintf "" _i.i_name))
+ (* Islands are client-rendered — emit placeholder for hydration.
+ SSR of island bodies is unreliable (client-only symbols like
+ signals, DOM refs) and can cascade errors. *)
+ Buffer.add_string buf (Printf.sprintf "" _i.i_name)
| Macro m ->
let expanded = expand_macro m args env in
render_to_buf buf expanded env