> ## Documentation Index
> Fetch the complete documentation index at: https://voyant.travel/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add a custom admin page

> Extend the admin dashboard without forking: drop a folder into the src/admin discovery seam to add a page, a widget, or a nav entry, then wire any generated admin artifacts with the voyant admin commands.

A [module](/docs/concepts/how-it-is-built) gives a capability its API. To give
it a presence in the [admin](/docs/platform/admin) dashboard, a custom **page**, a
dashboard or detail **widget**, or a sidebar **nav** entry, add a folder to the
deployment-local `src/admin/<name>/` seam. It is discovered at build time the
same way a [custom module](/docs/platform/extending/apps) is, so nothing in the
framework is edited and the surface survives `voyant upgrade`.

<Note>
  This is the UI counterpart to `src/modules` (API modules) and `src/extensions`
  (API routes on existing modules). Discovery is build-time: Vite compiles
  `import.meta.glob` to static imports.
  The `src/admin` directory is empty until your deployment adds an extension.
</Note>

## Add an admin surface

<Steps>
  <Step title="Drop a folder into src/admin">
    Create a directory under `src/admin/<name>/` in your deployment (the operator
    starter). The directory name is its key in the admin shell.

    ```
    src/admin/concierge/
      index.tsx      # default-exports the AdminExtension
      page.tsx       # a full page component (optional)
      widget.tsx     # a slot widget component (optional)
    ```

    A surface can contribute any combination of navigation, widgets, and routes,
    so include only the files you need.
  </Step>

  <Step title="Default-export an AdminExtension from index.tsx">
    `index.tsx` must default-export an `AdminExtension` wrapped with
    `defineAdminExtension` from `@voyant-travel/admin`. It is a bundle of any of
    `navigation`, `widgets`, and `routes`:

    ```tsx theme={null}
    import { defineAdminExtension } from "@voyant-travel/admin"
    import { ConciergePage } from "./page.js"
    import { ConciergeWidget } from "./widget.js"

    export default defineAdminExtension({
      id: "concierge",
      // a sidebar entry
      navigation: [{ items: [{ id: "concierge", title: "Concierge", url: "/concierge" }] }],
      // a widget injected into a named slot (e.g. dashboard.after-kpis,
      // booking.details.header, invoice.details.after-summary, …)
      widgets: [{ id: "concierge-kpi", slot: "dashboard.after-kpis", component: ConciergeWidget }],
      // a full page at /app/concierge (component or lazy `page: () => import(...)`)
      routes: [{ id: "concierge", path: "/concierge", title: "Concierge", component: ConciergePage }],
    })
    ```

    Keep page components lazy where possible (`page: () => import("./page.js")`)
    so each page lands in its own chunk instead of the workspace-chrome bundle.
    Widget `slot` names are the ones the starter exposes (see
    `src/lib/admin-extensions.tsx`); targeting an unknown slot simply renders
    nothing.
  </Step>

  <Step title="How it composes (no manifest to edit)">
    There is nothing to register. The operator application discovers
    `src/admin/*/index.tsx` and composes each default export into the admin shell:

    * **Nav and widgets** merge in `src/lib/admin-extensions.tsx`
      (`adminExtensionsFromGlob` then `createOperatorAdminExtensions`) and resolve
      through the shared `resolveAdminNavigation` and `resolveAdminWidgets`.
    * **Page routes** are grafted into the route tree at runtime by
      `src/router.tsx` (`buildAdminExtensionRoutes`). Discovered pages are
      reachable by string navigation (`<Link to="/concierge">`); they are not in
      the generated typed-link map.
  </Step>

  <Step title="Regenerate admin artifacts (only for packaged-module surfaces)">
    The `src/admin` seam needs no codegen. The `voyant admin generate` commands
    are for the other admin path: admin surfaces shipped by **modules** listed in
    your `voyant.config.*` manifest, which are wired into committed, generated
    files rather than discovered at build time.

    ```bash theme={null}
    voyant admin generate               # emit admin.extensions.generated.ts from the manifest
    voyant admin generate --routes      # emit the code-assembled admin route module
    voyant admin generate --destinations # emit the generated destination resolver map
    ```

    Run any of these after changing the `modules` list in `voyant.config.*`. Add
    `--check` to verify the generated files are in sync without rewriting them,
    which is useful in CI.
  </Step>

  <Step title="Verify">
    Check that the admin manifest, extensions, and routes are in parity, then run
    the full preflight.

    ```bash theme={null}
    voyant admin doctor   # manifest, extension, and route parity
    voyant doctor         # the full deployment preflight
    ```
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Admin" icon="gauge" href="/docs/platform/admin">
    The shared admin runtime, the application-owned shell, and its extension points.
  </Card>

  <Card title="Build an app" icon="cubes" href="/docs/platform/extending/apps">
    Give your admin surface an API and canonical state of its own.
  </Card>

  <Card title="Modules" icon="cube" href="/docs/concepts/how-it-is-built">
    The concepts behind module isolation, composition, and discovery.
  </Card>

  <Card title="Configuration" icon="sliders" href="/docs/platform/fundamentals/configuration">
    The voyant.config manifest that the admin generate commands read.
  </Card>
</CardGroup>
