alepha@docs:~/docs/.content/release/docs/reference/primitives$
cat $pageAdmin.md | pretty
3 min read

#$pageAdmin

#Import

typescript
1import { $pageAdmin } from "@alepha/ui/admin";

#Overview

$pageNav already parented to AdminRouter's /admin shell: the one-call form of "a page inside the shared admin shell". It exists because the alternative (injecting AdminRouter and writing parent: this.admin.layout by hand on every page) puts the rules of that composition nowhere an author will read them.

It carries the $ prefix to sit with the framework's other declarations at a call site, but it is a plain function wrapping $pageNav (which in turn wraps $page) rather than a createPrimitive primitive. Nothing about its lifecycle differs from declaring $page yourself.

The page appears in the shell's sidebar with no separate registration step: useNavEntries walks the parent chain and reads each page's own nav, the same as any other page hung off AdminRouter.layout.

Its tab reads Admin - <title>, the way every built-in page's does: a static head.title or the title a head function returns is prefixed, with the prefix in the reader's language (admin.title). The title itself is the application's to translate: a static one is used as written. When the page names no nav.label and no label, its unprefixed static title becomes the label, so its sidebar entry and breadcrumb do not carry the prefix.

Calling this registers AdminRouter. Declaring even one page this way mounts the whole /admin shell, including its fourteen built-in pages (Users, Sessions, Jobs, …): an admin page without the admin shell around it is not a thing. This is the intended reading, but it is a real side effect: an application that wants /admin to carry only its own pages, with none of the built-ins, must build its own layout page instead of reaching for this helper.

Stay below order: 1000, or declare your own nav.group. The built-ins are parked in a reserved high band, Identity (1000-1003) and System (1010-1016), precisely so an application's own pages come first: the conventional order: 100 sorts above them. useNavEntries sorts groups by their smallest member, so only an order of 1000 or more sinks a page in among the built-ins.

Gate with can: () => this.someApi.someAction.can(), not with permission alone. A permission named by this page's own $secure is self-declaring: $secure registers it into SecurityProvider at definition time, so an admin holding the * wildcard is granted it whether or not any controller backing the page exists, and the entry would stay visible over a dead API. An action name resolves against /api/_links, a registry built only from actions the server actually registered, so a page whose backend never shipped never appears for anyone to be granted.

tsx
1class ShopAdminRouter {2  products = $pageAdmin({3    path: "/products",4    nav: { label: "Catalogue", icon: <Gem />, group: "Commerce", order: 100 },5    can: () => this.productApi.commerceAdminProductList.can(),6    lazy: () => import("./AdminProducts.tsx"),7  });8}