Appearance
Admin ↔ public linking map
The single map from an admin-managed entity to (a) its admin edit view and (b) its public surface — and the shared affordances that let an admin cross between the two in one click. Before this, that round-trip was manual (copy a slug, guess a URL); every admin screen now carries a consistent control, and the buyer-facing track flow carries an admin-only jump back to the editor.
Introduced by #467 (part of the admin-console-usability capability, #466).
The single source
src/lib/entity-links.ts declares the entity → surface split once, so it is not re-derived per page. It is framework- and server-free, so both admin Svelte components and public page loads import it. It reuses the existing deep-link seam (src/lib/deeplink.ts, #414) for external nanawall.com URLs rather than introducing a parallel one.
| Export | Purpose |
|---|---|
adminEditPath(kind, id?) | The /admin/<section>/<id> edit URL. Section-only CRUD kinds (category, tier) — or a call with no id — resolve to the section list page. |
resolvePublicSurface(ref, { base? }) | The entity's public surface as a { live | preview | none } union, driven by the entity's state. |
PublicSurface is a discriminated union:
live— publicly reachable now (href,external).preview— no live public URL, but an admin can preview it at its real path (a draft/retired track — unroutable to buyers, admin-previewable).none— no public surface at all (admin-only);reasonexplains why, and is shown on the disabled affordance.
Entity → public surface
| Entity kind | Admin edit view | Public surface | Resolution |
|---|---|---|---|
| system | /admin/systems/<id> | nanawall.com product-detail page (external) | live when pdp_path is captured (via productDetailLink); none otherwise ("nothing to link to on nanawall.com yet"). |
| track | /admin/tracks/<id> | /select/<slug> guided flow (in-app) | live when published; preview when draft/retired (admin-only). |
| attribute | /admin/attributes/<id> | — | none — surfaces only as rows in the public comparison. |
| factor | /admin/factors/<id> | — | none — factors are the steps of the guided flow, no standalone page. |
| category | /admin/categories | — | none — admin-only grouping. |
| tier | /admin/tiers | — | none — admin-only right-sizing configuration. |
| right-sizing | /admin/right-sizing/<id> | — | none — admin-only selection logic. |
Only system (external) and track (in-app) have public surfaces; every other kind is admin-only and renders the affordance disabled with its reason.
Admin → public affordance
src/lib/admin/ui/PublicSurfaceLink.svelte (barrel-exported from $lib/admin/ui) renders a resolved PublicSurface consistently:
live→ View public link (↗+ new tab when external),preview→ Preview link (tooltip explains it is not publicly live),none→ a disabled "No public page" control whose tooltip is the reason.
It composes the shared Button, so all styling flows through the --ns-* token contract. It is mounted in the DetailHeader actions slot of every admin [id] detail screen (systems, tracks, attributes, factors, right-sizing) — one pattern, so it stays consistent as new admin screens are added.
Public → admin affordance
On the buyer-facing /select/<slug> track surface, an authenticated admin (only) gets a jump back to the track's admin edit view:
- On a live (published) track — a low-emphasis "Admin view — Edit in admin ↗" bar (
data-testid="admin-edit-affordance"). - On an admin preview (draft/retired) — an "Edit this track" link inside the existing preview banner.
The admin decision is resolved server-side by isRequestAdmin(request, env, db) in src/lib/server/auth/admin-request.ts. locals.admin is only populated for /admin paths (see hooks.server.ts), so a public route must resolve it itself; this helper lifts the inline resolveAdminAuth + dev-bypass defaulting the route had hand-rolled into one shared seam. It is cheap for buyers: with no Cloudflare Access token present, resolveAdminAuth returns unauthenticated before any database read — only a signed-in admin pays the full JWT verification + admin-user lookup.
isAdmin / adminEditHref are server-gated: a non-admin payload never carries the edit link, so the affordance can never leak to a buyer.
Admin → docs affordance (the third dimension, #568/#672)
Besides its public surface, every admin function section links to the documentation that explains it. ADMIN_SECTION_DOC in src/lib/entity-links.ts is the single map from /admin/<section> to a site-relative doc path; PageHeader renders the "Docs" affordance from it on every section page, resolved through docUrl() — the live docs host when PUBLIC_DOCS_BASE_URL is set, the GitHub-rendered source otherwise. No admin doc link bypasses this seam.
Coverage is universal (#672): all 22 admin function sections are mapped — systems, attributes, categories, factors, right-sizing, tracks, media, page-chrome, pages, corpus, themes, audit, analytics, users, content, content-tags, drupal-sync, eloqua, experiments, leads, privacy, tiers. A coverage test in src/lib/entity-links.spec.ts enumerates src/routes/admin/ and fails if a section ships unmapped, and a companion test fails if a mapped doc file does not exist.
Scope guard (deliberate exemption): the admin home (/admin itself) is a navigation surface, not a function — adminSectionFromPath returns null for it, so it renders no docs affordance. This is the only exemption, chosen rather than overlooked.
Pairing convention: each map target is the section's task-oriented how-to (docs/how-to/…), which carries a prominent "What/why" pointer to its conceptual explanation page (docs/explanation/…); each explanation's "In the admin" table links back. A reader entering from the admin, a how-to, or an explanation reaches the other two. (Analytics is the one section whose map target is its explanation — the measurement plan — since the dashboards are the concept.)
Related
src/lib/deeplink.ts— the external nanawall.com URL seam this map reuses (#414).- Admin UI kit — the shared component vocabulary
PublicSurfaceLinkbelongs to. #466— admin-console-usability capability (parent).