# CMS Public Landing — a true public face for DeepDrftManager — Design Spec

Status: **proposed** (2026-06-17). Author: product-designer. Date: 2026-06-17.
Implementer: staff-engineer. Reference pattern: Skipper `Home.razor` / `MainHomeLayout.razor`.

## 0. Goal

Give `DeepDrftManager` (the CMS) a public face: an unauthenticated splash at `/` with DeepDrft
branding and a single clear **Login** call-to-action. Authentication happens beyond it. Today an
anonymous hit to `/` falls straight through to the login form (the catalogue page at `/` carries
`[Authorize]`, so `AuthorizeRouteView` redirects). After this change, an anonymous visitor sees a
composed, on-brand splash; an authenticated admin is sent onward to the catalogue without ever seeing
the splash.

This is **additive**. The authenticated admin experience (catalogue dashboard, `CmsLayout`, all CMS
pages) is preserved unchanged except for the catalogue's route moving off `/`.

---

## 1. What exists today (confirmed from live source 2026-06-17)

- **`Components/Pages/Index.razor`** — `@page "/"`, `@attribute [Authorize]`, `@layout
  Layout.CmsLayout`. This **is** the authenticated catalogue dashboard (Tracks / Releases / Genres
  summary cards, each loading a count concurrently). No public face exists.
- **`Components/Routes.razor`** — `AuthorizeRouteView` with `DefaultLayout = Layout.CmsLayout`.
  `NotAuthorized`: authenticated-but-unauthorized → `RedirectToAccessDenied`; unauthenticated →
  `RedirectToLogin` (the AuthBlocksWeb component, not a Manager-local one). `AdditionalAssemblies`
  already includes `typeof(AuthBlocksWeb._Imports).Assembly`.
- **`Components/Layout/CmsLayout.razor`** — `MudThemeProvider IsDarkMode="false" Theme="DeepDrftPalettes.Cms"`
  (light-only, solid navy AppBar). Dense AppBar "Deep Drft — Admin" + a "Back to site" home button
  (`Href="/"`). Providers (Popover/Dialog/Snackbar) live here.
- **`Program.cs`** — `MapRazorComponents<App>().AddInteractiveServerRenderMode()...AllowAnonymous()`.
  Endpoint auth is anonymous by design (JWT lives in browser localStorage, never reaches the server on
  a nav); **page authorization is owned entirely by `AuthorizeRouteView`**. Consequence: a routable
  page with **no `[Authorize]` attribute renders for everyone**, no server 401, no redirect. This is
  the seam the splash uses.
- **Auth components available** (from `Cerebellum.AuthBlocks.Web`, assembly already wired into the
  router): `[HierarchicalRoleAuthorize]` attribute and `<HierarchicalRoleAuthorizeView>` view
  component. Bare = "requires authenticated"; with roles = walks the role hierarchy.
- **No `wwwroot/img/` directory exists** in `DeepDrftManager` today. The host's only wwwroot asset is
  `app.css`. The hero asset and its folder are net-new.
- **Theme** — `DeepDrftPalettes.Cms` (in `DeepDrftShared.Client.Common`): navy `#0D1B2A` ground /
  green-accent `#3D7A68` / warm off-white `#FAFAF8`; typography Cormorant Garamond (display) / Geist
  Mono (`Subtitle1`/`Button`/`Caption`) / DM Sans (body). This is the DeepDrft identity — **do not
  import Skipper's nautical styling.** Borrow Skipper's *structure*, not its look.

### What we borrow from Skipper (pattern, not pixels)

Skipper splits a public **`MainHomeLayout`** (app bar with logo + dark toggle, centered
`MudContainer`) from the authenticated app layout, routes both at `/`, and inside `Home.razor` wraps
the splash body in `<HierarchicalRoleAuthorizeView>`: `Authorized` → `<RedirectToDashboard/>`,
`NotAuthorized` → hero image + title + subtitle + Login/Register buttons in a centered `MudStack`.
We adopt: (a) a **dedicated public layout** distinct from `CmsLayout`, (b) the
**`HierarchicalRoleAuthorizeView` redirect-the-authed-user** idiom at `/`, (c) the **centered hero +
single CTA** composition. We diverge on branding and drop Register (CMS access is invite/seed-only —
there is no public registration path; see §4).

---

## 2. Routing reshape — the load-bearing decision

**Recommendation: Option A — splash owns `/`, catalogue moves to `/catalogue`, authed users are
redirected onward from the splash via `HierarchicalRoleAuthorizeView`.**

This is the Skipper pattern transplanted directly, and it is the right one here. Three options were
weighed:

### Option A (recommended) — splash at `/`, catalogue at `/catalogue`, redirect authed from `/`

- New `Home.razor` takes `@page "/"`, **no `[Authorize]`**, public layout. Body wrapped in
  `<HierarchicalRoleAuthorizeView>`: `Authorized` → redirect to `/catalogue`; `NotAuthorized` → the
  hero + Login CTA.
- `Index.razor` (the catalogue) changes its route from `@page "/"` to `@page "/catalogue"`,
  everything else unchanged (keeps `[Authorize]` + `CmsLayout`).
- **Pros:** Mirrors Skipper exactly (proven). `/` is a clean public URL. Anonymous and authed users
  share one entry point; the redirect is declarative and lives next to the splash it guards. No
  `Routes.razor` change. The catalogue page is otherwise untouched.
- **Cons:** The catalogue's URL changes — internal links to `/` that *meant* "the dashboard" must
  repoint to `/catalogue` (see §6 migration checklist; the live count is small — `CmsLayout`'s
  "Back to site" button and any nav). One new redirect component (`RedirectToCatalogue`) or an inline
  `NavigationManager` redirect.

### Option B — splash at `/`, catalogue stays at `/`, layout chosen by auth state in `Routes.razor`

Skipper's *other* mechanism: `Routes.razor` computes `MainHomeLayout` vs app layout from auth state in
`OnParametersSetAsync`, and both the splash and catalogue claim `@page "/"` is **not** possible (route
collision). To keep the catalogue at `/`, the splash would have to be conditionally rendered *inside*
the catalogue page or the layout — collapsing two concerns into one component.

- **Pros:** Catalogue URL never changes; zero link migration.
- **Cons:** Forces splash + dashboard logic into one routable component or bleaks auth-branching into
  the layout. Violates the project's preference for one-source/clean seams. The splash is structurally
  a *different page* with a *different layout*; conflating them is the wrong abstraction. Rejected.

### Option C — splash at a distinct route (`/welcome`), `/` unchanged

Splash lives at `/welcome`; `/` keeps the catalogue with `[Authorize]`, so anonymous `/` still
redirects to login (today's behavior), and nothing lands on the splash unless explicitly routed there.

- **Pros:** Smallest diff; no link migration.
- **Cons:** Defeats the goal. The "public face" is the thing an anonymous visitor sees when they
  arrive at the app root. A splash nobody routes to is dead. Rejected.

**Why A over the rest:** the goal is *"`/` is the public face."* That demands the splash own `/`,
which demands the catalogue move. The redirect-authed-user idiom keeps the admin's experience
seamless (they never see the splash). Option A is the only one that satisfies the goal without
conflating page concerns. The link-migration cost is real but small and mechanical.

### Where AuthBlocks fits

- The splash body is wrapped in **`<HierarchicalRoleAuthorizeView>`** (bare, no roles — "is this
  visitor authenticated at all?").
  - `Authorized` slot → redirect to `/catalogue` (mirrors Skipper's `RedirectToDashboard`). The CMS
    further gates the catalogue on the `Admin` role via the existing `[Authorize]` /
    `AuthorizeRouteView` path — so a logged-in non-admin still hits `RedirectToAccessDenied` at
    `/catalogue`, which is correct and unchanged.
  - `NotAuthorized` slot → the hero + Login CTA.
- The Login button links to **`/account/login?returnUrl={Uri.EscapeDataString("catalogue")}`** so a
  successful login lands the admin on the catalogue, not back on the splash. (Skipper uses
  `returnUrl=dashboard`; same idiom, our route.)
- `Routes.razor` needs **no change** — its `AuthorizeRouteView` already renders no-`[Authorize]` pages
  for anonymous users, and the host is already `AllowAnonymous` at the endpoint.

---

## 3. New + changed files and their responsibilities

### 3.1 New: `Components/Layout/CmsHomeLayout.razor` (public splash layout)

A public counterpart to `CmsLayout`, deliberately lighter. Responsibilities:

- Own its **own `MudThemeProvider`** — `IsDarkMode="false" Theme="DeepDrftPalettes.Cms"` (same theme
  as the admin surface, so the splash is of-a-piece with the CMS, navy AppBar and all). Splash is
  light-only like the rest of the CMS; no dark toggle (the CMS has none today — do not introduce one
  here).
- Own a **minimal provider set**: include `MudPopoverProvider` (tooltips on the splash, if any need
  it) and skip `MudDialogProvider` / `MudSnackbarProvider` unless a splash interaction needs them
  (it does not in this spec — omit them to keep the layout lean).
- A **slim AppBar** consistent with `CmsLayout`'s navy bar but read as a *front door*, not an admin
  console: brand text "Deep Drft" (or "Deep Drft — Admin" to set expectation; see §4 open question),
  `Typo.h6`, DM-Sans, letter-spacing as in `CmsLayout`. No "Back to site" home button (the splash
  *is* the front door). No nav drawer.
- `MudMainContent` wrapping a centered `MudContainer MaxWidth="Small"` (the hero is a focused column,
  not full-bleed). Vertical centering via MudBlazor utility classes (`d-flex`, `flex-column`,
  `justify-center`, `align-center`) and a min-height tied to the viewport (`Style="min-height:
  calc(100vh - <appbar>);"` or the `mud-height-full` utility on the content) so the hero sits centered
  in the page, not jammed at the top. Prefer MudBlazor utilities; the single inline `min-height` is the
  one justified bespoke style (no utility expresses "viewport minus app bar" cleanly).
- Include the `<div id="blazor-error-ui">` block (copy from `CmsLayout`) so the InteractiveServer
  error UI is present on the public surface too.

**Why a separate layout rather than reusing `CmsLayout`:** `CmsLayout` is built for the admin console
(full-width container `MaxWidth.False`, "Back to site" affordance, dialog/snackbar providers for CRUD
flows). The splash wants a centered narrow column and a front-door AppBar. Forcing one layout to do
both would mean auth-branching inside the layout — the same conflation Option B was rejected for.
This mirrors Skipper's `MainHomeLayout` vs app-layout split exactly.

### 3.2 New: `Components/Pages/Home.razor` (the splash page)

- `@page "/"`, `@layout Layout.CmsHomeLayout`, **no `[Authorize]`**.
- `<PageTitle>Deep Drft — Admin</PageTitle>` (or "Deep Drft" — see §4).
- Body = `<HierarchicalRoleAuthorizeView>` with the two slots from §2 (`Authorized` → redirect to
  `/catalogue`; `NotAuthorized` → hero + CTA per §5).
- No data fetch, no injected services beyond `NavigationManager` (only if doing an inline redirect
  rather than a `RedirectToCatalogue` component). Keep it presentational.

### 3.3 New: `Components/RedirectToCatalogue.razor` (optional, recommended)

Mirrors the existing `RedirectToAccessDenied.razor` exactly (inject `NavigationManager`, redirect in
`OnInitialized`). Target `/catalogue`. Use `NavigateTo("/catalogue", forceLoad: false)` —
client-side nav is fine here (unlike Skipper's `RedirectToDashboard` which uses `forceLoad: true`;
that was a Skipper-specific concern. Default to no force-load and let staff-engineer confirm against
the JWT-in-localStorage auth-state timing — if the authed redirect mis-fires on first paint, escalate
to `forceLoad: true` as Skipper does). Keeping this as a named component matches the existing
`RedirectToAccessDenied` convention; an inline redirect in `Home.razor` is acceptable if staff-engineer
prefers fewer files.

### 3.4 Changed: `Components/Pages/Index.razor` (the catalogue)

- **One-line change:** `@page "/"` → `@page "/catalogue"`. Everything else (the `[Authorize]`,
  `CmsLayout`, the three summary cards, the concurrent loaders) is unchanged.
- Optional rename for clarity (`Index.razor` → `Catalogue.razor`): **defer.** Not required; a rename
  ripples through `ILogger<Index>` and any references. Out of scope for this spec — flag only if
  staff-engineer is already touching those lines.

### 3.5 Changed: `Components/Layout/CmsLayout.razor` (the "Back to site" button)

- The "Back to site" home button currently has `Href="/"`. Post-reshape, `/` is the **public splash**,
  not the catalogue. Two readings:
  - If "Back to site" means *the admin home / catalogue*, repoint to `Href="/catalogue"`.
  - If "Back to site" means *the public DeepDrft site* (i.e., `DeepDrftPublic`), it should point at the
    public site's URL, not anything in Manager — but that cross-host URL isn't configured in the CMS
    today, so this is likely **not** the intent.
  - **Recommendation:** repoint to `/catalogue` (the in-CMS home). The tooltip text "Back to site" is
    then slightly off — consider "Catalogue" or "Home". Staff-engineer's call on wording; the *Href*
    must change to `/catalogue` regardless, or the admin's home button lands them on the splash, which
    then bounces them back to `/catalogue` via the authed-redirect — functional but an ugly double-hop.

---

## 4. Branding, copy, and the no-Register decision

- **Identity:** DeepDrft navy/green/off-white via `DeepDrftPalettes.Cms`. Display type Cormorant
  Garamond (it comes free from the theme's `Typo.h2`/`h3` etc. — do **not** hand-set font-family in
  markup; use `Typo` and let the theme resolve it). Body DM Sans, button/caption Geist Mono — again,
  theme-resolved.
- **No Register CTA.** Skipper offers Register (marina staff self-onboard with a code). The DeepDrft
  CMS is gated by AuthBlocks login + hierarchical `Admin` role, seeded/invited — there is no public
  registration page in `DeepDrftManager`, and AuthBlocksWeb's registration (if present) is not a path
  we want to advertise on the front door of an admin tool. **Single Login CTA only.** If Daniel later
  wants an invite-code flow, that is a separate feature, not part of this splash.
- **Copy (proposed, Daniel may revise):**
  - Title: **"Deep Drft"** (Cormorant, `Typo.h2`, the brand) — or stylized **"DEEP DRFT"** uppercase
    to echo Skipper's `SKIPPER` treatment. Recommend brand-cased "Deep Drft" to match the public
    site's wordmark rather than Skipper's all-caps.
  - Subtitle: **"Catalogue Management"** or **"Collective Content Management"** (Geist Mono,
    `Typo.subtitle1`, uppercase via `text-uppercase`, muted/secondary) — names what's behind the door
    without overselling. Recommend **"Catalogue Management"**.
  - CTA button label: **"Login"**.
- **Open question for Daniel (one):** should the AppBar + title say **"Deep Drft"** (front-door, clean)
  or **"Deep Drft — Admin"** (sets the expectation that this is the admin tool, consistent with
  `CmsLayout`'s AppBar)? Recommend **"Deep Drft — Admin"** in the AppBar for continuity with the
  authenticated surface, and **"Deep Drft"** as the large hero title. This is a copy call, not a
  structural one — staff-engineer can implement either without rework.

---

## 5. Hero + CTA composition

A centered, single-column hero in `MudContainer MaxWidth="Small"`, vertically centered in the
viewport. Composition top-to-bottom inside a `MudStack AlignItems="AlignItems.Center" Spacing="4"`:

1. **Hero image** — `MudImage Fluid="true" Src="img/cms-hero.png"` (asset path §5.1). Constrain its
   width with the container (`MaxWidth="Small"`) so it reads as a focused emblem, not a full-bleed
   banner. If Daniel's asset is wide/cinematic rather than emblem-shaped, wrap it in a
   `MudPaper`-backed rounded region with `Style="border-radius: 8px; overflow: hidden;"` — but default
   to the plain `MudImage` and let the asset dictate (see §5.1 open note).
2. **Title** — `MudText Typo="Typo.h2"` "Deep Drft" (Cormorant via theme), centered
   (`Align="Align.Center"`).
3. **Subtitle** — `MudText Typo="Typo.subtitle1"` "Catalogue Management" (Geist Mono via theme),
   `Class="text-uppercase mud-text-secondary"`, centered.
4. **Spacer** — `MudSpacer` or a `Class="my-4"` gap.
5. **Login CTA** — `MudButton Variant="Variant.Filled" Color="Color.Primary"
   Href="/account/login?returnUrl=..."` (see §2), full-width within a `MaxWidth.Small` inner region or
   a fixed comfortable width (`Style="min-width: 200px;"`). Label "Login". Single button, centered.

This is the Skipper hero stack (image / title / subtitle / spacer / CTA) with the Register row and the
two-column button grid removed (one CTA needs no grid). No bespoke CSS beyond the optional rounded-image
wrapper and the layout's one `min-height` — everything else is MudBlazor `Typo`, `Color`, and spacing
utilities.

### 5.1 Hero asset path and reference

- **Asset location:** `DeepDrftManager/wwwroot/img/cms-hero.png` (Daniel supplies the file). This
  creates the `wwwroot/img/` directory, which does not exist today — staff-engineer creates the folder
  when the asset is dropped in.
- **Reference in markup:** `Src="img/cms-hero.png"` (relative; `MapStaticAssets()` in `Program.cs`
  serves `wwwroot/`). Matches Skipper's `Src="img/skipper-hero.png"` idiom exactly. Do **not** wrap in
  `@Assets[...]` / fingerprinting unless the project's static-asset fingerprinting convention requires
  it for cache-busting — for a single rarely-changing hero, the plain relative path is fine and matches
  the public site's `img/...` references.
- **Filename:** `cms-hero.png` recommended. If Daniel's asset is a JPEG, name it `cms-hero.jpg` and
  update the `Src`. One asset, one reference — keep it boring.
- **Open note (non-blocking):** the spec assumes an emblem/portrait-ish hero suited to a centered
  narrow column (Skipper uses a boat illustration). If Daniel's asset is a wide cinematic photo, the
  composition still works but consider `MaxWidth="Medium"` on the container and the rounded-paper wrap
  from §5.1. Staff-engineer adjusts container width to the asset; no design rework needed.

---

## 6. Migration checklist (link repointing)

Every internal reference that meant "the catalogue lived at `/`" must move to `/catalogue`:

1. **`CmsLayout.razor`** "Back to site" button `Href="/"` → `Href="/catalogue"` (§3.5).
2. **`Index.razor`** summary-card "View" buttons → already navigate to `/tracks`, unaffected.
3. **Any nav menu / breadcrumb / link in CMS components pointing at `/`** as "home/dashboard" →
   `/catalogue`. staff-engineer: grep `DeepDrftManager` for `Href="/"` and `NavigateTo("/")` /
   `NavigateTo("/", ` and repoint the ones that mean *catalogue* (leave any that genuinely mean *the
   public front door* pointing at `/`).
4. **Login `returnUrl`** anywhere it defaults to the old root → `catalogue`.

This grep-and-repoint is the entire cost of Option A. It is mechanical and small (the CMS is a compact
host), but it must be exhaustive or an admin lands on the splash and double-hops.

---

## 7. Constraints honored

- **DRY / MudBlazor-first:** reuses `DeepDrftPalettes.Cms` (no new palette), reuses the
  `RedirectToAccessDenied` redirect idiom for `RedirectToCatalogue`, reuses `HierarchicalRoleAuthorizeView`
  (no new auth machinery), composes from `MudAppBar`/`MudContainer`/`MudStack`/`MudText`/`MudImage`/
  `MudButton`. Bespoke CSS limited to one layout `min-height` and an optional rounded-image wrapper.
- **No new shared component worth extracting.** The splash is host-specific (CMS front door); it does
  not belong in `DeepDrftShared.Client`. The public site's hero idiom (`ReleaseHeroOverlay`,
  full-bleed background + overlaid metadata) is a *different* composition (content hero, not a login
  gate) — do not try to reuse it here; the shapes don't match and forcing it would be the wrong borrow.
  If a *second* gated host ever needs a login splash, revisit extracting a `LoginSplash` shared
  component then — not now (YAGNI; one consumer).
- **Render mode:** no explicit `@rendermode` override anywhere — the host's global
  `AddInteractiveServerRenderMode()` governs, per DeepDrftManager convention. The splash is static
  presentational content + one auth-view + links; it needs no interactivity of its own, but inherits
  the host render mode without an override (correct).
- **Admin experience intact:** only the catalogue's *route* changes; its page, layout, and behavior are
  untouched. Authed admins are redirected past the splash and never see it.

---

## 8. Acceptance criteria

1. An **anonymous** visitor navigating to `/` sees the splash: navy CMS-themed front-door AppBar,
   centered hero image (from `wwwroot/img/cms-hero.png`), "Deep Drft" title (Cormorant), a
   "Catalogue Management" subtitle (Geist Mono, uppercase, muted), and a single filled "Login" button.
   No redirect to the login form occurs; the splash renders.
2. Clicking **Login** navigates to `/account/login` with `returnUrl` resolving to `/catalogue`; a
   successful login lands the admin on the catalogue.
3. An **authenticated admin** navigating to `/` is redirected to `/catalogue` (the catalogue
   dashboard) without the splash flashing/persisting.
4. An **authenticated non-admin** (logged in, lacking `Admin` role) hitting `/` is redirected toward
   `/catalogue` and then to access-denied via the existing `AuthorizeRouteView` path — behavior
   unchanged from today's catalogue gating.
5. The **catalogue dashboard** is reachable at `/catalogue`, renders under `CmsLayout` with
   `[Authorize]`, and its three summary cards load as before. No regression.
6. `CmsLayout`'s home/"Back to site" button lands an admin on `/catalogue`, not on the splash.
7. The splash uses the **shared `DeepDrftPalettes.Cms` theme** (navy AppBar, off-white ground) — it
   reads as the same product as the admin console, not as Skipper and not as a generic MudBlazor page.
8. No `@rendermode` override is introduced; no new palette is introduced; no new entry appears in
   `DeepDrftShared.Client`.
9. (If `cms-hero.png` is absent at build time) the page still compiles and renders — only the image is
   broken. The splash must not hard-depend on the asset to function (it is an `<img>` `src`, so this
   holds by construction).

---

## 9. Out of scope / deferred

- **Register / invite-code flow** — explicitly omitted (§4). Separate feature if ever wanted.
- **Dark-mode toggle on the splash** — the CMS is light-only today; do not introduce a toggle here.
- **Renaming `Index.razor` → `Catalogue.razor`** — optional cleanup, deferred (§3.4).
- **Cross-host "back to the public site" link** — if "Back to site" should point at `DeepDrftPublic`
  rather than `/catalogue`, that needs the public site URL configured in the CMS (it isn't today).
  Deferred; default to `/catalogue` (§3.5).
- **Extracting a shared `LoginSplash` component** — YAGNI with one consumer (§7).
