---
title: "Custom pages"
---

> Documentation Index
> Fetch the complete documentation index at: https://pupitre.rikology.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom pages

Use a custom page when the workflow is not a model's CRUD lifecycle. Put its module in `app/admin/pages/*_page.ts`, or register it directly on the panel.

```ts
import { definePage } from '@rikology/pupitre'

export default definePage('preferences')
  .path('/preferences').title('Preferences').icon('settings')
  .canAccess((ctx) => Boolean(ctx.auth.user))
  .form((s) => [s.text('fullName').required()])
  .fill((ctx) => ({ fullName: ctx.auth.user!.fullName }))
  .save(async ({ data, ctx }) => {
    ctx.auth.user!.merge({ fullName: String(data.fullName) })
    await ctx.auth.user!.save()
  })
```

Adapt the user attributes and access predicate to your host model and policy. A page access check protects page form/options/upload boundaries too. `.navigation({...})` controls menu group/sort/badge; `.actions([...])` adds header actions. `.component('HostPage')` selects a host registry component for a completely custom UI.

`.widgets([...])` and `.filters([...])` make custom dashboards using the same widget grid as the panel. The default path is `/<slug>`, mounted under the panel's path (and tenant path when applicable). `.title()` overrides the title inferred from the slug. See [CustomPageDefinition](/reference/api-page-widget/) for exact fill/save callback signatures.

Generate a page with `node ace make:page Preferences`. The generator preserves existing files unless `--force` is passed; the assembler hook discovers modules by their page suffix.

Source: https://pupitre.rikology.com/guides/custom-pages/index.mdx
