Isomorphic SSR: server renders HTML body, client takes over with SX
Server now renders page content as HTML inside <div id="sx-root">, visible immediately before JavaScript loads. The SX source is still included in a <script data-mount="#sx-root"> tag for client hydration. SSR pipeline: after aser produces the SX wire format, parse and render-to-html it (~17ms for a 22KB page). Islands with reactive state gracefully fall back to empty — client hydrates them. Supporting changes: - Load signals.sx into OCaml kernel (reactive primitives for island SSR) - Add cek-call and context to kernel env (needed by signals/deref) - Island-aware component accessors in sx_types.ml - render-to-html handles Island values (renders as component with fallback) - Fix 431 (Request Header Fields Too Large): replace SX-Components header (full component name list) with SX-Components-Hash (12 chars) - CORS allow SX-Components-Hash header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -425,7 +425,7 @@ let make_server_env () =
|
|||||||
(* context — scope lookup. The CEK handles this as a special form
|
(* context — scope lookup. The CEK handles this as a special form
|
||||||
by walking continuation frames, but compiled VM code needs it as
|
by walking continuation frames, but compiled VM code needs it as
|
||||||
a function that reads from the scope_stacks hashtable. *)
|
a function that reads from the scope_stacks hashtable. *)
|
||||||
bind "sx-context" (fun args ->
|
let context_impl = NativeFn ("context", fun args ->
|
||||||
match args with
|
match args with
|
||||||
| [String name] | [String name; _] ->
|
| [String name] | [String name; _] ->
|
||||||
let stack = try Hashtbl.find scope_stacks name with Not_found -> [] in
|
let stack = try Hashtbl.find scope_stacks name with Not_found -> [] in
|
||||||
@@ -433,7 +433,9 @@ let make_server_env () =
|
|||||||
| v :: _, _ -> v
|
| v :: _, _ -> v
|
||||||
| [], [_; default_val] -> default_val
|
| [], [_; default_val] -> default_val
|
||||||
| [], _ -> Nil)
|
| [], _ -> Nil)
|
||||||
| _ -> Nil);
|
| _ -> Nil) in
|
||||||
|
ignore (env_bind env "sx-context" context_impl);
|
||||||
|
ignore (env_bind env "context" context_impl);
|
||||||
|
|
||||||
(* qq-expand-runtime — quasiquote expansion at runtime.
|
(* qq-expand-runtime — quasiquote expansion at runtime.
|
||||||
The bytecode compiler emits CALL_PRIM "qq-expand-runtime" for
|
The bytecode compiler emits CALL_PRIM "qq-expand-runtime" for
|
||||||
@@ -464,6 +466,12 @@ let make_server_env () =
|
|||||||
| [fn_val; List call_args] ->
|
| [fn_val; List call_args] ->
|
||||||
Sx_ref.eval_expr (List (fn_val :: call_args)) (Env env)
|
Sx_ref.eval_expr (List (fn_val :: call_args)) (Env env)
|
||||||
| _ -> raise (Eval_error "call-lambda: expected (fn args env?)"));
|
| _ -> raise (Eval_error "call-lambda: expected (fn args env?)"));
|
||||||
|
bind "cek-call" (fun args ->
|
||||||
|
match args with
|
||||||
|
| [fn_val; List call_args] -> Sx_ref.cek_call fn_val (List call_args)
|
||||||
|
| [fn_val; Nil] -> Sx_ref.cek_call fn_val (List [])
|
||||||
|
| [fn_val] -> Sx_ref.cek_call fn_val (List [])
|
||||||
|
| _ -> Nil);
|
||||||
bind "expand-macro" (fun args ->
|
bind "expand-macro" (fun args ->
|
||||||
match args with
|
match args with
|
||||||
| [Macro m; List macro_args; Env e] ->
|
| [Macro m; List macro_args; Env e] ->
|
||||||
@@ -1072,6 +1080,22 @@ let rec dispatch env cmd =
|
|||||||
in
|
in
|
||||||
let body_final = flush_batched_io body_str in
|
let body_final = flush_batched_io body_str in
|
||||||
let t2 = Unix.gettimeofday () in
|
let t2 = Unix.gettimeofday () in
|
||||||
|
(* Phase 1b: render the aser'd SX to HTML for isomorphic SSR.
|
||||||
|
The aser output is flat (all components expanded, just HTML tags),
|
||||||
|
so render-to-html is cheap — no component lookups needed. *)
|
||||||
|
let body_html =
|
||||||
|
try
|
||||||
|
let body_exprs = Sx_parser.parse_all body_final in
|
||||||
|
let body_expr = match body_exprs with
|
||||||
|
| [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: body_exprs)
|
||||||
|
in
|
||||||
|
Sx_render.render_to_html body_expr env
|
||||||
|
with e ->
|
||||||
|
Printf.eprintf "[ssr] render-to-html failed: %s\n%!" (Printexc.to_string e);
|
||||||
|
"" (* fallback: client renders from SX source. Islands with
|
||||||
|
reactive state may fail SSR — client hydrates them. *)
|
||||||
|
in
|
||||||
|
let t2b = Unix.gettimeofday () in
|
||||||
(* Phase 2: render shell with body + all kwargs.
|
(* Phase 2: render shell with body + all kwargs.
|
||||||
Resolve symbol references (e.g. __shell-component-defs) to their
|
Resolve symbol references (e.g. __shell-component-defs) to their
|
||||||
values from the env — these were pre-injected by the bridge. *)
|
values from the env — these were pre-injected by the bridge. *)
|
||||||
@@ -1082,13 +1106,15 @@ let rec dispatch env cmd =
|
|||||||
with _ -> try Sx_primitives.get_primitive s with _ -> v)
|
with _ -> try Sx_primitives.get_primitive s with _ -> v)
|
||||||
| _ -> v
|
| _ -> v
|
||||||
) shell_kwargs in
|
) shell_kwargs in
|
||||||
let shell_args = Keyword "page-sx" :: String body_final :: resolved_kwargs in
|
let shell_args = Keyword "page-sx" :: String body_final
|
||||||
|
:: Keyword "body-html" :: String body_html
|
||||||
|
:: resolved_kwargs in
|
||||||
let shell_call = List (Symbol "~shared:shell/sx-page-shell" :: shell_args) in
|
let shell_call = List (Symbol "~shared:shell/sx-page-shell" :: shell_args) in
|
||||||
let html = Sx_render.render_to_html shell_call env in
|
let html = Sx_render.render_to_html shell_call env in
|
||||||
let t3 = Unix.gettimeofday () in
|
let t3 = Unix.gettimeofday () in
|
||||||
Printf.eprintf "[sx-page-full] aser=%.3fs io=%.3fs shell=%.3fs total=%.3fs body=%d html=%d\n%!"
|
Printf.eprintf "[sx-page-full] aser=%.3fs io=%.3fs ssr=%.3fs shell=%.3fs total=%.3fs body=%d ssr=%d html=%d\n%!"
|
||||||
(t1 -. t0) (t2 -. t1) (t3 -. t2) (t3 -. t0)
|
(t1 -. t0) (t2 -. t1) (t2b -. t2) (t3 -. t2b) (t3 -. t0)
|
||||||
(String.length body_final) (String.length html);
|
(String.length body_final) (String.length body_html) (String.length html);
|
||||||
send_ok_string html
|
send_ok_string html
|
||||||
with
|
with
|
||||||
| Eval_error msg ->
|
| Eval_error msg ->
|
||||||
|
|||||||
@@ -256,6 +256,18 @@ and render_list_to_html head args env =
|
|||||||
let v = env_get env name in
|
let v = env_get env name in
|
||||||
(match v with
|
(match v with
|
||||||
| Component _ -> render_component v args env
|
| Component _ -> render_component v args env
|
||||||
|
| Island i ->
|
||||||
|
(* Islands: render initial HTML server-side (like React SSR).
|
||||||
|
Log failures so we can fix them. *)
|
||||||
|
(try
|
||||||
|
let c = { c_name = i.i_name; c_params = i.i_params;
|
||||||
|
c_has_children = i.i_has_children; c_body = i.i_body;
|
||||||
|
c_closure = i.i_closure; c_affinity = "client";
|
||||||
|
c_compiled = None } in
|
||||||
|
render_component (Component c) args env
|
||||||
|
with e ->
|
||||||
|
Printf.eprintf "[ssr-island] ~%s FAILED: %s\n%!" i.i_name (Printexc.to_string e);
|
||||||
|
"")
|
||||||
| Macro m ->
|
| Macro m ->
|
||||||
let expanded = expand_macro m args env in
|
let expanded = expand_macro m args env in
|
||||||
do_render_to_html expanded env
|
do_render_to_html expanded env
|
||||||
|
|||||||
@@ -347,26 +347,32 @@ let set_lambda_name l n = match l with
|
|||||||
|
|
||||||
let component_name = function
|
let component_name = function
|
||||||
| Component c -> String c.c_name
|
| Component c -> String c.c_name
|
||||||
|
| Island i -> String i.i_name
|
||||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||||
|
|
||||||
let component_params = function
|
let component_params = function
|
||||||
| Component c -> List (List.map (fun s -> String s) c.c_params)
|
| Component c -> List (List.map (fun s -> String s) c.c_params)
|
||||||
|
| Island i -> List (List.map (fun s -> String s) i.i_params)
|
||||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||||
|
|
||||||
let component_body = function
|
let component_body = function
|
||||||
| Component c -> c.c_body
|
| Component c -> c.c_body
|
||||||
|
| Island i -> i.i_body
|
||||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||||
|
|
||||||
let component_closure = function
|
let component_closure = function
|
||||||
| Component c -> Env c.c_closure
|
| Component c -> Env c.c_closure
|
||||||
|
| Island i -> Env i.i_closure
|
||||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||||
|
|
||||||
let component_has_children = function
|
let component_has_children = function
|
||||||
| Component c -> Bool c.c_has_children
|
| Component c -> Bool c.c_has_children
|
||||||
|
| Island i -> Bool i.i_has_children
|
||||||
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
|
||||||
|
|
||||||
let component_affinity = function
|
let component_affinity = function
|
||||||
| Component c -> String c.c_affinity
|
| Component c -> String c.c_affinity
|
||||||
|
| Island _ -> String "client"
|
||||||
| _ -> String "auto"
|
| _ -> String "auto"
|
||||||
|
|
||||||
let macro_params = function
|
let macro_params = function
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ def create_base_app(
|
|||||||
response.headers["Access-Control-Allow-Origin"] = origin
|
response.headers["Access-Control-Allow-Origin"] = origin
|
||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
response.headers["Access-Control-Allow-Headers"] = (
|
response.headers["Access-Control-Allow-Headers"] = (
|
||||||
"SX-Request, SX-Target, SX-Current-URL, SX-Components, SX-Css, "
|
"SX-Request, SX-Target, SX-Current-URL, SX-Components, SX-Components-Hash, SX-Css, "
|
||||||
"HX-Request, HX-Target, HX-Current-URL, HX-Trigger, "
|
"HX-Request, HX-Target, HX-Current-URL, HX-Trigger, "
|
||||||
"Content-Type, X-CSRFToken"
|
"Content-Type, X-CSRFToken"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-23T08:59:15Z";
|
var SX_VERSION = "2026-03-23T13:21:57Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -1582,7 +1582,13 @@ PRIMITIVES["step-sf-lambda"] = stepSfLambda;
|
|||||||
var val = NIL;
|
var val = NIL;
|
||||||
var body = NIL;
|
var body = NIL;
|
||||||
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((val = trampoline(evalExpr(nth(restArgs, 1), env))), (body = slice(restArgs, 2))) : (body = restArgs));
|
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((val = trampoline(evalExpr(nth(restArgs, 1), env))), (body = slice(restArgs, 2))) : (body = restArgs));
|
||||||
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, rest(body), env), kont))));
|
scopePush(name, val);
|
||||||
|
return (function() {
|
||||||
|
var result = NIL;
|
||||||
|
{ var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } }
|
||||||
|
scopePop(name);
|
||||||
|
return makeCekValue(result, env, kont);
|
||||||
|
})();
|
||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["step-sf-scope"] = stepSfScope;
|
PRIMITIVES["step-sf-scope"] = stepSfScope;
|
||||||
|
|
||||||
@@ -1591,7 +1597,13 @@ PRIMITIVES["step-sf-scope"] = stepSfScope;
|
|||||||
var name = trampoline(evalExpr(first(args), env));
|
var name = trampoline(evalExpr(first(args), env));
|
||||||
var val = trampoline(evalExpr(nth(args, 1), env));
|
var val = trampoline(evalExpr(nth(args, 1), env));
|
||||||
var body = slice(args, 2);
|
var body = slice(args, 2);
|
||||||
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, rest(body), env), kont))));
|
scopePush(name, val);
|
||||||
|
return (function() {
|
||||||
|
var result = NIL;
|
||||||
|
{ var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } }
|
||||||
|
scopePop(name);
|
||||||
|
return makeCekValue(result, env, kont);
|
||||||
|
})();
|
||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["step-sf-provide"] = stepSfProvide;
|
PRIMITIVES["step-sf-provide"] = stepSfProvide;
|
||||||
|
|
||||||
@@ -1599,8 +1611,8 @@ PRIMITIVES["step-sf-provide"] = stepSfProvide;
|
|||||||
var stepSfContext = function(args, env, kont) { return (function() {
|
var stepSfContext = function(args, env, kont) { return (function() {
|
||||||
var name = trampoline(evalExpr(first(args), env));
|
var name = trampoline(evalExpr(first(args), env));
|
||||||
var defaultVal = (isSxTruthy((len(args) >= 2)) ? trampoline(evalExpr(nth(args, 1), env)) : NIL);
|
var defaultVal = (isSxTruthy((len(args) >= 2)) ? trampoline(evalExpr(nth(args, 1), env)) : NIL);
|
||||||
var frame = kontFindProvide(kont, name);
|
var val = scopePeek(name);
|
||||||
return (isSxTruthy(frame) ? makeCekValue(get(frame, "value"), env, kont) : (isSxTruthy((len(args) >= 2)) ? makeCekValue(defaultVal, env, kont) : error((String("No provider for: ") + String(name)))));
|
return makeCekValue((isSxTruthy(isNil(val)) ? defaultVal : val), env, kont);
|
||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["step-sf-context"] = stepSfContext;
|
PRIMITIVES["step-sf-context"] = stepSfContext;
|
||||||
|
|
||||||
@@ -1608,16 +1620,16 @@ PRIMITIVES["step-sf-context"] = stepSfContext;
|
|||||||
var stepSfEmit = function(args, env, kont) { return (function() {
|
var stepSfEmit = function(args, env, kont) { return (function() {
|
||||||
var name = trampoline(evalExpr(first(args), env));
|
var name = trampoline(evalExpr(first(args), env));
|
||||||
var val = trampoline(evalExpr(nth(args, 1), env));
|
var val = trampoline(evalExpr(nth(args, 1), env));
|
||||||
var frame = kontFindScopeAcc(kont, name);
|
scopeEmit(name, val);
|
||||||
return (isSxTruthy(frame) ? (append_b(get(frame, "emitted"), val), makeCekValue(NIL, env, kont)) : error((String("No scope for emit!: ") + String(name))));
|
return makeCekValue(NIL, env, kont);
|
||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["step-sf-emit"] = stepSfEmit;
|
PRIMITIVES["step-sf-emit"] = stepSfEmit;
|
||||||
|
|
||||||
// step-sf-emitted
|
// step-sf-emitted
|
||||||
var stepSfEmitted = function(args, env, kont) { return (function() {
|
var stepSfEmitted = function(args, env, kont) { return (function() {
|
||||||
var name = trampoline(evalExpr(first(args), env));
|
var name = trampoline(evalExpr(first(args), env));
|
||||||
var frame = kontFindScopeAcc(kont, name);
|
var val = scopePeek(name);
|
||||||
return (isSxTruthy(frame) ? makeCekValue(get(frame, "emitted"), env, kont) : error((String("No scope for emitted: ") + String(name))));
|
return makeCekValue((isSxTruthy(isNil(val)) ? [] : val), env, kont);
|
||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["step-sf-emitted"] = stepSfEmitted;
|
PRIMITIVES["step-sf-emitted"] = stepSfEmitted;
|
||||||
|
|
||||||
@@ -3573,9 +3585,10 @@ PRIMITIVES["get-verb-info"] = getVerbInfo;
|
|||||||
var targetSel = domGetAttr(el, "sx-target");
|
var targetSel = domGetAttr(el, "sx-target");
|
||||||
return (isSxTruthy(targetSel) ? dictSet(headers, "SX-Target", targetSel) : NIL);
|
return (isSxTruthy(targetSel) ? dictSet(headers, "SX-Target", targetSel) : NIL);
|
||||||
})();
|
})();
|
||||||
if (isSxTruthy(!isSxTruthy(isEmpty(loadedComponents)))) {
|
(function() {
|
||||||
headers["SX-Components"] = join(",", loadedComponents);
|
var compHash = domGetAttr(domQuery("script[data-components][data-hash]"), "data-hash");
|
||||||
}
|
return (isSxTruthy(compHash) ? dictSet(headers, "SX-Components-Hash", compHash) : NIL);
|
||||||
|
})();
|
||||||
if (isSxTruthy(cssHash)) {
|
if (isSxTruthy(cssHash)) {
|
||||||
headers["SX-Css"] = cssHash;
|
headers["SX-Css"] = cssHash;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,8 +521,18 @@ def components_for_request(source: str = "",
|
|||||||
elif extra_names:
|
elif extra_names:
|
||||||
needed = extra_names
|
needed = extra_names
|
||||||
|
|
||||||
loaded_raw = request.headers.get("SX-Components", "")
|
# Check hash first (new): if client hash matches current, skip all defs.
|
||||||
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
# Fall back to legacy name list (SX-Components) for backward compat.
|
||||||
|
comp_hash_header = request.headers.get("SX-Components-Hash", "")
|
||||||
|
if comp_hash_header:
|
||||||
|
from .jinja_bridge import components_for_page
|
||||||
|
_, current_hash = components_for_page("", service=None)
|
||||||
|
if comp_hash_header == current_hash:
|
||||||
|
return "" # client has everything
|
||||||
|
loaded = set() # hash mismatch — send all needed
|
||||||
|
else:
|
||||||
|
loaded_raw = request.headers.get("SX-Components", "")
|
||||||
|
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
for key, val in _COMPONENT_ENV.items():
|
for key, val in _COMPONENT_ENV.items():
|
||||||
|
|||||||
@@ -397,10 +397,11 @@ class OcamlBridge:
|
|||||||
# All directories loaded into the Python env
|
# All directories loaded into the Python env
|
||||||
all_dirs = list(set(_watched_dirs) | _dirs_from_cache)
|
all_dirs = list(set(_watched_dirs) | _dirs_from_cache)
|
||||||
|
|
||||||
# Web adapters (aser lives in adapter-sx.sx) — only load specific files
|
# Web adapters + signals (aser lives in adapter-sx.sx,
|
||||||
|
# signals.sx provides reactive primitives for island SSR)
|
||||||
web_dir = os.path.join(os.path.dirname(__file__), "../../web")
|
web_dir = os.path.join(os.path.dirname(__file__), "../../web")
|
||||||
if os.path.isdir(web_dir):
|
if os.path.isdir(web_dir):
|
||||||
for web_file in ["adapter-sx.sx"]:
|
for web_file in ["signals.sx", "adapter-sx.sx"]:
|
||||||
path = os.path.normpath(os.path.join(web_dir, web_file))
|
path = os.path.normpath(os.path.join(web_dir, web_file))
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
all_files.append(path)
|
all_files.append(path)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
(sx-css :as string?) (sx-css-classes :as string?)
|
(sx-css :as string?) (sx-css-classes :as string?)
|
||||||
(component-hash :as string?) (component-defs :as string?)
|
(component-hash :as string?) (component-defs :as string?)
|
||||||
(pages-sx :as string?) (page-sx :as string?)
|
(pages-sx :as string?) (page-sx :as string?)
|
||||||
|
(body-html :as string?)
|
||||||
(asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
|
(asset-url :as string) (sx-js-hash :as string) (body-js-hash :as string?)
|
||||||
(head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
|
(head-scripts :as list?) (inline-css :as string?) (inline-head-js :as string?)
|
||||||
(init-sx :as string?) (body-scripts :as list?))
|
(init-sx :as string?) (body-scripts :as list?))
|
||||||
@@ -65,6 +66,8 @@ details.group{overflow:hidden}details.group>summary{list-style:none}details.grou
|
|||||||
.sx-error .sx-indicator{display:none}.sx-loading .sx-indicator{display:inline-flex}
|
.sx-error .sx-indicator{display:none}.sx-loading .sx-indicator{display:inline-flex}
|
||||||
.js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}"))))
|
.js-wrap.open .js-pop{display:block}.js-wrap.open .js-backdrop{display:block}"))))
|
||||||
(body :class "bg-stone-50 text-stone-900"
|
(body :class "bg-stone-50 text-stone-900"
|
||||||
|
;; Server-rendered HTML — visible immediately before JS loads
|
||||||
|
(div :id "sx-root" (raw! (or body-html "")))
|
||||||
(script :type "text/sx" :data-components true :data-hash component-hash
|
(script :type "text/sx" :data-components true :data-hash component-hash
|
||||||
(raw! (or component-defs "")))
|
(raw! (or component-defs "")))
|
||||||
(when init-sx
|
(when init-sx
|
||||||
@@ -72,7 +75,7 @@ details.group{overflow:hidden}details.group>summary{list-style:none}details.grou
|
|||||||
(raw! init-sx)))
|
(raw! init-sx)))
|
||||||
(script :type "text/sx-pages"
|
(script :type "text/sx-pages"
|
||||||
(raw! (or pages-sx "")))
|
(raw! (or pages-sx "")))
|
||||||
(script :type "text/sx" :data-mount "body"
|
(script :type "text/sx" :data-mount "#sx-root"
|
||||||
(raw! (or page-sx "")))
|
(raw! (or page-sx "")))
|
||||||
(script :src (str asset-url "/scripts/sx-browser.js?v=" sx-js-hash))
|
(script :src (str asset-url "/scripts/sx-browser.js?v=" sx-js-hash))
|
||||||
;; Body scripts — configurable per app
|
;; Body scripts — configurable per app
|
||||||
|
|||||||
@@ -125,10 +125,14 @@
|
|||||||
(when target-sel
|
(when target-sel
|
||||||
(dict-set! headers "SX-Target" target-sel)))
|
(dict-set! headers "SX-Target" target-sel)))
|
||||||
|
|
||||||
;; Loaded component names
|
;; Send component hash instead of full name list to avoid 431
|
||||||
(when (not (empty? loaded-components))
|
;; (Request Header Fields Too Large) with many loaded components.
|
||||||
(dict-set! headers "SX-Components"
|
;; Server uses hash to decide whether to send component definitions.
|
||||||
(join "," loaded-components)))
|
(let ((comp-hash (dom-get-attr
|
||||||
|
(dom-query "script[data-components][data-hash]")
|
||||||
|
"data-hash")))
|
||||||
|
(when comp-hash
|
||||||
|
(dict-set! headers "SX-Components-Hash" comp-hash)))
|
||||||
|
|
||||||
;; CSS class hash
|
;; CSS class hash
|
||||||
(when css-hash
|
(when css-hash
|
||||||
|
|||||||
Reference in New Issue
Block a user