diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml
index e0374d92..3f2ec014 100644
--- a/hosts/ocaml/bin/sx_server.ml
+++ b/hosts/ocaml/bin/sx_server.ml
@@ -3040,8 +3040,10 @@ let http_mode port =
in
write_response fd response; true
end else if is_sx then begin
+ let has_state_cookie = Hashtbl.mem _request_cookies "sx-home-stepper" in
let cache_key = if is_ajax then "ajax:" ^ path else path in
- match Hashtbl.find_opt response_cache cache_key with
+ match (if has_state_cookie then None
+ else Hashtbl.find_opt response_cache cache_key) with
| Some cached -> write_response fd cached; true
| None ->
if is_ajax then begin
@@ -3062,6 +3064,18 @@ let http_mode port =
(escape_sx_string (Printexc.to_string e)))
in
write_response fd response; true
+ end else if has_state_cookie then begin
+ (* State cookie present — render on main thread so get-cookie works.
+ Don't cache: response varies by cookie value. *)
+ let response =
+ try match http_render_page env path [] with
+ | Some body -> http_response body
+ | None -> http_response ~status:404 "
Not Found
"
+ with e ->
+ Printf.eprintf "[render] Cookie render error for %s: %s\n%!" path (Printexc.to_string e);
+ http_response ~status:500 "Error
"
+ in
+ write_response fd response; true
end else begin
(* Full page: queue to render worker *)
Mutex.lock render_mutex;
diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx
index 5350b287..d24000e8 100644
--- a/sx/sx/home-stepper.sx
+++ b/sx/sx/home-stepper.sx
@@ -18,7 +18,15 @@
((val (when (and (string? raw) (contains? raw prefix)) (let ((start (+ (index-of raw prefix) (len prefix)))) (let ((rest (slice raw start)) (end-pos (index-of (slice raw start) ";"))) (if (> end-pos -1) (slice rest 0 end-pos) rest))))))
(let ((n (if val (parse-number val) 0))) {:step-idx (signal (if (and (number? n) (>= n 0) (<= n 16)) n 0))})))))
nil))
- (step-idx (if store (get store "step-idx") (signal 0)))
+ (step-idx
+ (if
+ store
+ (get store "step-idx")
+ (let
+ ((cv (get-cookie "sx-home-stepper")))
+ (let
+ ((n (if cv (parse-number cv) 0)))
+ (signal (if (and (number? n) (>= n 0) (<= n 16)) n 0))))))
(dom-stack-sig (signal (list)))
(code-tokens (signal (list))))
(letrec
diff --git a/tests/playwright/site-full.spec.js b/tests/playwright/site-full.spec.js
index 854fec20..9af82e26 100644
--- a/tests/playwright/site-full.spec.js
+++ b/tests/playwright/site-full.spec.js
@@ -159,6 +159,12 @@ test('home', async ({ page }) => {
const errors = trackErrors(page);
const entries = [];
+ // Set cookie to step 7, then load page — SSR should render at 7
+ await page.context().addCookies([{
+ name: 'sx-home-stepper', value: '7',
+ url: server.baseUrl,
+ }]);
+
// Capture SSR state before JS runs — detect hydration flash
const ssrResponse = await page.goto(server.baseUrl + '/sx/', { waitUntil: 'commit', timeout: 30000 });
const ssrHtml = await ssrResponse.text();
@@ -174,7 +180,7 @@ test('home', async ({ page }) => {
return m ? m[1] : null;
});
const noFlash = ssrIndex === hydratedIndex;
- entries.push({ ok: noFlash, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex}`, feature: 'no-flash' });
+ entries.push({ ok: noFlash, label: `No flash: SSR=${ssrIndex} hydrated=${hydratedIndex} (cookie=7)`, feature: 'no-flash' });
const info = await discoverPage(page);
entries.push({ ok: true, label: 'Boot: data-sx-ready', feature: 'boot' });