A Page is the foundation of every screen in Monorail. Everything
you see in a panel — the dashboard, the list view, the edit form, a
custom report, the record detail — is a subclass of
Monorail\Pages\Page.
If Resources are the "what" (the model, the table, the form), Pages are the "where" (the actual URL, the Inertia render, the navigation entry). Pages are the unit of routing.
Page (abstract)
├── DashboardPage — panel dashboard (/{panel})
└── ResourcePage (abstract) — scoped to a Resource
├── ListRecordsPage — /{panel}/{resource}
├── CreateRecordPage — /{panel}/{resource}/create
├── EditRecordPage — /{panel}/{resource}/{id}/edit
├── ViewRecordPage — /{panel}/{resource}/{id}
└── (your custom pages) — /{panel}/{resource}/{slug}
Every built-in CRUD screen ships as one of these subclasses. When you write a custom page you pick the right base class and override only what differs.
When a page is requested:
- Laravel routes the request to the page's handler.
mount(Request $request)runs — your hook for loading data, authorization, or setting$this->record.actions()is serialized to the page toolbar.content(Panel $panel)returns an array of blocks; each block'stoArray()is called.handle()wraps it all inInertia::render($component, [...])with shared panel props, page metadata, actions, and content.- React receives the schema and renders it deterministically.
No state lives on the client. The page is a pure function of the request.
php artisan monorail:make-page {name} [--resource=]| Form | Destination | Namespace |
|---|---|---|
monorail:make-page Settings |
app/Monorail/Pages/SettingsPage.php |
App\Monorail\Pages |
monorail:make-page AuditLog --resource=Post |
app/Monorail/Resources/PostResource/Pages/AuditLogPage.php |
App\Monorail\Resources\PostResource\Pages |
The Page suffix is appended automatically. The stub extends
Page and provides an empty content() for you to fill in.
Customise the generated template by publishing stubs:
php artisan vendor:publish --tag=monorail-stubs
# edit stubs/monorail/page.stubEvery page inherits these hooks from Page. Override only the ones
that matter to your screen.
| Method | Default | Override when |
|---|---|---|
getSlug() |
kebab-cased class basename | The URL segment should differ from the class name. |
component() |
'monorail/Page' |
You ship a custom React renderer. |
| Method | Default | Override when |
|---|---|---|
getNavigationLabel() |
getTitle() |
Sidebar label should differ from title. |
getNavigationIcon() |
null |
You want a lucide icon in the sidebar. |
getNavigationGroup() |
null |
Group the entry under a heading. |
getNavigationSort() |
0 |
Force ordering relative to siblings. |
shouldRegisterNavigation() |
true |
Hide the page from the sidebar (still routable). |
| Method | Default | Override when |
|---|---|---|
getTitle() |
titleized class basename minus Page |
You want a static or dynamic title. |
getSubtitle() |
null |
A subtitle improves the header. |
actions() |
[] |
Add toolbar buttons — see Page Actions. |
content(Panel $panel) |
[] |
Always — this is the body. Return an array of blocks. |
| Method | Default | Override when |
|---|---|---|
mount(Request) |
no-op | Load $this->record, validate params, fire side-effects. |
can(Request) |
true |
Gate the page behind a policy or ability. |
Extend DashboardPage (or Page directly) when the screen is not
tied to a single record. Compose it from
blocks — widgets, grids, HTML.
use Monorail\Pages\DashboardPage;
use Monorail\Pages\Blocks\GridBlock;
use Monorail\Pages\Blocks\WidgetBlock;
use Monorail\Panel\Panel;
final class OpsHealthPage extends DashboardPage
{
public function getTitle(): string
{
return 'Ops Health';
}
public function content(Panel $panel): array
{
return [
GridBlock::make()->columns(3)->schema([
WidgetBlock::make(QueueLagWidget::make()),
WidgetBlock::make(ErrorRateWidget::make()),
WidgetBlock::make(UptimeWidget::make()),
]),
];
}
}Register it on your panel via ->pages([OpsHealthPage::class]) or
with ->discoverPages(in: ..., namespace: ...).
Extend ResourcePage when the screen belongs to a resource — a
moderation inbox on PostResource, a billing report on
InvoiceResource. ResourcePage automatically pulls its label,
icon, navigation group, and authorization from the owning resource.
See Custom Resource Pages.
If your page needs a hand-rolled React view, return a different
component name from component() and mount the React page under
the same key in your host app's resources/js/pages/. The shared
panel, actions, and content props are still passed through, so
you can reuse the layout while taking over the body.
use App\Monorail\Pages\OpsHealthPage;
use Monorail\Panel\Panel;
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->pages([OpsHealthPage::class])
// or auto-discover everything in a directory:
->discoverPages(
directory: app_path('Monorail/Pages'),
namespace: 'App\\Monorail\\Pages',
);
}Resource-scoped pages are auto-discovered from each resource's
Pages/ subdirectory — no extra registration needed.
- Page Blocks — the composable schema fragments pages
return from
content(). - Custom Resource Pages — pages that live inside a resource.
- Server-Driven UI — the broader pattern Pages implement.