From 833415b17050511e7a4aacdf9cff9dcdc6e0c254 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 23:20:50 +0000 Subject: [PATCH] Error pages render within layout instead of bare nil/404 Route errors and missing pages now show a styled error message inside the normal layout (header, nav still work) instead of bare "nil" text or a raw "Not Found" page. AJAX errors return renderable SX error fragments instead of "nil" strings. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 44 +++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 7cbec7d4..f65d0e15 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -1544,14 +1544,32 @@ let http_render_page env path headers = Printf.eprintf "[http] route error for %s: %s\n%!" path (Printexc.to_string e); Nil in - match route_result with - | Nil -> None - | Dict d -> - let is_ajax = match Hashtbl.find_opt d "is-ajax" with Some (Bool true) -> true | _ -> false in - let nav_path = match Hashtbl.find_opt d "nav-path" with Some (String s) -> s | _ -> path in - let page_ast = match Hashtbl.find_opt d "page-ast" with Some v -> v | _ -> Nil in - if page_ast = Nil then None - else begin + (* Build an error page AST that keeps the layout intact *) + let error_page_ast msg = + List [Symbol "div"; Keyword "class"; String "p-8 max-w-2xl mx-auto"; + List [Symbol "h2"; Keyword "class"; String "text-xl font-semibold text-rose-600 mb-4"; + String "Page Error"]; + List [Symbol "p"; Keyword "class"; String "text-stone-600 mb-2"; String path]; + List [Symbol "pre"; Keyword "class"; String "text-sm bg-stone-100 p-4 rounded overflow-x-auto text-stone-700"; + String msg]] + in + (* Normalize route result — Nil and non-Dict become error pages *) + let is_ajax_req = List.exists (fun (k,_) -> String.lowercase_ascii k = "sx-request") headers in + let route_dict = match route_result with + | Dict d -> d + | _ -> + let d = Hashtbl.create 4 in + Hashtbl.replace d "is-ajax" (Bool is_ajax_req); + Hashtbl.replace d "nav-path" (String path); + Hashtbl.replace d "page-ast" (error_page_ast "Page not found"); + d + in + let d = route_dict in + let is_ajax = match Hashtbl.find_opt d "is-ajax" with Some (Bool true) -> true | _ -> false in + let nav_path = match Hashtbl.find_opt d "nav-path" with Some (String s) -> s | _ -> path in + let page_ast = match Hashtbl.find_opt d "page-ast" with Some v -> v | _ -> Nil in + let page_ast = if page_ast = Nil then error_page_ast "Page returned empty content" else page_ast in + begin let wrapped = List [Symbol "~layouts/doc"; Keyword "path"; String nav_path; page_ast] in if is_ajax then begin (* AJAX: return SX wire format (aser output) with text/sx content type *) @@ -1618,9 +1636,6 @@ let http_render_page env path headers = Some html end end - | _ -> - Printf.eprintf "[http] unexpected handler result for %s\n%!" path; - None (* ====================================================================== *) (* Static file serving + file hashing *) @@ -2409,10 +2424,13 @@ let http_mode port = | Some body -> let resp = http_response ~content_type:"text/sx; charset=utf-8" body in Hashtbl.replace response_cache cache_key resp; resp - | None -> http_response ~status:404 "nil" + | None -> http_response ~status:404 + "(div :class \"p-8\" (h2 :class \"text-rose-600 font-semibold\" \"Page not found\") (p :class \"text-stone-500\" \"No route matched this path\"))" with e -> Printf.eprintf "[ajax] Error for %s: %s\n%!" path (Printexc.to_string e); - http_response ~status:500 "nil" + http_response ~status:500 + (Printf.sprintf "(div :class \"p-8\" (h2 :class \"text-rose-600 font-semibold\" \"Render Error\") (pre :class \"text-sm bg-stone-100 p-4 rounded\" \"%s\"))" + (escape_sx_string (Printexc.to_string e))) in write_response fd response; true end else begin