laravel/folio
Laravel Folio is a page-based router for Laravel that lets you define routes by creating files, keeping routing simple and organized. Ideal for building pages quickly with less boilerplate, backed by official Laravel documentation and support.
Installation:
composer require laravel/folio
php artisan folio:install
This creates a pages table, publishes migrations, and sets up a default routes/folio.php file.
Define a Page:
Create a migration for a pages table (or use the published one) and add a record:
// database/migrations/xxxx_create_pages_table.php
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('path')->nullable();
$table->text('view');
$table->timestamps();
});
Insert a test page:
INSERT INTO pages (slug, path, view) VALUES ('about', 'about', 'pages.about');
First Route:
In routes/folio.php, define the route:
Route::get('/', 'pages.home');
Route::folio('pages/{page}', 'pages.show')->name('pages.show');
Create a view at resources/views/pages/show.blade.php.
Access the Page:
Visit /about to see the page rendered. Folio automatically resolves the route based on the slug and view fields.
Create a CMS-like system for marketing pages:
Model:
php artisan make:model Page -m
Update the migration to include slug, path, and view.
Controller:
namespace App\Http\Controllers;
use App\Models\Page;
class PageController extends Controller
{
public function show(Page $page)
{
return view($page->view, ['page' => $page]);
}
}
Route:
Route::folio('pages/{page}', [PageController::class, 'show'])->name('pages.show');
View:
<!-- resources/views/pages/show.blade.php -->
<h1>{{ $page->title }}</h1>
{!! $page->content !!}
Admin Panel:
Use Laravel Nova or a custom form to manage pages records.
| Command | Description |
|---|---|
php artisan folio:install |
Publishes migrations and config. |
php artisan folio:list |
Lists all registered Folio routes. |
php artisan folio:make |
Generates a new Folio route (e.g., php artisan folio:make about). |
Folio replaces static route definitions with dynamic ones tied to a database table. This is ideal for:
// routes/folio.php
Route::domain('{locale}.example.com')->folio('pages/{page}', [PageController::class, 'show'])
->middleware('locale');
Configure middleware to set the app locale:
// app/Http/Middleware/SetLocale.php
public function handle(Request $request, Closure $next)
{
$locale = $request->domain();
app()->setLocale($locale);
return $next($request);
}
Folio automatically resolves views based on the view field in the database. Use this pattern:
Nested Views:
Store views like pages.blog.post for a post at /blog/{slug}.
Route::folio('blog/{post}', [PostController::class, 'show'])
->where('post', '.*'); // Wildcard for dynamic slugs
Fallback Views:
Use a default view if view is null:
Route::folio('fallback', function () {
return view('pages.default');
});
Folio integrates with Laravel’s route helpers (route(), back(), etc.) and adds its own:
Named Routes:
Route::folio('contact', [ContactController::class, 'show'])->name('contact');
Generate URLs:
route('contact'); // /contact
Route Testing:
Use routeIs() in Blade:
<a href="{{ route('contact') }}" class="{{ request()->routeIs('contact') ? 'active' : '' }}">
Contact
</a>
Apply middleware to Folio routes:
Route::folio('admin/{page}', [AdminPageController::class, 'show'])
->middleware(['auth', 'verified']);
Use terminable middleware for Folio-specific logic:
// app/Http/Middleware/LogFolioAccess.php
public function terminate(Request $request, $response)
{
if ($request->routeIs('folio.*')) {
Log::info('Folio route accessed: '.$request->path());
}
}
Folio supports wildcard directories for modular routing:
resources/
views/
pages/
blog/
post.blade.php
about.blade.php
Define routes in routes/folio.php:
Route::folio('pages/{page}', [PageController::class, 'show'])
->where('page', '^(?!index$).+$'); // Exclude 'index' slugs
Generate URLs dynamically from a Page model:
// app/Models/Page.php
public function getUrlAttribute()
{
return route('pages.show', $this->slug);
}
Usage:
<a href="{{ $page->url }}">{{ $page->title }}</a>
Use Laravel’s testing helpers:
public function test_folio_route()
{
$page = Page::factory()->create(['slug' => 'test', 'view' => 'pages.test']);
$response = $this->get('/test');
$response->assertViewIs('pages.test');
}
Test route names:
$this->assertRouteIs('pages.show', '/test');
Route Caching Conflicts:
Folio routes are cached with route:cache. Clear the cache after adding new routes:
php artisan route:clear
php artisan folio:list # Verify routes are registered
Slug Collisions:
If two pages share the same slug but different path, Folio prioritizes the first match. Use path to disambiguate:
-- Page 1: /about (highest priority)
INSERT INTO pages (slug, path, view) VALUES ('about', 'about', 'pages.about');
-- Page 2: /team/about (lower priority)
INSERT INTO pages (slug, path, view) VALUES ('about', 'team/about', 'pages.team.about');
Wildcard Overreach:
Avoid overly broad wildcards (e.g., .*) in where() clauses. Test edge cases:
// Bad: Matches everything, including non-existent paths
Route::folio('{page}', [PageController::class, 'show'])->where('page', '.*');
// Good: Explicit pattern
Route::folio('{page}', [PageController::class, 'show'])->where('page', '[a-z0-9\-]+');
View Not Found:
If Folio returns a 404 for a valid slug, check:
view field exists in the database.resources/views/{view}.blade.php.pages.about vs. page.about).Middleware Order: Folio middleware runs after global middleware. Ensure critical middleware (e.g., auth) is applied directly to Folio routes:
Route::folio('admin/{page}', [AdminController::class, 'show'])
->middleware('auth:support'); // Runs after global middleware
List Routes:
php artisan folio:list
Output:
+--------+-----------+----------------+---------------------+---------------------+
| Domain | Method | URI | Controller | Route Name |
+--------+-----------+----------------+---------------------+---------------------+
| null | GET|HEAD | pages/{page} | App\Http\Controllers\PageController@show | pages.show |
+--------+-----------+----------------+---------------------+---------------------+
Check Route Resolution:
Use dd() in a middleware to inspect the resolved route:
public function handle(Request $request,
How can I help you explore Laravel packages today?