+ {# a Hiccup-rendered component, passed in pre-rendered and emitted verbatim #}
+ {{ hiccup_frag|safe }}
+
+
diff --git a/src/clj/auto_ap/ssr/selmer.clj b/src/clj/auto_ap/ssr/selmer.clj
new file mode 100644
index 00000000..b96315e8
--- /dev/null
+++ b/src/clj/auto_ap/ssr/selmer.clj
@@ -0,0 +1,43 @@
+(ns auto-ap.ssr.selmer
+ "Selmer rendering + the Hiccup<->Selmer interop bridge for the SSR form/wizard
+ migration (see .claude/skills/ssr-form-migration). Interactive, attribute-heavy
+ components render from Selmer templates with plain-HTML Alpine/HTMX attributes;
+ the bridge lets a Selmer template embed Hiccup output and lets a Selmer fragment
+ sit inside a Hiccup tree during the strangler transition.
+
+ Templates live under resources/templates/ and are referenced by classpath-relative
+ path, e.g. (render \"templates/components/typeahead.html\" ctx)."
+ (:require
+ [hiccup.util :as hu]
+ [hiccup2.core :as h2]
+ [selmer.parser :as selmer]))
+
+(defn hiccup->html
+ "Render a Hiccup form to an HTML string so it can be embedded in a Selmer
+ context value and emitted with the |safe filter: {{ frag|safe }}."
+ [hiccup]
+ (str (h2/html {} hiccup)))
+
+(defn raw
+ "Wrap an already-rendered HTML string (e.g. from `render`) so hiccup2 emits it
+ verbatim instead of escaping it. Use to drop a Selmer fragment into a Hiccup tree:
+ [:div (sel/raw (sel/render \"...\" ctx))]."
+ [^String html]
+ (hu/raw-string html))
+
+(defn render
+ "Render a Selmer template file (classpath-relative path) with `ctx`, returning an
+ HTML string. Hiccup values in `ctx` should be pre-rendered via `hiccup->html` and
+ referenced with |safe in the template."
+ [template ctx]
+ (selmer/render-file template ctx))
+
+(defn render-str
+ "Render a Selmer template given as a string (handy for tests/REPL)."
+ [template ctx]
+ (selmer/render template ctx))
+
+(defn render->hiccup
+ "Render a Selmer template file and wrap the result for safe embedding in Hiccup."
+ [template ctx]
+ (raw (render template ctx)))
diff --git a/test/clj/auto_ap/ssr/selmer_test.clj b/test/clj/auto_ap/ssr/selmer_test.clj
new file mode 100644
index 00000000..1f0790d8
--- /dev/null
+++ b/test/clj/auto_ap/ssr/selmer_test.clj
@@ -0,0 +1,36 @@
+(ns auto-ap.ssr.selmer-test
+ (:require
+ [auto-ap.ssr.selmer :as sut]
+ [clojure.string :as str]
+ [clojure.test :refer [deftest is testing]]
+ [hiccup2.core :as h2]))
+
+(deftest hiccup->html
+ (testing "renders a Hiccup form to an HTML string"
+ (is (= "A & B"
+ (sut/hiccup->html [:span.label "A & B"])))))
+
+(deftest selmer-embeds-hiccup
+ (testing "a Hiccup component renders inside a Selmer template via |safe"
+ (let [frag (sut/hiccup->html [:span.badge "from hiccup"])
+ out (sut/render-str "
{{frag|safe}}
" {:frag frag})]
+ (is (str/includes? out "from hiccup"))
+ ;; without |safe the markup would be escaped; |safe keeps it verbatim
+ (is (not (str/includes? out "<span"))))))
+
+(deftest selmer-fragment-inside-hiccup
+ (testing "a Selmer fragment renders inside a Hiccup tree without double-escaping"
+ (let [sel (sut/render-str "{{label}}" {:url "/x" :label "Go"})
+ out (str (h2/html {} [:div (sut/raw sel)]))]
+ (is (= "