Nitro

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:

routes/web.php
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:

app/Nitro/Pages/Posts/Show.php
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>:

resources/views/components/layouts/app.blade.php
<!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:

resources/views/components/layouts/app.blade.php
<body>
    <aside class="sidebar">...</aside>

    {{-- Where this layout's pages go. --}}
    <main nitro:layout="app">
        {{ $slot }}
    </main>

    @nitroScripts
</body>
resources/views/components/layouts/settings.blade.php
{{-- 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:

app/Nitro/Pages/Settings/Billing.php
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]:

PHP
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.

Browser + server
<?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)));
    }
}
routes/web.php
<?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-]+');

Next steps