Skip to content

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.

ExportPurpose
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); reason explains why, and is shown on the disabled affordance.

Entity → public surface

Entity kindAdmin edit viewPublic surfaceResolution
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/categoriesnone — admin-only grouping.
tier/admin/tiersnone — 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:

  • liveView public link ( + new tab when external),
  • previewPreview 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.)

  • src/lib/deeplink.ts — the external nanawall.com URL seam this map reuses (#414).
  • Admin UI kit — the shared component vocabulary PublicSurfaceLink belongs to.
  • #466 — admin-console-usability capability (parent).