host: view + edit the SX source of each blog post (blog 47/47, 213 total)
Posts ARE SX source, so expose it: a public raw-source view and a guarded in-browser source editor. - GET /<slug>/source — raw sx_content as text/plain (public; a published post's source isn't secret). - GET /<slug>/edit — edit form pre-filled with the post's title, raw source (in a textarea, render-to-html-escaped so it shows verbatim), and status (current value pre-selected). Guarded (editor only). Slug is preserved. - POST /<slug>/edit — save the edited source; same write-time validation as create (unparseable body -> 400, post left intact); 303 back to the post. - post page gains "view source · edit · all posts" footer links. Routing: /:slug/source + /:slug/edit are two-segment patterns; the router consumes :param as exactly one segment and requires a full match, so /:slug does not shadow them (asserted). 14 new blog tests cover view (200/text-plain/ raw body/404/no-shadow) and edit (401 unauth GET+POST, 200 form, source shown, 303 save, persisted, slug preserved, 400 malformed, 404 missing). Verified live on blog.rose-ash.com: view source, guarded edit form, save round-trip (rendered post + source both reflect the edit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -107,7 +107,15 @@
|
||||
(if r
|
||||
(dream-html
|
||||
(host/blog--page (get r :title)
|
||||
(quasiquote (article (raw! (unquote (host/blog-render r)))))))
|
||||
(quasiquote
|
||||
(div
|
||||
(article (raw! (unquote (host/blog-render r))))
|
||||
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
|
||||
(a :href (unquote (str "/" slug "/source")) "view source")
|
||||
" · "
|
||||
(a :href (unquote (str "/" slug "/edit")) "edit")
|
||||
" · "
|
||||
(a :href "/" "all posts"))))))
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(quasiquote
|
||||
@@ -136,6 +144,20 @@
|
||||
|
||||
(define host/blog-index (fn (req) (host/ok (host/blog-list))))
|
||||
|
||||
;; GET /<slug>/source — the raw sx_content as text/plain. Posts ARE SX source, so
|
||||
;; this just hands back the stored markup (public; a published post's source is
|
||||
;; not secret). 404 if the post is absent.
|
||||
(define host/blog-source
|
||||
(fn (req)
|
||||
(let ((slug (dream-param req "slug")))
|
||||
(let ((r (host/blog-get slug)))
|
||||
(if r
|
||||
(dream-response 200 {:content-type "text/plain; charset=utf-8"}
|
||||
(or (get r :sx-content) ""))
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug))))))))))))
|
||||
|
||||
;; ── create page (GET /new) — clean minimal form as an SX tree ───────
|
||||
;; No legacy JS editor, no external assets, no shims. The rich WYSIWYG is a
|
||||
;; future native SX-island editor (Phase 5.2+). Posts to /new.
|
||||
@@ -243,6 +265,66 @@
|
||||
(begin (host/blog-delete! slug) (host/ok {:slug slug :deleted true}))
|
||||
(host/error 404 "no such post")))))
|
||||
|
||||
;; GET /<slug>/edit — edit form pre-filled with the post's current title, raw
|
||||
;; sx_content (in a textarea — render-to-html escapes the text child, so the
|
||||
;; browser shows the source verbatim), and status (current value pre-selected).
|
||||
;; Guarded: only an editor reaches the editor. Keeps the slug (edits don't re-slug).
|
||||
(define host/blog-edit-form
|
||||
(fn (req)
|
||||
(let ((slug (dream-param req "slug")))
|
||||
(let ((r (host/blog-get slug)))
|
||||
(if (nil? r)
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||
(let ((status (get r :status)))
|
||||
(let ((mk-opt
|
||||
(fn (val label)
|
||||
(if (= val status)
|
||||
(quasiquote (option :value (unquote val) :selected "selected" (unquote label)))
|
||||
(quasiquote (option :value (unquote val) (unquote label)))))))
|
||||
(dream-html
|
||||
(host/blog--page (str "Edit: " (get r :title))
|
||||
(quasiquote
|
||||
(div
|
||||
(h1 (unquote (str "Edit: " (get r :title))))
|
||||
(form :method "post" :action (unquote (str "/" slug "/edit"))
|
||||
(p (input :name "title" :value (unquote (get r :title))
|
||||
:style "font-size:1.4em;width:100%"))
|
||||
(p (textarea :name "sx_content" :rows "16"
|
||||
:style "width:100%;font-family:monospace"
|
||||
(unquote (or (get r :sx-content) ""))))
|
||||
(p (select :name "status"
|
||||
(unquote (mk-opt "draft" "Draft"))
|
||||
(unquote (mk-opt "published" "Published")))
|
||||
" "
|
||||
(button :type "submit" "Save")))
|
||||
(p (a :href (unquote (str "/" slug "/")) "view post")
|
||||
" · "
|
||||
(a :href (unquote (str "/" slug "/source")) "view source")))))))))))))
|
||||
|
||||
;; POST /<slug>/edit — save the edited source. Same write-time validation as the
|
||||
;; create paths (unparseable body -> 400, post left intact). Slug is preserved.
|
||||
(define host/blog-edit-submit
|
||||
(fn (req)
|
||||
(let ((slug (dream-param req "slug")))
|
||||
(let ((r (host/blog-get slug)))
|
||||
(if (nil? r)
|
||||
(dream-html-status 404
|
||||
(host/blog--page "Not found"
|
||||
(quasiquote (div (h1 "404") (p (unquote (str "No post: " slug)))))))
|
||||
(let ((title (or (dream-form-field req "title") (get r :title)))
|
||||
(sx-content (or (dream-form-field req "sx_content") ""))
|
||||
(status (or (dream-form-field req "status") (get r :status))))
|
||||
(if (host/blog-content-ok? sx-content)
|
||||
(begin
|
||||
(host/blog-put! slug title sx-content status)
|
||||
(dream-redirect (str "/" slug "/")))
|
||||
(dream-html-status 400
|
||||
(host/blog--page "Error"
|
||||
(quasiquote (div (h1 "Error") (p "Post body is not valid SX markup.")
|
||||
(p (a :href (unquote (str "/" slug "/edit")) "Back")))))))))))))
|
||||
|
||||
;; ── routes ──────────────────────────────────────────────────────────
|
||||
;; Public reads + the create form. /, /posts, /new BEFORE /:slug (catch-all).
|
||||
;; MUST be mounted LAST in the app so domain routes (/feed, /health) win.
|
||||
@@ -251,6 +333,7 @@
|
||||
(dream-get "/" host/blog-home)
|
||||
(dream-get "/posts" host/blog-index)
|
||||
(dream-get "/new" host/blog-new-form)
|
||||
(dream-get "/:slug/source" host/blog-source)
|
||||
(dream-get "/:slug" host/blog-post)))
|
||||
|
||||
;; Guarded writes: form ingest + JSON create/update/delete behind auth+ACL.
|
||||
@@ -267,6 +350,8 @@
|
||||
(fn (resolve)
|
||||
(list
|
||||
(dream-post "/new" (host/blog--protect resolve host/blog-form-submit))
|
||||
(dream-get "/:slug/edit" (host/blog--protect resolve host/blog-edit-form))
|
||||
(dream-post "/:slug/edit" (host/blog--protect resolve host/blog-edit-submit))
|
||||
(dream-post "/posts" (host/blog--protect resolve host/blog-create))
|
||||
(dream-put "/posts/:slug" (host/blog--protect resolve host/blog-update-handler))
|
||||
(dream-delete "/posts/:slug" (host/blog--protect resolve host/blog-delete-handler)))))
|
||||
|
||||
Reference in New Issue
Block a user