;; lib/host/blog.sx — Blog domain on the host. Posts are content-on-sx documents ;; whose SOURCE OF TRUTH is the durable SX store (persist op-log on disk): a post ;; is published by appending insert ops to its stream. Serving GET // renders ;; the post to HTML via content/html. The original strangler target (Quart blog ;; post_detail); published posts are world-visible, so this endpoint is ANONYMOUS. ;; ;; READ PATH — materialised view, not per-request IO. The durable backend reads ;; via `perform` (kernel IO suspension), which is serviceable on the main thread ;; (boot) but NOT inside an http-listen request handler thread. So posts are ;; materialised from the store into an in-memory view at boot (and on publish), ;; and request handlers read that view — fast, perform-free. The store stays the ;; source of truth; the view is a cache rebuilt from it on startup. ;; Depends on lib/content/* (+ Smalltalk + persist preloads) + lib/dream/* + ;; lib/host/handler.sx. ;; Register content classes + render methods (idempotent); called at load below. (define host/blog-bootstrap! (fn () (begin (st-bootstrap-classes!) (content/bootstrap!)))) ;; ── store (durable source of truth) + view (in-memory serving cache) ─ (define host/blog-store (persist/open)) (define host/blog-view {}) (define host/blog-use-store! (fn (b) (begin (set! host/blog-store b) (set! host/blog-view {})))) ;; content streams are keyed "content:"; recover the slug. (define host/blog--stream-slug (fn (stream) (if (starts-with? stream "content:") (substr stream 8) nil))) ;; ── publish + lookup ──────────────────────────────────────────────── ;; Publish a simple post (title heading + body paragraph): append its insert ops ;; to the durable store, then refresh the in-memory view. `at` is a logical ts. (define host/blog-publish! (fn (slug title body at) (let ((hid (str slug "-h")) (tid (str slug "-body"))) (content/commit-all! host/blog-store slug (list (op-insert (mk-heading hid 1 title) nil) (op-insert (mk-text tid body) hid)) at) (set! host/blog-view (assoc host/blog-view slug (content/head host/blog-store slug)))))) ;; Materialise every persisted post from the store into the view. Run at boot on ;; the main thread (content/head performs IO, fine here, not in a request). (define host/blog-load-all! (fn () (for-each (fn (stream) (let ((slug (host/blog--stream-slug stream))) (when slug (let ((doc (content/head host/blog-store slug))) (when (> (content/count doc) 0) (set! host/blog-view (assoc host/blog-view slug doc))))))) (persist/backend-streams host/blog-store)))) ;; Idempotent seed: if the slug isn't already materialised, recover it from the ;; store (prior run) or publish it fresh. No duplicate ops on restart. (define host/blog-seed! (fn (slug title body at) (when (nil? (get host/blog-view slug)) (let ((existing (content/head host/blog-store slug))) (if (> (content/count existing) 0) (set! host/blog-view (assoc host/blog-view slug existing)) (host/blog-publish! slug title body at)))))) ;; Lookup is pure in-memory (no perform) — safe inside a request handler. (define host/blog-lookup (fn (slug) (get host/blog-view slug))) ;; ── handler: GET // -> rendered HTML (200) or 404 ───────────── (define host/blog-post (fn (req) (let ((slug (dream-param req "slug"))) (let ((doc (host/blog-lookup slug))) (if doc (dream-html (content/html doc)) (dream-html-status 404 (str "Not found" "

404

No published post: " slug "

"))))))) ;; Anonymous read route. MUST be mounted LAST: the :slug pattern matches any ;; single-segment path, so domain routes (/feed, /health) take precedence. (define host/blog-routes (list (dream-get "/:slug" host/blog-post))) ;; Self-bootstrap at load (content modules are loaded before this one). (host/blog-bootstrap!)