The marketing landing becomes a tailorable per-client template — a typed LandingContent model rendered by LandingPage.astro; the Dossier render stays byte-for-byte identical; client instances are values of the same type, canonical in the client's own repo, generated by the generate-landing skill

0037-landing-as-tailorable-template

decision read as Explain confidence verified status active 2026-06-16 owner forward-deployed-engineer
Reversibility
two-way door

DEC-0037 — The landing becomes a tailorable per-client template (typed LandingContent rendered by LandingPage.astro)

Reversibility: two-way door — a behavior-preserving content extraction, revertible by inlining the LandingContent data back into index.astro. The durable parts are the LandingPage(content: LandingContent) seam, the "no theme in content" invariant (#2), and the OkfToken[] hero-artifact contract (#1/#5) that makes byte-identity mechanically testable.

Path update (2026-06-17, audit reconciliation — not a re-decision). Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight (two apps, one origin) moved this whole model from Astro/@dossier/site to SvelteKit/@dossier/app. The decision stands — only the locations + toolchain changed. Map the references below to their current homes: packages/site/src/content/landing/{schema,dossier}.tspackages/app/src/lib/content/landing/{schema,dossier}.ts; examples/rba.tspackages/app/src/lib/content/landing/examples/rba.ts (+ examples/registry.ts); the round-trip test → packages/app/test/landing-dossier-roundtrip.test.ts; LandingPage.astro/HeroLoop.astro/CaptureForm.astro → the .svelte equivalents in packages/app/src/lib/components/; index.astro → the app's landing route; the content.config.ts Zod layer → the plain schema.ts type module; the type gate astro checksvelte-check (pnpm -F @dossier/app check); the preview route packages/site/src/pages/preview/[client].astro (DOCS_ENABLED-gated) → the SvelteKit packages/app/src/routes/preview/[client]/ (dev-only via import.meta.env.DEV + prerender = false). The generate-landing SKILL.md now carries these current paths (single source of truth).

Length/rhythm pass (2026-06-17, second polish round — extends this decision, not a re-decision; continues the content-contract note below). The operator repeatedly judged the landing too long; measured (Playwright, 1280×900) at 5,727px ≈ 6.4 screens / 7 sections. Three changes brought it to 4,727px ≈ 5.3 screens / 6 sections — a full screen removed, nothing informational lost. (1) Global section rhythm tightened in src/styles/landing.css: the .landing-section + .landing-section gap --ds-space-32 → --ds-space-20 (128→80px desktop) and the ≤50rem step --ds-space-24 → --ds-space-16 (seven sections at the old 8rem gap read far longer than the content warranted — tokens only, zero hex). (2) The OKF definition lead de-duplicated (SSOT copy, both instances dossier.ts + examples/rba.ts): the lead had verbatim re-listed all four OKF properties; trimmed to a framing sentence ("This is OKF — … one plain-text atom you own. Not an export of your knowledge — the system of record itself."), so the lead frames and the four properties carry the specifics. (3) The "How it works" loop diagram was merged UP into the hero and the standalone loop section deleted (the operator chose this option explicitly): the labelled 5-stage diagram (Ingest→Extract→OKF graph→Serve→Curate + the "humans curate · agents extend → it compounds" return arc) now renders inside the hero's .hero-loop wrapper (entrance index 3) and replaces the abstract HeroLoop animation; its travelling-flow motion is driven by the hero's existing offscreen-pause IntersectionObserver (repurposed to toggle data-active on the inner .loop__diagram). The deleted section's own lead literally called itself "the labelled view of the cycle running in the hero above" — self-admittedly redundant; the named cycle now leads the page. Contract change (DEC-0037 invariant-relevant): LoopContent in schema.ts SHRANK — head: SectionHead and caption: string were removed (the section that rendered them is gone); it now carries only stages, diagramAriaLabel, returnArcLabel, and both instances dropped their loop.head/loop.caption. Invariant #3 (universal messaging preserved) STILL HOLDS by reconciliation: the old loop caption's "OKF repo is the system of record / indexes are replaceable caches" message did not vanish — it lives in the OKF beat now (the OKF lead carries "system of record itself"; the fourth OKF property is "Indexes are caches" → "derived and disposable… lose nothing"). The landing-rba.test.ts assertion that read loop.caption was repointed to verify that message in its new home (okf.lead + okf.properties) and stays green. Verified (2026-06-17, FDE): pnpm -F @dossier/app check → 467 files, 0 errors / 0 warnings; vitest run landing2 files / 15 tests pass; pnpm -F @dossier/app build Vite compile + prerender of / succeeds, and the prerendered index.html was confirmed to contain loop__diagram exactly once (inside the hero .hero-loop), no landing-section loop, the stage labels + return-arc text present, no loop-title (same known pre-existing win32 EPERM symlink adapter-vercel post-prerender failure — NOT a regression); Playwright light/dark/mobile confirmed the new hero diagram animates (data-active) and is legible. Files (all under packages/app/): src/styles/landing.css (rhythm tokens; .hero .hero-loop sizing; removed dead .loop/.loop__caption rules), src/lib/content/landing/{dossier.ts,examples/rba.ts} (OKF-lead trim; dropped loop.head/loop.caption), src/lib/components/LandingPage.svelte (diagram moved into the hero, loop <section> deleted, hero-loop observer repurposed, HeroLoop import/usage removed), src/lib/content/landing/schema.ts (LoopContent shrank), test/landing-rba.test.ts (system-of-record/indexes-are-caches assertion repointed to the OKF beat). Dead-code follow-up filed: src/lib/components/HeroLoop.svelte is now unused (the merge removed its only import) but was deliberately not deleted — it's a high-craft animated component and four landing.css comments (lines 503, 769, 976, 993) cite it as the canonical motion-convention reference; either delete it (rewording those comment references) or formally park it → tracked by Delete or formally park the now-unused HeroLoop.svelte (its only import was removed when the loop diagram merged into the hero) (backlog, p2, Principal SvelteKit Engineer) so it doesn't silently rot.

Content-contract extension (2026-06-17, polish pass — extends this decision, not a re-decision). Two new universal "beats" were added to the LandingContent contract under this record's invariants, and the hero artifact was relocated. The editorial/product call: the landing previously demonstrated OKF (the real 0007 atom in the hero) and linked Board/Graph from the nav, but never named/defined the format for a first-time visitor and never showed the live derived surfaces — "tell" without "show." The pass closes both gaps. (1) An "OKF, the Open Knowledge Format" explainer beat — eyebrow "THE FORMAT", an accent acronym + expansion, a plain-language definition, and four defining properties (Atomic Markdown + YAML · Typed + sourced + scored · In your own git · Indexes are caches), sourced from Adopt OKF as Dossier's canonical knowledge format + Dossier — The Knowledge Model (v0), no fabricated claims. (2) A "live surfaces" showcase beat — the real /board and /graph read-only, derived-from-git surfaces shown with token+SVG teasers and whole-card links into each ("nothing here is a mockup — both surfaces read your OKF and render it"). (3) Two refinements driven by the user the same session: the real 0007-produces-edge-direction.md hero artifact was moved out of the hero and into the OKF beat (so "define the format" and "show a real one" are one self-contained section; the hero gets leaner, the old "That file in the hero is OKF…" referential coupling is removed), and the OKF beat became a two-column composition ≥64rem (heading spans the top; definition + four properties stack in a narrow left column beside the real file at thumbnail size in a wider right column; collapses to the single-column stack below 64rem). All five DEC-0037 invariants hold by construction: #1 only strings/booleans/token-segments in content; #2 NO theme/color/font/radius field — SurfaceShowcase.motif is a closed 'board'|'graph' selector, not a style field; #3 universal messaging preserved + unweakened; #4/#5 the artifact stays a REAL round-tripped atom (the round-trip test now reads …landing.okf.artifact, was …hero.artifact). Schema shape (all in packages/app/src/lib/content/landing/): new OkfProperty, OkfExplainerContent (which now owns the relocated artifact: HeroArtifact field), SurfaceShowcase, and SurfacesContent (a 2-tuple Board+Graph), wired as okf + surfaces on LandingContent in schema.ts; the okf+surfaces blocks added to dossier.ts (Dossier voice) and examples/rba.ts (re-framed in RBA's voice, universal core unweakened); both rendered by LandingPage.svelte; .okf*/.surface* styling + the @media (min-width:64rem) grid in src/styles/landing.css (tokens only, zero hex). Verified (2026-06-17, FDE): pnpm -F @dossier/app check → 0 errors / 0 warnings; vitest run landing2 files / 15 tests pass (round-trip byte-for-byte + RBA + highlight-class assertions); pnpm -F @dossier/app build prerenders /, /board, /graph and the prerendered index.html carries the new okf__head/okf__lead/okf__artifact markup + the /board+/graph links (the post-prerender Vercel-adapter EPERM…symlink…catchall.func is a known pre-existing Windows environmental failure, reproduced on a stash — NOT a regression, NOT part of prerendering); Playwright light/dark/mobile (≤390px) confirmed both beats and the two-column→single-column collapse with zero horizontal scroll. Skill drift fixed in place: .claude/skills/generate-landing/SKILL.md named the artifact field at hero.artifact; re-pointed to okf.artifact (one table-cell correction — the prior path-migration task Fix the stale round-trip-test path in the generate-landing SKILL.md (points at a file that no longer exists) was already done and predates this relocation, so this was a fix-in-place, not a re-open).

Context

The operator (DXA owner) said the Dossier landing "looks clean and so good" and asked that, when a client's landing is generated, the Dossier page be the TEMPLATE — tailoring the messaging to the client while keeping the underlying theme and core messaging — to accelerate client onboarding: a client grasps the use-cases and value faster when the page speaks in their own vocabulary.

This extends KB-agnostic @dossier/site (renders any tenant's OKF KB) + runtime-driven site rendering + the Node-26 Windows build fix — which made the docs surface KB-agnostic (one DOSSIER_KB env selects any tenant's OKF repo; the Dossier render is the unset default) — from the docs surface to the marketing landing. Same discipline (one renderer, many instances, our own render as the byte-for-byte dogfood), a different surface.

Options considered

1. How a client's landing is produced — hand-fork the page vs. extract a typed content model.

  • (a) Copy index.astro and hand-edit the copy per client. Rejected: it forks the markup, the motion, and the theme per client — every brand-bar fix or motion-polish pass (A motion language for the public landing — make the compounding learning-loop story kinetic while holding the brand bar (and a documented exception for the section-4 loop diagram)) would have to be re-applied N times, and nothing stops a fork from drifting the palette or breaking the loop geometry. The opposite of single-source-of-truth.
  • (b) Extract the content into a typed LandingContent model rendered by one component (chosen). The landing's copy becomes a typed value (packages/site/src/content/landing/schema.ts); a new LandingPage.astro holds all markup, the inline motion hooks (--hero-i/--reveal-i/--flow-i), the loop SVG geometry, and the four <script> blocks, and renders any instance into identical markup. index.astro is now thin — <LandingPage content={dossierLanding} />. Dossier's own copy (content/landing/dossier.ts, transcribed verbatim) is the reference instance / dogfood; a client instance is a value of the same type.

2. Where a client instance lives — in @dossier/site vs. in the client's own repo.

3. How generation happens — a hosted-runtime feature vs. a Claude Code skill.

4. How the hero artifact is modeled — a pre-highlighted HTML string vs. structured OkfToken[] segments.

  • (a) Store the hero code block as a syntax-highlighted raw-HTML string. Rejected: byte-identity of the rendered atom could then only be eyeballed, and a string can smuggle an arbitrary class or hex (breaking invariant #2).
  • (b) Model it as ordered OkfToken[] segments (chosen). Each segment is { text, tok } with tok constrained to OkfTokenKind = 'key' | 'type' | 'comment' | null — the only three highlight classes landing.css defines, plus bare text. body.map(t => t.text).join('') reproduces the raw atom byte-for-byte, so byte-identity is mechanically unit-testable rather than eyeballed, and an instance physically cannot introduce a palette or a stray class.

Decision

Extract the landing's content into a typed LandingContent model rendered by a single LandingPage.astro; keep Dossier's own render byte-for-byte identical (the reference instance / dogfood); treat a client landing as a value of the same type whose canonical home is the client's own repo, generated by the human-curated generate-landing skill — theme and universal messaging never tailorable.

Invariants encoded (the load-bearing part)

  • #1 PIXEL-IDENTICAL Dossier render — proven mechanically (the OkfToken[] round-trip test) and visually (Playwright, light+dark).
  • #2 THEME NEVER TAILORABLE — the type carries no color/font/radius/motion field; the hero artifact's only highlight classes are the three landing.css defines (OkfTokenKind = 'key' | 'type' | 'comment' | null), so a client instance physically cannot introduce a palette or stray class.
  • #3 UNIVERSAL MESSAGING preserved in REQUIRED fields — the sovereignty thesis, the five-stage loop (a fixed 5-tuple, LoopStages, with the OKF node at index 2), provenance/confidence, GraphRAG explainability, "humans curate · agents extend", and the Nadella north-star quote (byte-identical).
  • #4 / #5 the hero artifact is a REAL atom from the instance's OWN repo — shown verbatim/trimmed and round-tripped; client landing content is canonical in the client's repo, the in-package example is a labelled demo.
  • Key sub-decision: model the hero artifact as structured OkfToken[] segments (NOT a pre-highlighted raw-HTML string) precisely so byte-identity of the rendered code is mechanically testable rather than eyeballed.

Rationale

  • It directly accelerates onboarding from the operator's own observation. A client grasps the use-cases and value faster when the landing speaks in their own vocabulary — so the page that already "looks clean and so good" becomes the template, re-framed per client, with theme and core messaging held constant.
  • It is the marketing-surface analogue of a move we already validated. KB-agnostic @dossier/site (renders any tenant's OKF KB) + runtime-driven site rendering + the Node-26 Windows build fix proved that one renderer + many instances + the Dossier render as the byte-for-byte dogfood works for the docs surface; this applies the same discipline to the landing — one LandingPage.astro, many LandingContent values, dossier.ts as the unchanged reference.
  • The invariants are encoded in the TYPE, not enforced by review. Theme un-tailorability (#2) is true by construction — there is no palette field and the OkfTokenKind union has no escape hatch — so a tailored instance cannot drift the brand even if an author tries. The five-stage loop is a fixed-length tuple with OKF pinned at index 2, so the geometry/return-arc assumptions can't be violated. Universal messaging lives in required fields a client re-frames but cannot delete (#3).
  • The OkfToken[] choice makes "pixel-identical" a test, not a promise. Byte-identity of the hero atom is asserted by body.map(t => t.text).join('') === RAW, so #1 is mechanically guarded rather than eyeballed.
  • Sovereignty holds for the marketing surface too. The client's landing content is canonical in the client's own repo; the in-package examples/ value is a labelled demo only — consistent with Adopt OKF as Dossier's canonical knowledge format and Market to every organization; agencies are the highest-leverage channel, not a gate (any org, agency or direct, owns its own copy).
  • verified — but scoped precisely (no fabricated status). Verified this session by the FDE: 15/15 vitest pass across the two landing test files; astro check clean on all six new/changed landing files (the only errors are pre-existing, unrelated /graph in-flight work); and both renders confirmed visually via Playwright against astro dev/ (Dossier, unchanged) and /preview/rba (tailored). The follow-up gap (the skill not yet run as a live end-to-end invocation) is recorded honestly in Review, not implied as done.

Consequences

  • Editing Dossier's landing words now means editing the data module (content/landing/dossier.ts), never the component (LandingPage.astro). Markup, motion, and theme have exactly one home.
  • A new onboarding capability exists — the generate-landing skill — proven end-to-render on the real RBA client instance (/preview/rba).
  • The marketing surface is now provably theme-locked. A tailored client landing cannot introduce a palette, a stray highlight class, or a non-real hero artifact; the five-stage loop and the north-star quote survive every re-frame.
  • Verification (reproduced this session, FDE):
    • packages/site/test/landing-dossier-roundtrip.test.ts — the Dossier hero artifact's token segments concatenate byte-for-byte to the real 0007 atom excerpt; only the three landing.css highlight classes are ever named.
    • packages/site/test/landing-rba.test.ts — 13 invariants incl. the RBA hero round-trip and a test that reads the real RBA atom file. (15/15 vitest pass across both files.)
    • astro check clean on all six new/changed landing files. The only astro check errors are pre-existing and unrelated: the in-flight /graph work (graph-island.ts d3 typings + a graph.astro parse error) — not introduced by this change.
    • Both renders confirmed visually via Playwright against astro dev: screenshots/landing-dossier-dark.png (/, Dossier, unchanged) and clients/rba/site-shots/landing-rba-dark.png (/preview/rba, tailored).
  • Process note (provenance correction — do not propagate). This work was originally drafted by a multi-agent workflow that mis-cited "DEC-0033" (assuming it free); 0033 is already the edge-vocabulary ADR (OKF edge vocabulary is registry-driven — a vertical declares its own traversable edges), so all code citations were corrected to DEC-0037. (A separate parallel in-flight /graph workstream had also begun staging a 0037-knowledge-graph-explorer-surface ADR — only an empty atomic-write temp exists, no committed 0037 file — so the number is free for this record; the latent collision is surfaced to the human, see Review.)
  • Parallel in-flight activity. The working tree carries heavy concurrent work (a /graph route + ADRs 0033–0036). A Graph nav link from that work was removed from the Dossier landing instance to preserve render parity; board.astro retains its Graph link. Adding Graph to the landing is a deliberate future choice, not part of this decision.
  • Two-way door. Revertible by inlining the LandingContent data back into index.astro. The durable commitments are the LandingPage(content: LandingContent) seam, the "no theme in content" invariant (#2), and the OkfToken[] hero-artifact contract (#1/#5).

Review

Promote toward a fuller verified once the generate-landing skill has been run as a live end-to-end invocation (provision → ingest → serve → generate) and produced a client instance mechanically, rather than the RBA instance being authored as the skill's intended output. Specifically:

  • The skill itself is authored and its contract verified against real artifacts, but it has NOT yet been run as a live invocation — the RBA instance was authored as the skill's intended output, not produced by literally invoking the skill.
  • The RBA hero body is a faithful, lightly-trimmed excerpt of the real atom (round-tripped against a hand-authored RAW constant), not a mechanical body-match against the source file. Promote once the skill mechanically transcribes a client's real atom and the round-trip is asserted against the source file directly.
  • Open hygiene routed to the Astro Starlight Engineer: the generate-landing SKILL.md references the round-trip test by its old path RESOLVED 2026-06-17 (Fix the stale round-trip-test path in the generate-landing SKILL.md (points at a file that no longer exists)done): the SKILL.md (and this record, via the Path-update note above) were reconciled to the post-DEC-0043 packages/app/src/lib/content/landing/ + packages/app/test/ locations and the SvelteKit toolchain; no path in the skill resolves to a nonexistent file.