Pages
Pages
A page is a whole screen of your application. It is a component with its own route, layout and title, and it loads its data in mount(). The server renders it the first time it loads.
On this page
Pages live in app/Nitro/Pages, and their views live in
resources/views/nitro/pages. A page has everything a component has: properties, browser
methods, #[Server] methods and the same Blade. On top of that, a page has a route, a layout
around it, and a <head> for search engines and link previews.
You are reading one
Every page of these docs is a Nitro page, and you can read its class and view below. When you move between pages with the sidebar, the layout stays and only the page changes.
Routing a page
You can route a page by its class or by its name:
use App\Nitro\Pages\Posts\Show;
Route::nitro('/posts/{post}', Show::class)->name('posts.show');
Route::nitro('/posts', 'pages::posts.index')->name('posts.index');
Route::middleware('auth')->group(function () {
Route::nitro('/settings', Settings::class)->name('settings');
});
Route::nitro() returns a normal Laravel route. Route names, middleware, groups and route
model binding all work as usual. The page App\Nitro\Pages\Posts\Index is named
pages::posts.index.
Writing a page
php artisan make:nitro Posts/Show --page writes a page's class and view, and prints the route to add
(see make:nitro).
A page's mount() method receives the route's parameters, with route model binding. The
#[Title] and #[Layout] attributes control how the page is shown:
namespace App\Nitro\Pages\Posts;
use App\Models\Post;
use Nitro\Attributes\Layout;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Attributes\Title;
use Nitro\Component;
#[Title('Post')]
#[Layout('components.layouts.app')]
class Show extends Component
{
#[Locked]
public array $post = [];
public string $comment = '';
public function mount(Post $post): void
{
$this->post = $post->only('id', 'title', 'body');
}
#[Server]
public function addComment(): void
{
$this->validate(['comment' => 'required|max:1000']);
Post::findOrFail($this->post['id'])->comments()->create(['body' => $this->comment, 'user_id' => auth()->id()]);
$this->reset('comment');
}
}
mount(Post $post)runs once, on the server, with the bound model. It stores what the view needs in properties. Here the post is kept as an array and marked#[Locked], so the browser can't change it.addComment()is a#[Server]method. It saves the comment with Eloquent and then clears the field.- Everything else on the page runs in the browser, such as typing the comment or showing and hiding parts of the page.
Choosing a layout
A page renders inside its layout as $slot, and its title is available as
$title. If a page has no #[Layout], it uses the layout set in
nitro.layout, which is components.layouts.app by default. Your layout needs
@nitroHead in its <head> and @nitroScripts before
</body>:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@nitroHead
</head>
<body>
{{ $slot }}
@nitroScripts
</body>
</html>
When a nitro:navigate visit goes to a page with the same layout, the layout stays and only the page changes. A sidebar keeps its scroll position, and a player keeps playing.
#[Layout] takes the layout's data as its second argument, as variables in the layout:
#[Layout('components.layouts.app', ['section' => 'billing'])]. A page whose layout has other
data counts as another layout.
Layouts inside layouts
A section of your app, such as settings or an account area, often has its own layout around its pages (a
sub-navigation, a header) inside the app's layout. Write it as a layout that uses the app's, and mark where
each layout's pages go with nitro:layout and a name:
<body>
<aside class="sidebar">...</aside>
{{-- Where this layout's pages go. --}}
<main nitro:layout="app">
{{ $slot }}
</main>
@nitroScripts
</body>
{{-- The settings section: its own sub-navigation, inside the app's layout. --}}
<x-layouts.app :title="$title">
<div class="settings">
<nav>
<a href="/settings/profile" nitro:navigate nitro:current="active">Profile</a>
<a href="/settings/billing" nitro:navigate nitro:current="active">Billing</a>
</nav>
{{-- Where the settings pages go. --}}
<section nitro:layout="settings">
{{ $slot }}
</section>
</div>
</x-layouts.app>
A page in the section names its layout:
use Nitro\Attributes\Layout;
use Nitro\Attributes\Title;
#[Layout('components.layouts.settings')]
#[Title('Billing')]
class Billing extends Component
A visit then changes only the innermost layout region the two pages share:
| A visit | Changes |
|---|---|
| Between two settings pages | The settings page. The sub-navigation and the sidebar stay (the same layout) |
| From a dashboard page to a settings page, or back |
What is inside
nitro:layout="app"
. The sidebar stays
|
To a page in a layout without
nitro:layout="app"
|
The whole page |
- What stays, stays as it was. Outside the region, nothing is rendered again: a sidebar keeps its scroll position and what was typed in it, its components keep their state, and its scripts don't run again.
- What changes starts afresh. Inside the region, the new page's HTML takes the place of the old, and its components start, as on a page load.
- Names, in the same nesting. Regions match by their names, outermost first, each inside the one before it. The first name the two pages don't share ends the match: the region before it is the one that changes.
- Optional. Between pages of one layout, a visit keeps the layout without
nitro:layout. You only need it where a visit crosses from one layout to another that shares a part of it.
Mark the app's layout once
Put nitro:layout="app" around $slot in your app's layout even before you have a nested one. Every layout built on it then shares the sidebar on a visit, with nothing else to do.
Choosing between a component and a page
Use this table to decide which one to write:
| You need | Write |
|---|---|
| A whole screen, with a URL | A page |
| A part placed in a view (a search box, a cart, a table) | A component |
| Loads data from the route |
A page, in
mount()
|
| Used on several screens | A component, placed on each |
You can't place a page inside a view, and you can't route a component. If you try, Nitro's error message tells you what to use instead.
Loading a page's code
Nitro compiles each page into a chunk of its own. The browser loads that chunk the first time someone
visits the page, so a large application doesn't send every page up front. You can change this with
#[LazyLoad]:
use Nitro\Attributes\LazyLoad;
#[LazyLoad(false)] // In the main bundle, so the first visit has no chunk to load.
class Dashboard extends Component
#[LazyLoad('reports')] // One chunk for pages and components that are used together.
class Revenue extends Component
This page
The docs you are reading are a single Nitro page, Docs\Show, routed as
/docs/{page}. Its mount() method renders the page's content on the server and
sets its head. Its view places the live examples as real components.
<?php
namespace App\Nitro\Pages\Docs;
use App\Docs\Content;
use App\Docs\Nav;
use Nitro\Attributes\Layout;
use Nitro\Attributes\Locked;
use Nitro\Component;
/**
* A docs page. The server renders its content with App\Docs\Content. The page also knows its
* place in the sidebar, and sets the head that search engines and link previews read.
*/
#[Layout('layouts.docs')]
class Show extends Component
{
#[Locked]
public string $slug = '';
#[Locked]
public string $title = '';
#[Locked]
public string $section = '';
#[Locked]
public string $lead = '';
/** The page's HTML blocks and live examples, in order. */
#[Locked]
public array $blocks = [];
#[Locked]
public ?array $prev = null;
#[Locked]
public ?array $next = null;
public function mount(string $page): void
{
$meta = Nav::page($page) ?? abort(404);
$content = Content::render($page);
$this->slug = $page;
$this->title = $meta['title'];
$this->section = $meta['section'];
$this->lead = $content['lead'];
$this->blocks = $content['blocks'];
['prev' => $this->prev, 'next' => $this->next] = Nav::neighbours($page);
$this->head()
->title($meta['title'])
->description($this->lead !== '' ? $this->lead : "{$meta['title']}: {$meta['section']} in the Nitro documentation.")
->canonical(url(Nav::url($page)));
}
}
<article class="mx-auto max-w-[46rem]">
<p class="mb-3 text-sm font-medium"><span class="nitro-text">{{ $section }}</span></p>
<h1 class="text-[2rem] leading-tight font-semibold tracking-tight text-slate-800 sm:text-4xl dark:text-slate-200">{{ $title }}</h1>
@if ($lead !== '')
<p class="mt-4 text-lg leading-8 text-slate-600 dark:text-slate-400">{{ $lead }}</p>
@endif
{{-- On small screens: "On this page" under the title (filled by resources/js/app.js). --}}
<details class="group mt-6 rounded-xl ring-1 ring-slate-200 xl:hidden dark:ring-white/10">
<summary class="flex cursor-pointer list-none items-center justify-between px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300">
On this page
<svg class="size-4 transition group-open:rotate-180" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m4 6 4 4 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</summary>
<nav data-toc class="border-t border-slate-200 px-4 py-3 text-sm dark:border-white/10"></nav>
</details>
<div class="prose-docs mt-10" data-content>
@foreach ($blocks as $i => $block)
@if (isset($block['html']))
{!! $block['html'] !!}
@else
{{-- A live example: a real component, running in the page. --}}
<div class="not-prose relative my-8 overflow-hidden rounded-2xl ring-1 ring-slate-200 dark:ring-white/10">
<div class="pointer-events-none absolute inset-x-0 -top-px h-px nitro-gradient opacity-70"></div>
<div class="flex items-center gap-2 border-b border-slate-200/80 bg-slate-50/80 px-4 py-2.5 dark:border-white/[0.07] dark:bg-white/[0.02]">
<span class="relative flex size-2">
<span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-60 motion-reduce:animate-none"></span>
<span class="relative inline-flex size-2 rounded-full bg-emerald-500"></span>
</span>
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">{{ $block['title'] }}</span>
<span class="text-xs text-slate-400 dark:text-slate-500">live, in this page</span>
@if ($block['source'])
<a href="{{ $block['source'] }}" class="ml-auto text-xs font-medium text-slate-500 hover:text-browser-600 dark:hover:text-browser-400">View source ↓</a>
@endif
</div>
<div class="bg-[radial-gradient(var(--color-slate-200)_1px,transparent_1px)] bg-[length:16px_16px] px-6 py-8 dark:bg-[radial-gradient(var(--color-slate-800)_1px,transparent_1px)]">
<nitro:dynamic-component :is="$block['component']" :key="$slug.'-'.$i" />
</div>
</div>
@endif
@endforeach
</div>
{{-- Previous / next --}}
<nav class="mt-16 grid gap-4 border-t border-slate-200 pt-8 sm:grid-cols-2 dark:border-white/10">
@if ($prev)
<a nitro:navigate nitro:prefetch href="/docs/{{ $prev['slug'] }}" rel="prev" class="group rounded-xl p-4 ring-1 ring-slate-200 transition hover:ring-slate-300 dark:ring-white/10 dark:hover:ring-white/20">
<span class="text-xs text-slate-500">← Previous</span>
<span class="mt-1 block font-semibold text-slate-900 group-hover:text-browser-600 dark:text-white dark:group-hover:text-browser-400">{{ $prev['title'] }}</span>
</a>
@endif
@if ($next)
<a nitro:navigate nitro:prefetch href="/docs/{{ $next['slug'] }}" rel="next" class="group rounded-xl p-4 text-right ring-1 ring-slate-200 transition hover:ring-slate-300 sm:col-start-2 dark:ring-white/10 dark:hover:ring-white/20">
<span class="text-xs text-slate-500">Next →</span>
<span class="mt-1 block font-semibold text-slate-900 group-hover:text-browser-600 dark:text-white dark:group-hover:text-browser-400">{{ $next['title'] }}</span>
</a>
@endif
</nav>
<div class="mt-8 flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500">
<a href="https://github.com/nitroframework/docs/edit/main/resources/views/docs/content/{{ $slug }}.blade.php" class="inline-flex items-center gap-1.5 hover:text-slate-900 dark:hover:text-white">
<svg class="size-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10.5 2.5 13.5 5.5 6 13H3v-3l7.5-7.5Z" stroke-linejoin="round"/></svg>
Edit this page on GitHub
</a>
</div>
</article>
<?php
use App\Nitro\Pages\Docs\Show;
use App\Http\Controllers\CompiledController;
use App\Http\Controllers\RobotsController;
use App\Http\Controllers\SearchIndexController;
use App\Http\Controllers\SitemapController;
use Illuminate\Support\Facades\Route;
Route::view('/', 'home');
/** For search engines: every page (App\Docs\Nav), and where that list is. */
Route::get('/sitemap.xml', SitemapController::class);
Route::get('/robots.txt', RobotsController::class);
/** Islands only exist in a plain Blade view: the Islands page's demo. */
Route::view('/demo/islands', 'demo.islands');
/** A component's compiled JavaScript, for a "Compiled" panel as it opens (App\Docs\Code::compiled()). */
Route::get('/docs/compiled/{component}', CompiledController::class)->where('component', '[a-z0-9:.-]+');
/** What ⌘K searches (App\Docs\SearchIndex). */
Route::get('/docs/search.json', SearchIndexController::class);
/** The docs: one Nitro page per sidebar entry (config/docs.php); the ones not written yet show a placeholder. */
Route::redirect('/docs', '/docs/introduction');
Route::nitro('/docs/{page}', Show::class)->where('page', '[a-z0-9-]+');