host: article declares :body composition + decompose real posts (figure/iframe→cards)

Prep for importing a real blog post into the :body composition:
- article now DECLARES {:name "body" :type "Composition"} (layer 2 — the type defines that an
  article's body is a composition). The edit FORM + submit read scalar-fields only, so the
  Composition field never gets a stray text input (or gets nil'd on save).
- decompose handles real-post block kinds: <figure> → card-image WITH its <figcaption> as the
  caption (host/blog--find-child digs out the inner <img>); <iframe>/<embed>/<video> →
  card-embed with src as :url. card-embed's template now renders an actual <iframe> (videos
  play) instead of the url as text.

blog 175/175, full host conformance 404/404 (+ test: figure→card-image(caption) & iframe→
card-embed via import). Next: wipe content (reseed types+demos), import nt-live-encore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 11:35:29 +00:00
parent 616c3cf966
commit a8c095b1b3
2 changed files with 42 additions and 11 deletions

View File

@@ -612,7 +612,7 @@
;; -- Slice 8: typed scalar fields on a type --
(host-bl-test "fields-of reads a type's declared fields (seeded on article)"
(map (fn (f) (get f :name)) (host/blog-fields-of "article"))
(list "subtitle" "hero"))
(list "subtitle" "hero" "body"))
(host-bl-test "widget-for: explicit > value-type default > text fallback"
(list (host/blog--widget-for {:name "a" :type "URL"})
(host/blog--widget-for {:name "b" :type "Text"})
@@ -622,9 +622,9 @@
(host-bl-test "set-fields! is idempotent + preserves the rest of the record"
(begin
(host/blog--set-fields! "article"
(list {:name "subtitle" :type "String"} {:name "hero" :type "URL"}))
(list {:name "subtitle" :type "String"} {:name "hero" :type "URL"} {:name "body" :type "Composition"}))
(list (get (host/blog-get "article") :title) (len (host/blog-fields-of "article"))))
(list "Article" 2))
(list "Article" 3))
(host-bl-test "a type with no declared fields -> empty list"
(host/blog-fields-of "tag") (list))
(host-bl-test "/meta shows the article's typed fields"
@@ -636,7 +636,7 @@
(host/blog-put! "fpost" "F Post" "(article (h1 \"F\"))" "published")
(host/blog-relate! "fpost" "article" "is-a")
(map (fn (f) (get f :name)) (host/blog--fields-for-post "fpost")))
(list "subtitle" "hero"))
(list "subtitle" "hero" "body"))
(host-bl-test "a post of no typed type has no fields"
(host/blog--fields-for-post "hello") (list))
(host-bl-test "set/get field-values round-trips on an instance"
@@ -823,6 +823,18 @@
(let ((html (dream-resp-body (host-bl-app (host-bl-req "/imp-x/")))))
(list (contains? html "Heading One") (contains? html "Para text.") (contains? html "p.jpg")))
(list true true true))
;; real-post block kinds: a <figure> -> card-image WITH its figcaption; an <iframe> ->
;; card-embed with its src as the url. (The nt-live-encore import shape.)
(host-bl-test "decompose maps figure->card-image (with caption) and iframe->card-embed"
(begin
(host/blog-import-post! {"slug" "imp-fig" "title" "Fig" "status" "published"
"sx_content" "(article (figure (img :src \"p.jpg\" :alt \"a\") (figcaption \"the cap\")) (iframe :src \"https://youtube.com/embed/xyz\"))"})
(list (host/blog-is-a? "imp-fig__body__b0" "card-image")
(get (host/blog-field-values-of "imp-fig__body__b0") "caption")
(get (host/blog-field-values-of "imp-fig__body__b0") "src")
(host/blog-is-a? "imp-fig__body__b1" "card-embed")
(get (host/blog-field-values-of "imp-fig__body__b1") "url")))
(list true "the cap" "p.jpg" true "https://youtube.com/embed/xyz"))
;; -- block editor: structural edits to the post :body composition (step 6). --
(host-bl-test "block-add! creates a card object + contains edge + appends a ref to the body"
(begin