# 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().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 `` 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 ``: `Authorized` → ``, `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 ``: `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 **``** (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 - );"` 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 `
` 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]`**. - `Deep Drft — Admin` (or "Deep Drft" — see §4). - Body = `` 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` 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 `` `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).