Loading...

This is taking longer than expected.

Back to the help centre

Routes, layouts and middleware

Where each route file is loaded, what runs on every request, and how pages, sidebars and the loading overlay fit together.

Your first page needs three decisions: which route file it goes in, which layout it renders into and which sidebar entry opens it. This guide answers the three, and lists the middleware that already runs on every request so you do not add it twice.

Route files

bootstrap/app.php wires the files. Laravel loads routes/web.php and routes/api.php itself; the rest are loaded in the then: callback with the middleware, prefix and name below.

File Middleware Prefix Name prefix What lives there
routes/web.php web none none The landing page, /features, the help centre, /locale/{locale}, /logs, /offline and the web push endpoints
routes/auth.php web none none Sign-in, registration, password reset and verification, /logout, /stop-acting, /privacy, /terms, and the account area under /account with auth and security
routes/admin.php web, auth, security, role:admin admin admin. The admin dashboard, the Laravel Front CRUDs, categories/{type}, dev and webhook-events
routes/app.php web, auth, security, and ensure.subscribed when billing is on app app. The product. The kit ships only the dashboard at /app
routes/api.php api api none POST /api/stripe/webhook, behind webhook and log.webhook:stripe
routes/console.php Artisan closures and the schedule; see Queues and scheduled work

The health check answers at /up.

ensure.subscribed is added to /app only when the billing package is installed and BILLING_CORE_ENABLED and BILLING_USER_ENABLED are not false; without billing, the alias does not exist and a signed-in person reaches /app with no subscription. See Turn on plans and billing.

Add your own routes

Product pages go in routes/app.php; they inherit the /app prefix and the app. name. Register a Livewire component as a full page:

use App\Livewire\App;

Route::livewire('/orders', App\Orders\Index::class)->name('orders');
Route::livewire('/orders/{order}', App\Orders\Show::class)->name('orders.show');

That is /app/orders, named app.orders. config('app.home_route') is /app, the place /home redirects to and the place the sign-in flow lands on; change it in config/app.php when the product starts somewhere else. Admin CRUDs go in routes/admin.php as Route::front('Order'), which registers every route of the App\Front\Resources\Order resource; see The admin panel.

Middleware on every web request

bootstrap/app.php appends six classes to the web group, in this order.

Middleware What it does
LocaleMiddleware Sets the language: the signed-in account's locale, else the visitor's session choice, else the first browser language the app publishes, else the fallback
RememberLastUrl Stores the URL of every plain GET page in the session as last_non_livewire_url, which lastUrl() reads and error reports include
CaptureReferralCode On a plain GET, remembers a referral code from the query string or restores it from the cookie; see Referrals
SystemValidations For a signed-in person, sets the warnings the layout shows at the top of the page: the missing PIN when config('auth.enable_pin') is on and, while billing is on, a failed payment, an outstanding balance or an account left without a plan
TrackingMiddleware When config('features.tracking_enabled') is on, opens a visitor tracking session on the first plain GET or when fbclid or utm_campaign arrive
UpdateLastLoggedAt Stamps last_logged_at on the signed-in account once a day

All of them skip Livewire update requests, so they run once per page, not once per interaction.

trustProxies(at: '*') trusts every proxy, so the scheme and client IP come from the forwarded headers of the load balancer in front of the app.

Aliases

Alias Class
security App\Http\Middleware\Security: sends a person who must change their password to /password/request and signs out a blocked account
role, permission, role_or_permission Spatie's permission middleware; see Roles and permissions
log.webhook App\Http\Middleware\LogWebhookEvent: stores each incoming webhook, its response and its error as a row; see Audit trails and webhook events
webhook Cashier's VerifyWebhookSignature
ensure.subscribed The billing package's EnsureHasSubscription, present only when billing is on

App\Http\Middleware\IsPWABuilder is attached to / alone: when APP_PWA is on and the request comes from the wrapped mobile app, it redirects to /app instead of showing the landing page.

Livewire conventions

config/livewire.php fixes where things go.

Setting Value Meaning
class_namespace App\Livewire Components are classes under app/Livewire/, grouped as Admin, App, Auth, Shared and Web
view_path resources/views/livewire Their views mirror that tree
component_layout layouts::app The layout a full-page component renders into unless it calls ->layout()
component_namespaces layouts, pages layouts::app is resources/views/layouts/app.blade.php; pages:: points at resources/views/pages/
make_command.type class php artisan make:livewire creates a class plus a view, not a single-file component
legacy_model_binding true wire:model="post.title" binds straight to a model attribute
navigate.progress_bar_color #2299dd The colour of the thin bar shown during wire:navigate

Layouts

resources/views/layouts/base.blade.php is the HTML shell: the <head> with the title, favicon, Vite assets and WireUI scripts, then <x-notifications>, <x-dialog>, the loading overlay, the content-base section and the modal host. The other layouts extend it:

Layout Used by
layouts/app.blade.php Every signed-in page: sidebar, top bar with notifications, announcements and the account menu, breadcrumbs, flash messages and the system warnings
layouts/auth.blade.php Sign-in, registration and the other guest forms, with no chrome
layouts/web.blade.php The landing and the public pages, with the marketing navigation
layouts/help.blade.php The help centre

The public and help layouts name the browser tab after the page: pass title to layouts.web or layouts.help and the tab reads Title - App name. Without one, the public layout shows only the app name and the help layout Help centre.

An address that does not exist renders resources/views/errors/404.blade.php inside layouts.auth: a translated We could not find that page with a Back to home button. The button leads a visitor to /, an administrator to the admin dashboard and any other signed-in user to the app dashboard. Edit that file to change the page; a 404 your code throws with abort(404) shows it too.

The app layout picks its sidebar from the first URL segment: layouts/sidebars/app.blade.php under /app, admin.blade.php under /admin, account.blade.php under /account. A component renders breadcrumbs by passing them to the layout:

public function render()
{
    return view('livewire.app.orders.show')->layout('layouts.app', [
        'breadcrumb' => [
            ['label' => __('Orders'), 'url' => '/app/orders'],
            ['label' => $this->order->number],
        ],
    ]);
}

layouts/partials/breadcrumbs.blade.php prints a home link to the section root and one entry per item; the last one is the current page and has no URL.

Add a sidebar entry

Each sidebar renders <x-design::sidebar-menu>, backed by App\Classes\SidebarMenu. It merges the items you write in the Blade file with the Laravel Front resources of the section that answer true to showOnMenu() and pass the viewAny policy for the signed-in person, sorts everything by order, and groups by menu_group. Items with no group form the ungrouped block at the top.

To add a page to the product sidebar, edit layouts/sidebars/app.blade.php:

<x-design::sidebar-menu
    :items="[
        ['name' => 'Dashboard', 'url' => '/app', 'icon' => 'home', 'exact' => true, 'order' => 1],
        ['name' => 'Orders', 'url' => '/app/orders', 'icon' => 'shopping-bag', 'order' => 2],
        ['name' => 'Reports', 'url' => '/app/reports', 'icon' => 'chart-bar', 'menu_group' => 'Analytics', 'show' => auth()->user()->can('viewAny', Report::class)],
    ]"
    :groups="[
        ['name' => 'Analytics', 'order' => 1],
    ]"
/>
Key Meaning
name Passed through __(), so add it to lang/
url Marked active when the current URL is it or starts with it, unless exact is true
icon A Heroicon name
order Position; items without one go last
show Hides the item when false
menu_group Puts the item under a collapsible heading; groups orders the headings

A Front resource declares its own menu_group, menu_order and icon, so a CRUD appears in the admin sidebar without touching the Blade file.

The loading overlay

resources/views/components/project/loading-overlay.blade.php is a full-screen spinner mounted once in the base layout and driven by resources/js/app.js. It shows itself on wire:navigate navigation, on plain link clicks that leave the page and while a form with wire:submit is being sent. After 3 seconds it adds a message and a Reload button, so a stuck request always has a way out.

For a button that is not a form submit, opt in with the spinner attribute on the element that carries wire:click:

<x-button wire:click="retryPayment" spinner :label="__('Retry payment')" />

For a file upload, put spinner on the <input type="file"> itself, not on the button that opens the picker, because the request starts on the input's change.

Do not add spinner to wire:model.live fields, filters, tabs or searches: the overlay blocks the whole screen and those should feel instant.

The language switch

GET /locale/{locale}, named locale, checks the code against the keys of config('app.languages') and answers 404 for anything else. For a visitor it stores the choice in the session, which LocaleMiddleware reads on the next request; for a signed-in person it writes nothing, because their language comes from the profile. Either way it redirects back. <x-language-switcher> renders one link per language through locale_url() and only for guests. See Languages and translations.