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, routes/api.php, routes/console.php and routes/channels.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 at /; the public pages once per language, such as /en/features and /es/caracteristicas, the demo, the public add-ons and the legal texts, plus the addresses without a language that redirect to them; the help centre under /{locale}/help; /sitemap.xml, /robots.txt, /locale/{locale}, /logs, /offline, the web push endpoints and the direct upload endpoints under /media-uploads |
routes/auth.php |
web |
none | none | Sign-in, registration, password reset and verification, /logout, /stop-acting, /privacy and /terms (which redirect to the reader's language), and with auth and security: the account area under /account and the support team's pages under /admin/support (admin.support.*: the inbox, each ticket, chats and assistant), inside the admin panel but outside its role, each guarded by its own module switch and permission |
routes/admin.php |
web, auth, security, role: with config('app.admin_role') |
admin |
admin. |
The admin dashboard, the Laravel Front CRUDs, categories/{type}, dev, design, space, the media folders under media and webhook-events |
routes/app.php |
web, auth, security, and ensure.subscribed when billing is on |
app |
app. |
The product. The kit ships the dashboard at /app, the media library at /app/media, the space page at /app/space and categories/{type} |
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 | |||
routes/channels.php |
The private broadcasting channels; see Real time with Reverb |
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, and a seventh when the billing package is on.
| 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 |
EnsureHasCountry |
Only with billing on, and only while plans are live: a signed-in account with no country is given the detected one, and when none can be detected every page navigation outside the admin panel, the profile and the sign-out and verification pages sends it to its profile with a warning to choose one |
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 layouts/app.blade.php, found through the design layers like layouts.app; 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
Every layout lives in a layouts/ folder inside a design folder and is found through the same chain as the design components: resources/views/components/your-project/layouts/ first, the generic resources/views/components/project/layouts/ last, and the first one that has it wins. It is still called by its name, layouts.app or layouts.sidebars.admin; Make it your own explains how to change one in your folder.
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 section no folder has a sidebar for shows none. 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. A screen that should start its trail somewhere else passes the layout a title for that first link and a url for it, as the support screens under /admin/support do so their trail starts at their own section.
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. resources set to false leaves the Laravel Front resources out, as the admin sidebar does for someone without the admin role, and an item may carry its own active, which wins over the one worked out from its URL, for an entry whose address prefixes a sibling's.
To add a page to the product sidebar, edit layouts/sidebars/app.blade.php in your design folder, copied from the generic one the first time:
<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, whenever the page is being unloaded (a full reload, a form posted the classic way; the browser's beforeunload and pagehide events) and while a form with wire:submit is being sent. Navigations also draw a thin progress bar at the top. 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. The attribute only counts on an element that has wire:click or wire:target itself; on a wrapper around the button it does nothing:
<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; the overlay then stays while the file uploads.
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, or to the same public page in the chosen language when the link names one. <x-language-switcher> renders one link per language through locale_url(), only for guests outside the help centre; in the help centre it shows to everyone and links each language's copy of the page directly. See Languages and translations.