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
- 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/siteto 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}.ts→packages/app/src/lib/content/landing/{schema,dossier}.ts;examples/rba.ts→packages/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.svelteequivalents inpackages/app/src/lib/components/;index.astro→ the app's landing route; thecontent.config.tsZod layer → the plainschema.tstype module; the type gateastro check→svelte-check(pnpm -F @dossier/app check); the preview routepackages/site/src/pages/preview/[client].astro(DOCS_ENABLED-gated) → the SvelteKitpackages/app/src/routes/preview/[client]/(dev-only viaimport.meta.env.DEV+prerender = false). Thegenerate-landingSKILL.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-sectiongap--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 instancesdossier.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-loopwrapper (entrance index 3) and replaces the abstractHeroLoopanimation; its travelling-flow motion is driven by the hero's existing offscreen-pause IntersectionObserver (repurposed to toggledata-activeon 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):LoopContentinschema.tsSHRANK —head: SectionHeadandcaption: stringwere removed (the section that rendered them is gone); it now carries onlystages,diagramAriaLabel,returnArcLabel, and both instances dropped theirloop.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 OKFleadcarries "system of record itself"; the fourth OKF property is "Indexes are caches" → "derived and disposable… lose nothing"). Thelanding-rba.test.tsassertion that readloop.captionwas 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 landing→ 2 files / 15 tests pass;pnpm -F @dossier/app buildVite compile + prerender of/succeeds, and the prerenderedindex.htmlwas confirmed to containloop__diagramexactly once (inside the hero.hero-loop), nolanding-section loop, the stage labels + return-arc text present, noloop-title(same known pre-existing win32EPERM symlinkadapter-vercel post-prerender failure — NOT a regression); Playwright light/dark/mobile confirmed the new hero diagram animates (data-active) and is legible. Files (all underpackages/app/):src/styles/landing.css(rhythm tokens;.hero .hero-loopsizing; removed dead.loop/.loop__captionrules),src/lib/content/landing/{dossier.ts,examples/rba.ts}(OKF-lead trim; droppedloop.head/loop.caption),src/lib/components/LandingPage.svelte(diagram moved into the hero, loop<section>deleted, hero-loop observer repurposed,HeroLoopimport/usage removed),src/lib/content/landing/schema.ts(LoopContentshrank),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.svelteis now unused (the merge removed its only import) but was deliberately not deleted — it's a high-craft animated component and fourlanding.csscomments (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
LandingContentcontract under this record's invariants, and the hero artifact was relocated. The editorial/product call: the landing previously demonstrated OKF (the real0007atom 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/boardand/graphread-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 real0007-produces-edge-direction.mdhero 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.motifis 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 inpackages/app/src/lib/content/landing/): newOkfProperty,OkfExplainerContent(which now owns the relocatedartifact: HeroArtifactfield),SurfaceShowcase, andSurfacesContent(a 2-tuple Board+Graph), wired asokf+surfacesonLandingContentinschema.ts; theokf+surfacesblocks added todossier.ts(Dossier voice) andexamples/rba.ts(re-framed in RBA's voice, universal core unweakened); both rendered byLandingPage.svelte;.okf*/.surface*styling + the@media (min-width:64rem)grid insrc/styles/landing.css(tokens only, zero hex). Verified (2026-06-17, FDE):pnpm -F @dossier/app check→ 0 errors / 0 warnings;vitest run landing→ 2 files / 15 tests pass (round-trip byte-for-byte + RBA + highlight-class assertions);pnpm -F @dossier/app buildprerenders/,/board,/graphand the prerenderedindex.htmlcarries the newokf__head/okf__lead/okf__artifactmarkup + the/board+/graphlinks (the post-prerender Vercel-adapterEPERM…symlink…catchall.funcis 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.mdnamed the artifact field athero.artifact; re-pointed tookf.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 alreadydoneand 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.astroand 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
LandingContentmodel rendered by one component (chosen). The landing's copy becomes a typed value (packages/site/src/content/landing/schema.ts); a newLandingPage.astroholds 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.astrois 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.
- (a) Keep client landing values inside
packages/site. Rejected as a sovereignty violation (Adopt OKF as Dossier's canonical knowledge format): the client's landing content is the client's, and belongs in the client's own git, not Dossier's package. - (b) Canonical home is the client's repo
clients/<id>/landing/landing.config.ts; the in-package copy is a labelled DEMO (chosen). Proven withpackages/site/src/content/landing/examples/rba.ts, header-commented as a demo, rendered via a dev-onlypackages/site/src/pages/preview/[client].astroroute gated byDOCS_ENABLED(zero/preview/*pages emitted in the public production build, mirroring Decouple the agentic board from DOCS_ENABLED — ship /board publicly in production while the /knowledge reading surface stays gated dark's gating discipline).
3. How generation happens — a hosted-runtime feature vs. a Claude Code skill.
- (a) Make landing generation a hosted control-plane feature. Rejected as premature: the hosted control plane is Build a fully-owned hosted control plane (do NOT adopt the Vercel claude-managed-agents starter); settle the system of record as hybrid / thin-control-plane with the client-owned OKF git repo canonical's future build, deliberately untouched here.
- (b) A human-curated Claude Code SKILL (chosen).
.claude/skills/generate-landing/SKILL.mdauthors oneLandingContentvalue from a provisioned client's OKF repo + tenant manifest — "humans curate, agents extend" (Claude-primitives-first build strategy). It writes a data module, never runtime/server/DB code.
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 }withtokconstrained toOkfTokenKind = 'key' | 'type' | 'comment' | null— the only three highlight classeslanding.cssdefines, 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.
- Typed content model.
packages/site/src/content/landing/schema.tsdefinesLandingContent.LandingPage.astro(new) holds all markup, motion hooks, loop SVG geometry, and the four<script>blocks.index.astrois thin:<LandingPage content={dossierLanding} />. Editing Dossier's landing words now means editingcontent/landing/dossier.ts, never the component. - Reference instance = dogfood.
content/landing/dossier.tsis Dossier's current copy transcribed verbatim — the unset-equivalent default, kept pixel-identical. - Client instances are sovereign. A client instance is a value of the same type; canonical home
clients/<id>/landing/landing.config.tsin the client's own repo (Adopt OKF as Dossier's canonical knowledge format).examples/rba.tsis a labelled in-package demo, rendered via the dev-only,DOCS_ENABLED-gatedpreview/[client].astroroute. - Generation is a skill, not a runtime.
.claude/skills/generate-landing/SKILL.md— human-curated, writes a data module only; the hosted control plane (Build a fully-owned hosted control plane (do NOT adopt the Vercel claude-managed-agents starter); settle the system of record as hybrid / thin-control-plane with the client-owned OKF git repo canonical) stays a future build, untouched.
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.cssdefines (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, manyLandingContentvalues,dossier.tsas 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
OkfTokenKindunion 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 bybody.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 checkclean on all six new/changed landing files (the only errors are pre-existing, unrelated/graphin-flight work); and both renders confirmed visually via Playwright againstastro 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-landingskill — 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 real0007atom excerpt; only the threelanding.csshighlight 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 checkclean on all six new/changed landing files. The onlyastro checkerrors are pre-existing and unrelated: the in-flight/graphwork (graph-island.tsd3 typings + agraph.astroparse error) — not introduced by this change.- Both renders confirmed visually via Playwright against
astro dev:screenshots/landing-dossier-dark.png(/, Dossier, unchanged) andclients/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);
0033is 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/graphworkstream had also begun staging a0037-knowledge-graph-explorer-surfaceADR — only an empty atomic-write temp exists, no committed0037file — 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
/graphroute + ADRs 0033–0036). AGraphnav link from that work was removed from the Dossier landing instance to preserve render parity;board.astroretains 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
LandingContentdata back intoindex.astro. The durable commitments are theLandingPage(content: LandingContent)seam, the "no theme in content" invariant (#2), and theOkfToken[]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
RAWconstant), 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: theRESOLVED 2026-06-17 (Fix the stale round-trip-test path in the generate-landing SKILL.md (points at a file that no longer exists) →generate-landingSKILL.md references the round-trip test by its old pathdone): the SKILL.md (and this record, via the Path-update note above) were reconciled to the post-DEC-0043packages/app/src/lib/content/landing/+packages/app/test/locations and the SvelteKit toolchain; no path in the skill resolves to a nonexistent file.