Splash owns /, catalogue moves to /catalogue, authed users redirected via HierarchicalRoleAuthorizeView. Skipper's public-layout pattern, branded to DeepDrft. Adds Phase 13 to PLAN.md.
22 KiB
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—AuthorizeRouteViewwithDefaultLayout = Layout.CmsLayout.NotAuthorized: authenticated-but-unauthorized →RedirectToAccessDenied; unauthenticated →RedirectToLogin(the AuthBlocksWeb component, not a Manager-local one).AdditionalAssembliesalready includestypeof(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 byAuthorizeRouteView. 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 inDeepDrftManagertoday. The host's only wwwroot asset isapp.css. The hero asset and its folder are net-new. - Theme —
DeepDrftPalettes.Cms(inDeepDrftShared.Client.Common): navy#0D1B2Aground / 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.razortakes@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. NoRoutes.razorchange. 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 inlineNavigationManagerredirect.
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?").Authorizedslot → redirect to/catalogue(mirrors Skipper'sRedirectToDashboard). The CMS further gates the catalogue on theAdminrole via the existing[Authorize]/AuthorizeRouteViewpath — so a logged-in non-admin still hitsRedirectToAccessDeniedat/catalogue, which is correct and unchanged.NotAuthorizedslot → 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 usesreturnUrl=dashboard; same idiom, our route.) Routes.razorneeds no change — itsAuthorizeRouteViewalready renders no-[Authorize]pages for anonymous users, and the host is alreadyAllowAnonymousat 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 skipMudDialogProvider/MudSnackbarProviderunless 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 inCmsLayout. No "Back to site" home button (the splash is the front door). No nav drawer. MudMainContentwrapping a centeredMudContainer 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 themud-height-fullutility on the content) so the hero sits centered in the page, not jammed at the top. Prefer MudBlazor utilities; the single inlinemin-heightis the one justified bespoke style (no utility expresses "viewport minus app bar" cleanly).- Include the
<div id="blazor-error-ui">block (copy fromCmsLayout) 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 aRedirectToCataloguecomponent). 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 throughILogger<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/catalogueregardless, or the admin's home button lands them on the splash, which then bounces them back to/cataloguevia the authed-redirect — functional but an ugly double-hop.
- If "Back to site" means the admin home / catalogue, repoint to
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'sTypo.h2/h3etc. — do not hand-set font-family in markup; useTypoand 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
Adminrole, seeded/invited — there is no public registration page inDeepDrftManager, 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'sSKIPPERtreatment. 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 viatext-uppercase, muted/secondary) — names what's behind the door without overselling. Recommend "Catalogue Management". - CTA button label: "Login".
- Title: "Deep Drft" (Cormorant,
- 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":
- 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 aMudPaper-backed rounded region withStyle="border-radius: 8px; overflow: hidden;"— but default to the plainMudImageand let the asset dictate (see §5.1 open note). - Title —
MudText Typo="Typo.h2""Deep Drft" (Cormorant via theme), centered (Align="Align.Center"). - Subtitle —
MudText Typo="Typo.subtitle1""Catalogue Management" (Geist Mono via theme),Class="text-uppercase mud-text-secondary", centered. - Spacer —
MudSpaceror aClass="my-4"gap. - Login CTA —
MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/account/login?returnUrl=..."(see §2), full-width within aMaxWidth.Smallinner 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 thewwwroot/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()inProgram.csserveswwwroot/). Matches Skipper'sSrc="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'simg/...references. - Filename:
cms-hero.pngrecommended. If Daniel's asset is a JPEG, name itcms-hero.jpgand update theSrc. 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:
CmsLayout.razor"Back to site" buttonHref="/"→Href="/catalogue"(§3.5).Index.razorsummary-card "View" buttons → already navigate to/tracks, unaffected.- Any nav menu / breadcrumb / link in CMS components pointing at
/as "home/dashboard" →/catalogue. staff-engineer: grepDeepDrftManagerforHref="/"andNavigateTo("/")/NavigateTo("/",and repoint the ones that mean catalogue (leave any that genuinely mean the public front door pointing at/). - Login
returnUrlanywhere 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 theRedirectToAccessDeniedredirect idiom forRedirectToCatalogue, reusesHierarchicalRoleAuthorizeView(no new auth machinery), composes fromMudAppBar/MudContainer/MudStack/MudText/MudImage/MudButton. Bespoke CSS limited to one layoutmin-heightand 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 aLoginSplashshared component then — not now (YAGNI; one consumer). - Render mode: no explicit
@rendermodeoverride anywhere — the host's globalAddInteractiveServerRenderMode()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
- An anonymous visitor navigating to
/sees the splash: navy CMS-themed front-door AppBar, centered hero image (fromwwwroot/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. - Clicking Login navigates to
/account/loginwithreturnUrlresolving to/catalogue; a successful login lands the admin on the catalogue. - An authenticated admin navigating to
/is redirected to/catalogue(the catalogue dashboard) without the splash flashing/persisting. - An authenticated non-admin (logged in, lacking
Adminrole) hitting/is redirected toward/catalogueand then to access-denied via the existingAuthorizeRouteViewpath — behavior unchanged from today's catalogue gating. - The catalogue dashboard is reachable at
/catalogue, renders underCmsLayoutwith[Authorize], and its three summary cards load as before. No regression. CmsLayout's home/"Back to site" button lands an admin on/catalogue, not on the splash.- The splash uses the shared
DeepDrftPalettes.Cmstheme (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. - No
@rendermodeoverride is introduced; no new palette is introduced; no new entry appears inDeepDrftShared.Client. - (If
cms-hero.pngis 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
DeepDrftPublicrather 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
LoginSplashcomponent — YAGNI with one consumer (§7).