Commitde9fe292Recorded5 Jul 2026Repositorysigil-web

Add ui-routes: a routes analog that defaults handler responses

Message

Gather hypermedia routes under ui-routes and an action handler can just end with its live-collection mutation: a handler that returns a value which isn't an http-response is taken as "handled, nothing extra for the actor" and becomes an empty (ui-response). Removes the easy-to-forget trailing (ui-response) from mutate-and-broadcast handlers.

Structural scoping (David's call over a path-prefix middleware): ui-routes is a plain routes-analog handler, so it composes inside routes just like router -- put UI routes under ui-routes and a JSON API / webhook under plain routes/router, which never coerce. #f still falls through (a missing-id mutation returning #f becomes a 404 via with-not-found) and explicit responses pass through untouched.

Lives in (sigil web ui) since it injects ui-response; imports (sigil web routes) and reuses routes (no cycle -- routes does not import ui).

Changed
 src/sigil/web/ui.sgl | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)
Diff
src/sigil/web/ui.sglmodified
@@ -30,6 +30,7 @@
30
(sigil sxml)
31
(sigil http request)
32
(sigil http response)
+33
(sigil web routes)
34
(sigil resources))
35
36
(export
@@ -44,6 +45,7 @@
45
ui-js
46
ui-flash
47
ui-response
+48
ui-routes
49
50
;; Deprecated aliases (kept through 1.0)
51
sse-morph
@@ -452,6 +454,37 @@
454
cache-control: "no-cache")
455
body: (string-join updates "")))
456
+457
;;; Combine hypermedia route handlers, defaulting their responses.
+458
;;;
+459
;;; Like `routes` (first non-#f handler wins), but a handler that returns a
+460
;;; value which isn't an HTTP response is taken to mean "handled, nothing
+461
;;; extra for the acting user" and becomes an empty `(ui-response)`. So an
+462
;;; action handler can just end with its live-collection mutation:
+463
;;;
+464
;;; ```scheme
+465
;;; (define (toggle-handler req)
+466
;;; (live-update! todos (item-id req)
+467
;;; (lambda (i) (dict-set i done?: (not (dict-ref i done?:))))))
+468
;;; ```
+469
;;;
+470
;;; Gather your hypermedia routes under `ui-routes` and keep other kinds
+471
;;; (a JSON API, a webhook) under plain `routes`/`router`, which never
+472
;;; coerce. `#f` still falls through, and explicit responses pass through
+473
;;; untouched, so `ui-routes` composes inside `routes` just like `router`:
+474
;;;
+475
;;; ```scheme
+476
;;; (routes
+477
;;; (ui-routes (router …ui…) (live-routes todos)) ; forgiving
+478
;;; (router …api…)) ; untouched
+479
;;; ```
+480
(define (ui-routes . handlers)
+481
(let ((combined (apply routes handlers)))
+482
(lambda (request)
+483
(let ((result (combined request)))
+484
(if (or (not result) (http-response? result))
+485
result
+486
(ui-response))))))
+487
488
489
;; ============================================================
490
;; Full-Page Response Helpers