# ADR 0003 — SSR-first, htmx-hard, no JSON **Status:** accepted (2026-06-10). The load-bearing front-end principle for demarkus-library. Deliberately strict: we are pushing the hypermedia approach as far as it goes and treating every escape hatch as a tracked concession. ## Decision The application is **hypermedia-driven (HATEOAS)**. The server renders HTML; htmx is the only general interaction layer; there is no JSON API and no client-side application state. This is not "SSR with some htmx" — it is htmx-hard: the default answer to any interaction is "return server-rendered HTML and swap it in." ### The rules (hard) 1. **The server owns all rendering.** Every response is HTML — a full page or a fragment built by `html/template`. The browser receives finished markup, never data to assemble. goldmark→bluemonday→template is the only render path. 2. **No JSON tier, ever.** Handlers return HTML via `c.Render`/`c.HTML`, never `c.JSON`. No REST/JSON projection, no GraphQL, no serialized view models crossing the wire. (Health check returns plain text; that's the lone non-HTML response and it's not application data.) 3. **No client-side state.** Application state lives on the server (session + the world). The DOM is the only client state, and the server is its source of truth. No stores, no client router, no reactivity layer. 4. **Interactivity is `hx-*` attributes.** Navigation (`hx-boost`), search (`hx-get` debounced), version/panel swaps, pagination, forms (`hx-post` → PUBLISH) — all are htmx returning server-rendered fragments into a target. Reach for an attribute before a line of JS. 5. **Self-hosted assets.** htmx is vendored in-repo and served from the Go binary (`/static/htmx.min.js`, pinned to the latest htmx — currently **2.0.10**) — not a CDN. Single binary, no runtime dependency, no third-party origin, version pinned and reviewable. Keep it current: bump to the latest htmx release as they ship. (Embodied 2026-06-10.) 6. **Fragments are first-class.** Templates are factored so the inner content block can render standalone (for an htmx swap) or inside the page chrome (full load). A fragment request and a full navigation hit the same handler and the same template — no duplicate rendering paths. 7. **Degrade without JS.** Because the server renders every route in full, `hx-boost` navigation and form posts work with JS disabled. htmx is enhancement, never the renderer. Keep it that way: no interaction may be *only* reachable through htmx. ### How far we push it (the experiment) We will attempt the entire app as hypermedia before admitting any island: - **Catalog search (LOOKUP):** debounced `hx-get`, results as an HTML `` to click through. No JSON, no client filtering. - **Editions (VERSIONS) + citations (backlinks):** fragment swaps into a panel. - **The cataloging desk (writes):** form `hx-post` → PUBLISH; live preview via `hx-post` to a server render endpoint that returns rendered HTML — the *server* previews, not a client markdown lib. - **The AI librarian (Phase 4):** streaming via SSE (`hx-ext="sse"`) — a token stream and a live tool-call trace, still server-driven HTML over the wire. ### JS islands — last resort, tracked A real island (custom JS beyond htmx + its official extensions) is allowed ONLY when hypermedia genuinely cannot do the job, and each one is recorded as a concession in this ADR with the reason. Pre-agreed candidates from the plan: - **CodeMirror** — the editor textarea needs real code-editing affordances (Phase 3). - **Canvas graph** — force-directed citation graph (post-v1). Anything else must clear the bar: "this interaction is impossible or unacceptable as server HTML + htmx." Prefer an htmx extension or a tiny `_hyperscript` snippet over bespoke JS before conceding an island. **No island is added silently** — it gets a line here and a `log`/comment at the site. ### Conceded islands - **mermaid** (2026-06-12) — diagram rendering is client-side by nature (the server cannot rasterize mermaid without a headless browser, which is a far larger concession). Vendored `mermaid.min.js` **11.15.0**; loaded lazily by `islands.js` only when a page contains a `language-mermaid` block; degrades to the readable source code block; render failure restores the source block. - **KaTeX** (2026-06-12) — math typesetting; same server-side rasterization argument. Vendored **0.17.0** (js + auto-render + css + woff2 fonts); loaded lazily on delimiter presence (`\(…\)` inline, `$$…$$`/`\[…\]` display — single-`$` deliberately unsupported, dollar-amount false positives); degrades to readable TeX source. `trust: false`, `throwOnError: false`. - Both islands ride one tiny loader (`/static/islands.js`) that re-scans on `htmx:afterSettle` so fragment swaps keep working. Server side, math is shielded from markdown by the goldmark passthrough extension; bluemonday still sanitizes everything (islands render from text content, never raw HTML). ## Consequences - No Node build, no bundler, no SPA framework — fits demarkus's single-Go-binary ethos and makes Phase 5's shareable, server-rendered links free. - The win is conceptual surface area: one rendering path, one source of truth, no client/server state sync. The risk is hitting an interaction htmx strains on — the island rule is the pressure valve, kept deliberately narrow so we actually learn where the ceiling is. - `.coderabbit.yaml` enforces this on the web adapter: flag any `c.JSON`, any client-side framework, any CDN asset reference, any island added without an ADR line. ## Vendored asset versions - **htmx 2.0.10** (2026-06-10) — current latest; initial vendor. (Briefly 2.0.4 the same day before bumping to latest.) - **mermaid 11.15.0** (2026-06-12) — islands concession above. - **KaTeX 0.17.0** (2026-06-12) — islands concession above; woff2 fonts only (modern-browser format; the css's woff/ttf fallbacks 404 by design). ### Conceded islands — Phase 4 (2026-07-03) - **htmx SSE extension** (`sse.min.js` **2.2.4**, vendored + pinned beside htmx) — the pre-agreed streaming-librarian spend named in this ADR's experiment list. Declarative attributes only (`sse-connect`/`sse-swap`/ `sse-close`): the librarian pane's live answers stream trace/token/ rendered/done frames of server-rendered content into hx-swap targets — no hand-written EventSource code. Degrades: the ask form is a real POST that runs the ask server-side and PRG-redirects to the finished answer; SSE is enhancement, never the renderer. - **Pane click-engagement toggle** (2026-07-03, islands.js) — clicking into a pane moves the visual attention marker (an `.engaged` class) without touching the URL focus (which owns layout + the live read; re-focusing collapses panes and reads as navigation). Presentational-only: no fetch, no URL change, no client state, reset by the next server render — the same register as the graph hover-highlight. htmx attributes cannot express click-scoped CSS toggling; without JS the server-truth focus marker stands. Related non-island notes: the reader-overlay Esc handler, palette glue, and overlay summon/dismiss already ride islands.js under ADR 0005/0006 grants; the transcript's bottom-anchored chat scroll is pure CSS (column-reverse), deliberately NOT an island. ## Vendored asset versions (continued) - **htmx-ext-sse 2.2.4** (2026-07-02) — SSE island above.