97 lines
6.6 KiB
XML
97 lines
6.6 KiB
XML
# ADR-001 — Single-Image Deploy for HTMX-SSR Consumers
|
|
# Plain record (the framework has no ADR contract tooling yet); mirrors the
|
|
# constellation _template.ncl shape so it can graduate to the typed schema later.
|
|
|
|
{
|
|
id = "adr-001",
|
|
title = "Single-Image Deploy for HTMX-SSR Consumers — Baked Route Superset, Runtime Per-Site Substance",
|
|
status = 'Accepted,
|
|
date = "2026-06",
|
|
|
|
context = m%"
|
|
`website-htmx-rustelo/code` builds the shared `rustelo-htmx-server` binary that
|
|
multiple sites consume (the personal bilingual site and ontoref/outreach/site).
|
|
Historically the binary WAS one specific site: site/config (routes, menus, theme,
|
|
logo, languages) is consumed at COMPILE TIME via build.rs/build-config codegen, so
|
|
a new consumer had to edit the framework repo and rebuild to get their site. This
|
|
is the framework-vs-one-site tension realised as friction: "we are incapable of
|
|
creating an outreach/site using this framework as a base without modifying and
|
|
recompiling it." Investigation (2026-06-14) showed the question is not which pole
|
|
to pick but where to draw the seam: most per-site SUBSTANCE is already runtime
|
|
(FTL deferred in htmx-ssr; SITE_BASE_URL, SITE_NAME, SUPPORTED_LANGUAGES via env;
|
|
menus/footer loaded from routes.ncl/footer.ncl at startup; brand CSS via custom.css;
|
|
templates via HTMX_TEMPLATE_PATH), while the route TABLE is compiled into the binary
|
|
by the rustelo routing engine for type-safety and performance.
|
|
"%,
|
|
|
|
decision = m%"
|
|
Deploy as a SINGLE shared image, not a per-site build. Bake the SHARED STRUCTURE
|
|
(the route table, as a deliberate framework-level superset of all standard content
|
|
kinds and pages) and supply every per-site SUBSTANCE knob at runtime: brand/logo
|
|
(SITE_LOGO_LIGHT/DARK/ALT), name (SITE_NAME), public origin (SITE_BASE_URL),
|
|
languages (SUPPORTED_LANGUAGES), content, templates, CSS, menus/footer (consumer
|
|
routes.ncl/footer.ncl). A consumer runs the prebuilt binary, points env + supplies
|
|
config/content, and gets their site without a rebuild. Routing stays compiled — it
|
|
is NOT moved to runtime.
|
|
"%,
|
|
|
|
rationale = [
|
|
{ claim = "Per-site build is adoption-hostile",
|
|
detail = "Compiling per site forces every adopter to have the full Rust toolchain plus the stratumiops/foundation path deps and rebuild on each config change — contradicting the voluntary-adoption axiom and ADR-038 (single OCI image). A prebuilt image + consumer config is near-zero adoption cost." },
|
|
{ claim = "Runtime routing would fight the framework's grain",
|
|
detail = "The route table is generated into compiled code by the rustelo engine for type-safety and performance. Making it fully dynamic is a rearchitecture of rustelo core — the forbidden collapse to the all-dynamic pole." },
|
|
{ claim = "Sites differ in content/brand, not route structure",
|
|
detail = "Confirmed with the maintainer: future sites share the route shape (home/about/blog/projects/adr/catalog/...) and differ in content + brand. So one baked superset serves them all; unused routes simply carry no content and hide from the runtime menu." },
|
|
{ claim = "The runtime-substance pattern already exists",
|
|
detail = "FTL is already deferred to runtime in htmx-ssr; menus/footer already load from routes.ncl at startup; origin/name/languages are already env. Only the logo remained baked — now overridable via SITE_LOGO_* — so the model is mechanism-complete for substance." },
|
|
],
|
|
|
|
consequences = {
|
|
positive = [
|
|
"One image serves N sites; adopters need only config + content, no toolchain.",
|
|
"Brand/content changes never require a framework rebuild (only a shared-code bug or a new framework capability does).",
|
|
"Aligns with ADR-038 (OCI single image) and protocol-not-runtime.",
|
|
],
|
|
negative = [
|
|
"The baked route table must be maintained as a deliberate SUPERSET; a consumer menu/link to a route absent from the superset 404s (guarded by validate-route-superset).",
|
|
"Per-site correctness depends on config discipline (the consumer must supply its own brand env + content); the seam must be documented (.claude/CLAUDE.md) and verified (validate-seam).",
|
|
],
|
|
},
|
|
|
|
alternatives_considered = [
|
|
{ option = "Per-site build / image", why_rejected = "Adoption-hostile (toolchain + path deps + rebuild per config change); contradicts voluntary-adoption and ADR-038." },
|
|
{ option = "Fully runtime-dynamic routing (one binary, routes loaded at startup)", why_rejected = "Rearchitects the rustelo routing engine; loses compiled-route type-safety/perf; the forbidden all-dynamic collapse." },
|
|
],
|
|
|
|
constraints = [
|
|
{ id = "routing-stays-baked",
|
|
claim = "The route table is compiled; it is never moved to runtime loading.",
|
|
scope = "crates/server (routing), site/config/routes.ncl",
|
|
severity = 'Hard,
|
|
check = { tag = 'Grep, pattern = "ROUTES_REGISTRY|resource_contributor", path = "crates/server/src/resources.rs" },
|
|
rationale = "Preserves rustelo's compiled-route type-safety and performance; the superset is the unit of structure-sharing." },
|
|
{ id = "per-site-substance-is-runtime",
|
|
claim = "Brand, logo, name, origin, languages, content, templates and menus are supplied at runtime by the consumer, never baked per site.",
|
|
scope = "crates/server/src/theme.rs, shell, consumer run.sh + site/config",
|
|
severity = 'Hard,
|
|
check = { tag = 'NuCmd, cmd = "just validate-seam", expect_exit = 0 },
|
|
rationale = "A shared binary must carry each site's substance via runtime config; validate-seam rejects substance leaking into shared Rust." },
|
|
{ id = "route-table-is-a-superset",
|
|
claim = "site/config/routes.ncl declares ContentIndex + PostViewer for every standard content kind (blog, projects, adr, catalog) so any consumer's links resolve.",
|
|
scope = "site/config/routes.ncl",
|
|
severity = 'Hard,
|
|
check = { tag = 'NuCmd, cmd = "just validate-route-superset", expect_exit = 0 },
|
|
rationale = "A consumer menu/link to a kind absent from the baked superset 404s — the build/runtime split gotcha." },
|
|
],
|
|
|
|
related_adrs = [
|
|
"ontoref adr-038 (OCI distribution — single runnable image)",
|
|
"ontoref adr-029 (tier coexistence — optional accelerator, protocol-not-runtime)",
|
|
],
|
|
|
|
ontology_check = {
|
|
decision_string = "Single shared image: bake the route superset (structure), runtime the per-site substance — neither collapse to one site baked nor to fully-dynamic routing.",
|
|
invariants_at_risk = ["framework-vs-one-site", "formalization-vs-adoption", "consume-not-fork"],
|
|
verdict = 'Safe,
|
|
},
|
|
}
|