laravel/folio
Laravel Folio is a powerful page-based router for Laravel applications. It simplifies routing by mapping Blade views to routes automatically, letting you build pages with a clean, file-based structure instead of manual route definitions.
Install and scaffold.
composer require laravel/folio
php artisan folio:install
The install command creates a resources/views/pages directory (and wires up whatever it needs). You then mount that directory onto a URI — either in routes/web.php or AppServiceProvider::boot():
use Laravel\Folio\Folio;
Folio::path(resource_path('views/pages'))->uri('/');
First page. Create resources/views/pages/index.blade.php:
<x-layouts.app>
<h1>Hello from Folio</h1>
</x-layouts.app>
Hit / and you're done — no route file entry, no controller, no closure. Every .blade.php under pages/ becomes a GET route derived from its path.
Where to look first, in order:
php artisan folio:list — the single most useful command. It prints URI, route name, and the backing file for every page Folio registered. Run it after every routing change.php artisan folio:page users/index (or php artisan list folio to confirm the exact command name on your version) — scaffolds a page file in the right place.laravel.com/docs/folio — the README is intentionally a stub; the real documentation lives there.vendor/laravel/folio/src — Folio.php, PendingFolioRoute.php, and the page-resolution pipeline are small and readable when something doesn't match.First real use case: a marketing site, docs site, or blog index. Folio is at its best when URLs are "one page per file" and mostly GET: /about, /pricing, /blog/[slug], /docs/[...path]. Do not migrate an existing CRUD-heavy app into Folio wholesale.
| File | URI |
|---|---|
pages/index.blade.php |
/ |
pages/about.blade.php |
/about |
pages/blog/index.blade.php |
/blog |
pages/blog/[Post].blade.php |
/blog/{post} |
pages/users/[User]/edit.blade.php |
/users/{user}/edit |
pages/docs/[...path].blade.php |
/docs/{path} (catch-all wildcard) |
[Post] (capitalized) triggers implicit route-model binding and type-hints the model in the render closure; [post] (lowercase) is a plain string parameter. Keep the convention consistent across a project or you will chase 404s.
@php header@php
use function Laravel\Folio\{middleware, name, render};
middleware(['auth', 'verified']);
name('dashboard');
@endphp
<x-layouts.app>...</x-layouts.app>
middleware() and name() must be called inside the @php block. Named routes then work with route('dashboard'), and request()->routeIs('dashboard') works for nav highlighting.
render() for data and alternate viewsKeep Blade files thin; push queries into the render callback, which receives the bound models after resolution:
@php
use function Laravel\Folio\render;
render(function (View $view, User $user) {
return $view->with('posts', $user->posts()->latest()->paginate(10));
});
@endphp
@foreach ($posts as $post)
...
@endforeach
Returning something other than the page view (a redirect, a different view) also works from here — useful for gating logic that isn't authorization.
Split concerns by directory instead of repeating middleware('auth') on every page:
Folio::path(resource_path('views/pages'))->uri('/');
Folio::path(resource_path('views/pages/admin'))->uri('/admin')->middleware(['auth', 'can:admin']);
Folio::path(resource_path('views/pages/tenant'))->domain('{tenant}.example.com')->uri('/');
Overlapping paths across domains had a silent route-dropping bug — fixed in v1.1.11. Pin at least that version if you use domains.
Folio is GET-only. The everyday split:
routes/web.php / controllers for POST/PUT/PATCH/DELETE, webhooks, and anything needing a named controller action.@livewire('post-editor') inside the page, so submission happens over Livewire's endpoint, not a Folio route.Folio doesn't impose a layout system. Blade components (<x-layouts.app>) or classic @extends('layouts.app') both work, since the rendered page is just a view. Use whatever the rest of the app uses.
Folio pages are ordinary routes, so ordinary feature tests work:
$this->get('/users/1')->assertOk()->assertSee('Jane');
$this->get('/admin')->assertRedirect('/login');
Use $this->get('/dashboard')->assertSeeLivewire(...) where relevant, and withoutVite() in tests that render a layout referencing @vite.
GET only. There is no way to define a POST route as a Folio page. This is the first wall people hit. Reach for Livewire/Volt or a conventional controller route; don't fight the router.
Route caching is mandatory in production — and easy to forget. Folio pages are compiled into the route cache. New pages added in a deploy are invisible until php artisan route:cache reruns. If a route "doesn't exist" on staging but works locally, that is almost always the cause. Run php artisan optimize:clear while debugging; run route:cache in the deploy script.
Wildcards are greedy. A root-level [...slug].blade.php will shadow everything. Scope catch-alls into subdirectories (docs/[...path]) and confirm the final ordering with php artisan route:list — Folio registers concrete pages and wildcards in a defined order, and a stray catch-all silently wins.
Filesystem case-sensitivity. [user] and [User] differ semantically (string vs. model binding) but collide on Windows and default macOS volumes. Folio's own history includes Windows path fixes. Normalize on capitalized model parameters everywhere.
Naming collisions. Route names are auto-derived from the path, which can collide when you have both /blog and /blog/index. Set explicit name() on index/parent pages and treat folio:list as the source of truth. The route() helper's handling of base URI, domain, and query params was only fully fixed in the 1.0.0-beta.6 era — if you're on a pre-1.0 pin, upgrade.
Slugs beginning with "index". Fixed in v1.1.6; earlier versions mangled /index-of-something-style routes. Another reason to stay on a current 1.1.x.
Don't stack Folio and Volt @php headers in one file. Both parse a leading @php block for directives. If a page is a Volt component, let Volt own the header; if it's a Folio page, don't add Volt's use function lines into the same block.
Debugging workflow, in order: folio:list → route:list → check the @php block for a swallowed exception (a failing name()/middleware() call shows as a 404, not a 500) → check the route cache.
Extension points worth knowing:
Laravel\Folio\Events\ViewMatched — fires when a page is matched. Hook analytics, audit logging, or per-page policy checks without touching page files.Folio::path(...)->middleware(...) — mount-level middleware beats per-page repetition, and terminable middleware is supported.php artisan folio:list — also useful as a CI assertion that a page you expect actually registered.Version floor. v1.1.19 fixed route caching breaking Folio and v1.1.11 fixed domain/overlap route loss; 1.1.14+ adds Laravel 13 compatibility, and 1.1.12/1.1.16 cover PHP 8.5 and Symfony 8. There's no 2.x yet — treat 1.1.x as the line and pin ^1.1.19 or newer.
Team-fit note. Folio's benefit is proportional to how "page-shaped" your URLs are. On a content site it removes a whole layer of boilerplate; on a resource-heavy CRUD app it mostly relocates routing into filenames and makes route discovery harder for people who don't know the convention. Run folio:list in onboarding docs, or new hires will be grepping routes/web.php for routes that don't exist there.
How can I help you explore Laravel packages today?