Laravel Folio & Volt: File-Based Routing & Livewire Components
Master Laravel Folio for file-based routing and Volt for single-file Livewire components.
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
- ✓Basic knowledge of Laravel (routes, Blade templates)
- ✓Familiarity with Livewire (components, directives)
- ✓PHP 8.1+ and Laravel 10+ installed
- Laravel Folio enables file-based routing: create a Blade file and it automatically becomes a route.
- Volt allows writing Livewire components in a single file with template, logic, and styles.
- Combine Folio and Volt to build pages with dynamic behavior without writing routes or controllers.
- Ideal for rapid prototyping and simple pages, but can scale with directory-based routing and nested components.
- Both tools are part of Laravel's ecosystem and work seamlessly with existing Laravel features.
Imagine you have a filing cabinet. Normally, you need a map (routes) to find each file. Folio removes the map: just put a file in the right drawer, and it's instantly accessible. Volt is like having a self-contained folder that includes instructions, design, and styling all in one place, so you don't have to search through multiple folders to understand a single page.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Building a Laravel application traditionally involves defining routes in web.php, creating controllers, and then crafting Blade templates. While this MVC pattern is powerful, it can feel cumbersome for simple pages or rapid prototyping. Enter Laravel Folio and Volt—two packages that streamline development by embracing file-based conventions.
Folio allows you to create routes simply by placing Blade files in a pages directory. No more route definitions, no controllers for static or simple dynamic pages. Volt takes this a step further by letting you write Livewire components in a single file, combining template, logic, and even styles. Together, they form a powerful duo for building Laravel applications with minimal boilerplate.
In this tutorial, you'll learn how to install and configure Folio and Volt, create file-based routes, build dynamic Volt components, and combine them to create interactive pages. We'll cover real-world use cases like a blog with pagination, form handling, and authentication. You'll also see how to debug these components in production and avoid common pitfalls. By the end, you'll be able to rapidly build Laravel applications with clean, maintainable code.
Installing and Configuring Folio and Volt
Before diving into file-based routing and single-file components, you need to install the required packages. Laravel Folio and Volt are first-party packages that can be installed via Composer. Ensure you have a Laravel application (version 10 or later recommended).
First, install Folio: ``bash composer require laravel/folio ``
Then, install Volt: ``bash composer require livewire/volt ``
After installation, you need to publish the configuration and assets. For Folio, run: ``bash php artisan folio:install ` This command creates a resources/views/pages directory and registers Folio's service provider. It also adds a FolioServiceProvider to your config/app.php` if not already present.
For Volt, run: ``bash php artisan volt:install ` This publishes the Volt configuration file (config/volt.php`) and creates a sample component. It also registers the Volt service provider.
Now, you can start using Folio and Volt. Folio automatically maps any Blade file in resources/views/pages to a route. For example, creating resources/views/pages/dashboard.blade.php will make it accessible at /dashboard. Volt components are placed in resources/views/livewire and can be included in any Blade view using <livewire:component-name />.
To verify the installation, create a simple Folio page and a Volt component. We'll do that in the next sections.
resources/views/pages and resources/views/livewire directories are included in your deployment process. Also, run php artisan view:cache to cache Blade views for better performance.Creating Your First Folio Page
Folio makes routing as simple as creating a Blade file. Let's create a homepage and an about page.
Create a file resources/views/pages/index.blade.php with the following content: ``blade <!DOCTYPE html> <html> <head> <title>Home</title> </head> <body> <h1>Welcome to My Site</h1> <p>This is the homepage.</p> </body> </html> ``
Now, navigate to http://your-app.test/. You should see the homepage. Folio automatically maps index.blade.php to the root URL.
Next, create resources/views/pages/about.blade.php: ``blade <!DOCTYPE html> <html> <head> <title>About</title> </head> <body> <h1>About Us</h1> <p>We are a company that loves Laravel.</p> </body> </html> ``
Visit /about and you'll see the about page. No routes, no controllers—just Blade files.
Folio also supports nested directories. Create a directory resources/views/pages/blog and inside it create index.blade.php and post.blade.php. The index.blade.php will be accessible at /blog, and post.blade.php at /blog/post. This allows you to organize pages logically.
You can also use route parameters. For example, create resources/views/pages/users/[user].blade.php. The [user] part becomes a route parameter. Access the parameter in your Blade file using the $user variable (the name matches the bracket content).
Let's create a user profile page: ``blade <!-- resources/views/pages/users/[user].blade.php --> <h1>Profile of {{ $user }}</h1> ``
Now, visiting /users/john will display "Profile of john". This is incredibly powerful for dynamic pages without controllers.
[user].blade.php and a static profile.blade.php, the static route takes precedence. Folio resolves conflicts by prioritizing exact matches over parameterized ones.[param] in filenames for dynamic segments.Building Volt Components: Single-File Livewire
Volt allows you to write a Livewire component in a single file. This file contains the template, logic, and even styles. Let's create a simple counter component.
Create resources/views/livewire/counter.blade.php: ```blade <?php
use Livewire\Volt\Component;
new class extends Component { public $count = 0;
public function increment() { $this->count++; }
public function decrement() { $this->count--; } } ?>
<div> <h1>Counter: {{ $count }}</h1> <button wire:click="increment">+</button> <button wire:click="decrement">-</button> </div> ```
To use this component in any Blade view, simply include <livewire:counter />. The component's logic is defined in an anonymous class at the top of the file, and the template follows.
Volt components can also include styles and scripts. You can use <style> and <script> tags directly in the component file. They will be extracted and rendered in the appropriate sections of your layout.
For example, add some styling to the counter: ``blade <style> button { margin: 5px; padding: 10px; } </style> ``
Volt also supports computed properties, actions, and lifecycle hooks. You can define a mount method to initialize data, or use #[Computed] attribute for computed properties.
Let's create a more complex component: a todo list.
```blade <?php
use Livewire\Volt\Component;
new class extends Component { public $todos = []; public $newTodo = '';
public function addTodo() { $this->todos[] = $this->newTodo; $this->newTodo = ''; }
public function removeTodo($index) { unset($this->todos[$index]); $this->todos = array_values($this->todos); } } ?>
<div> <input type="text" wire:model="newTodo" placeholder="New todo"> <button wire:click="addTodo">Add</button> <ul> @foreach($todos as $index => $todo) <li>{{ $todo }} <button wire:click="removeTodo({{ $index }})">X</button></li> @endforeach </ul> </div> ```
This component manages a list of todos with add and remove functionality, all in one file.
php artisan livewire:list. For debugging, enable Livewire's JavaScript error logging in the browser console.wire:click, wire:model, and other Livewire directives for interactivity.Combining Folio and Volt: Dynamic Pages with Livewire
The real power of Folio and Volt emerges when you combine them. You can create Folio pages that include Volt components, or even make a Folio page itself a Volt component. Let's explore both approaches.
First, create a Folio page that uses a Volt component. Create resources/views/pages/dashboard.blade.php: ``blade <x-app-layout> <h1>Dashboard</h1> <livewire:counter /> </x-app-layout> ``
Now, /dashboard will render the dashboard layout with the counter component. This is straightforward.
But you can also make a Folio page a Volt component itself. To do this, create a file in resources/views/pages with a .volt.blade.php extension. For example, resources/views/pages/settings.volt.blade.php. This file will be treated as a Volt component, meaning it can contain an anonymous class with logic.
Let's create a settings page with a form to update a user's name: ```blade <?php
use Livewire\Volt\Component; use Illuminate\Support\Facades\Auth;
new class extends Component { public $name = '';
public function mount() { $this->name = Auth::user()->name; }
public function update() { Auth::user()->update(['name' => $this->name]); session()->flash('message', 'Name updated.'); } } ?>
<x-app-layout> <h1>Settings</h1> @if (session('message')) <div>{{ session('message') }}</div> @endif <form wire:submit="update"> <input type="text" wire:model="name"> <button type="submit">Update</button> </form> </x-app-layout> ```
Now, /settings will render this Volt-powered page. The component has its own logic, and the page is defined by the file itself. This is a powerful pattern for pages that need interactivity without separate controllers.
You can also pass route parameters from Folio to Volt components. For example, create resources/views/pages/users/[user].volt.blade.php: ```blade <?php
use Livewire\Volt\Component; use App\Models\User;
new class extends Component { public $user;
public function mount($user) { $this->user = User::where('username', $user)->firstOrFail(); } } ?>
<x-app-layout> <h1>{{ $user->name }}</h1> <p>Email: {{ $user->email }}</p> </x-app-layout> ```
Now, /users/john will display John's profile. The $user parameter from the route is automatically passed to the mount method.
.volt.blade.php files, remember that they are both a route and a component. Ensure that the component logic does not conflict with the page layout. Use x-app-layout to wrap the component content..volt.blade.php files in the pages directory. This allows you to create interactive pages with file-based routing and single-file components.Advanced Folio Features: Middleware, Prefixes, and Caching
Folio is not just about simple pages; it supports middleware, route prefixes, and even caching for production. Let's explore these advanced features.
Middleware
You can apply middleware to Folio pages by creating a _middleware file in the pages directory. For example, create resources/views/pages/_middleware.php: ```php <?php
return [ 'auth', ]; ```
This will apply the auth middleware to all Folio pages in that directory and its subdirectories. You can also create middleware files in subdirectories to scope middleware to specific sections.
Route Prefixes
If you want all Folio pages under a certain directory to have a prefix, you can configure it in the FolioServiceProvider. For example, to prefix all pages under admin with /admin, you can register a path: ``php // In App\Providers\FolioServiceProvider Folio::path(resource_path('views/pages/admin'))->middleware(['auth']); ``
Then, create pages in resources/views/pages/admin and they will be accessible under /admin/....
Caching
In production, you can cache Folio's route registrations to improve performance. Run: ``bash php artisan folio:cache ``
This will generate a cached routes file. To clear the cache, run: ``bash php artisan folio:clear-cache ``
Customizing Route Model Binding
You can customize how route parameters are resolved by using the #[Bind] attribute in Volt components or by defining a binding in the Folio service provider.
For example, to bind a [user] parameter to a User model by username: ```php // In App\Providers\FolioServiceProvider use App\Models\User; use Laravel\Folio\Folio;
Folio::bind('user', function ($value) { return User::where('username', $value)->firstOrFail(); }); ```
Then, in your Volt component, you can type-hint the model: ``php public function mount(User $user) { $this->user = $user; } ``
This makes your code cleaner and leverages Laravel's model binding.
php artisan folio:cache in production to avoid scanning the filesystem on every request. Also, ensure that your _middleware.php files are not accidentally exposed; they are PHP files that return arrays, so they are safe._middleware.php files, route prefixes via service provider, and caching for production. Custom route model binding can be defined globally.Volt Advanced: Computed Properties, Validation, and Events
Volt components can do everything traditional Livewire components can, including computed properties, validation, and event dispatching. Let's see how.
Computed Properties
Use the #[Computed] attribute to define computed properties. These are cached for the duration of the request.
```blade <?php
use Livewire\Volt\Component; use Livewire\Attributes\Computed;
new class extends Component { public $items = [1, 2, 3, 4, 5];
#[Computed] public function evenItems() { return array_filter($this->items, fn($item) => $item % 2 === 0); } } ?>
<div> @foreach ($this->evenItems as $item) <p>{{ $item }}</p> @endforeach </div> ```
Validation
You can validate properties using the #[Rule] attribute or by calling $this->validate() in actions.
```blade <?php
use Livewire\Volt\Component; use Livewire\Attributes\Rule;
new class extends Component { #[Rule('required|email')] public $email = '';
public function submit() { $this->validate(); // Process the email } } ?>
<div> <input type="email" wire:model="email"> @error('email') <span>{{ $message }}</span> @enderror <button wire:click="submit">Submit</button> </div> ```
Events
Volt components can dispatch and listen to events using $this->dispatch() and #[On] attribute.
```blade <?php
use Livewire\Volt\Component; use Livewire\Attributes\On;
new class extends Component { public $message = '';
#[On('post-created')] public function handlePostCreated($postId) { $this->message = "Post #{$postId} created!"; }
public function createPost() { // Create post logic $this->dispatch('post-created', postId: 1); } } ?>
<div> <p>{{ $message }}</p> <button wire:click="createPost">Create Post</button> </div> ```
These features make Volt components fully capable of handling complex interactions.
unset($this->evenItems) to clear it. For validation, always use $this->validate() to ensure data integrity.#[Computed], validation with #[Rule], and events with dispatch and #[On]. These make components powerful and reusable.Real-World Example: Blog with Pagination and Search
Let's build a blog page using Folio and Volt. We'll have a list of posts with pagination and a search feature.
First, create a Folio page for the blog index: resources/views/pages/blog/index.volt.blade.php.
```blade <?php
use Livewire\Volt\Component; use App\Models\Post; use Livewire\WithPagination;
new class extends Component { use WithPagination;
public $search = '';
public function updatingSearch() { $this->resetPage(); }
public function with() { return [ 'posts' => Post::where('title', 'like', '%'.$this->search.'%') ->paginate(10), ]; } } ?>
<x-app-layout> <h1>Blog</h1> <input type="text" wire:model.live="search" placeholder="Search posts..."> @foreach ($posts as $post) <div> <h2>{{ $post->title }}</h2> <p>{{ $post->excerpt }}</p> <a href="/blog/{{ $post->slug }}">Read more</a> </div> @endforeach {{ $posts->links() }} </x-app-layout> ```
This component uses Livewire's WithPagination trait and a search feature. The method returns data to the view. The with()updatingSearch hook resets pagination when search changes.
Now, create a single post page: resources/views/pages/blog/[post].volt.blade.php.
```blade <?php
use Livewire\Volt\Component; use App\Models\Post;
new class extends Component { public $post;
public function mount(Post $post) { $this->post = $post; } } ?>
<x-app-layout> <article> <h1>{{ $post->title }}</h1> <div>{{ $post->body }}</div> </article> </x-app-layout> ```
Now, /blog shows paginated posts with search, and /blog/my-post-slug shows the full post. All without writing a single route or controller.
To make this production-ready, add caching for the post queries. You can use Laravel's cache in the method or in the with()mount method.
simplePaginate instead of paginate if you don't need page numbers.with() to provide data to the view and mount() for route model binding.The Case of the Missing Route: When Folio Routes Clash with Existing Routes
pages/admin.blade.php, navigating to /admin returned a 404 error. Other Folio routes worked fine.web.php.web.php routes. An existing route for /admin (maybe from a package) was matching first and returning a 404 because it wasn't defined properly.php artisan route:list and found a route for /admin from a third-party package. They either removed that route or adjusted Folio's route prefix to avoid conflicts.- Always run
php artisan route:listto see all registered routes, including Folio routes. - Folio routes have lower priority than routes in
web.php; use route prefixes or middleware to avoid conflicts. - Use Folio's directory-based routing to group related pages under a common prefix.
- Test routes after adding new Folio pages, especially if you have existing routes.
- Consider using Folio's
domainoption to scope routes to specific subdomains.
resources/views/pages/ with correct extension. Run php artisan route:list to see if Folio registered the route. Ensure no other route matches first.resources/views/livewire/ with .php extension. Check for syntax errors using php artisan view:clear. Ensure Livewire is installed and configured.php artisan livewire:publish --assets to publish Livewire's frontend assets. Verify that @livewireScripts and @livewireStyles are in your layout.mount method accepts the parameter. Check that the Folio route definition includes the parameter (e.g., [user]).lazy loading for components that are not immediately visible. Consider splitting large components into smaller ones. Enable Livewire's asset caching.ls resources/views/pages/php artisan route:list | grep folio| File | Command / Code | Purpose |
|---|---|---|
| terminal.sh | composer require laravel/folio | Installing and Configuring Folio and Volt |
| resources | Profile of {{ $user }} | Creating Your First Folio Page |
| resources | use Livewire\Volt\Component; | Building Volt Components |
| resources | use Livewire\Volt\Component; | Combining Folio and Volt |
| app | namespace App\Providers; | Advanced Folio Features |
| resources | use Livewire\Volt\Component; | Volt Advanced |
| resources | use Livewire\Volt\Component; | Real-World Example |
Key takeaways
resources/views/pages and they become routes automatically..volt.blade.php files to create interactive pages with route parameters and model binding.Common mistakes to avoid
3 patternsForgetting to install Livewire when using Volt
Using `.blade.php` instead of `.volt.blade.php` for Volt pages
Not clearing view cache after adding new Folio pages
Interview Questions on This Topic
How does Laravel Folio handle route parameters?
[user].blade.php creates a {user} parameter. The parameter value is available as a variable with the same name in the Blade view. You can also use [...slug] for optional parameters.Frequently Asked Questions
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
That's Laravel. Mark it forged?
7 min read · try the examples if you haven't