File based routing in Inertia.js
August 1, 2024
As a part of automating my life I've been building out an Inertia.js frontend with a Laravel backend.
One thing I wanted was to have a very simplified routing structure for "static" pages, that is pages that don't need props, controllers, or anything else. They're still Vue components, but with just static data.
On this site, for example, I have a lists page which contains my best lists of things. (They're very good lists.)
Here's what it looks like on the filesystem:
resources└─ js └─ Pages └─ Lists └─ Broken.vue <-- static page └─ Default.vue <-- static page └─ Scifi.vue <-- static page
To make this easier, I added an inertiaPages
macro to the router, so that I could route all of those lists in one
line:
// web.phpRoute::inertiaPages('/lists/{page}', resource_path('js/Pages/Lists'));
When anyone visits /lists/scifi
, Inertia will serve the Scifi.vue
page from the js/Pages/Lists
directory.
The macro is relatively straightforward. You can place it in the boot
method of your AppServiceProvider
. We'll walk
through it step by step, but here it is in its entirety:
namespace App\Providers; use Illuminate\Routing\Router;use Illuminate\Support\Arr;use Illuminate\Support\Facades\File;use Illuminate\Support\ServiceProvider;use Inertia\Inertia;use SplFileInfo; class AppServiceProvider extends ServiceProvider{ public function boot(): void { Router::macro('inertiaPages', function ($uri, $directory) { $pages = array_map(function (SplFileInfo $file) { return Str::kebab($file->getBasename('.vue')); }, File::allFiles($directory)); if ($default = Arr::where($pages, fn($value) => $value === 'default')) { Arr::pull($pages, head(array_keys($default))); $this->match( ['GET', 'HEAD'], Str::chopEnd($uri, '/{page}'), fn() => Inertia::render('Lists/Default') ); } return $this ->match( ['GET', 'HEAD'], $uri, fn($page) => Inertia::render('Lists/' . ucfirst(Str::camel($page))) ) ->whereIn('page', $pages); }); }}
The first few lines just register the macro in the router, receiving the URI pattern and the directory to look for the pages.
Router::macro('inertiaPages', function ($uri, $directory) { //});
Next, we grab all the files in the directory and get just their basenames, with no .vue
extension. (Note, this doesn't
handle subdirectory routing. With a few tweaks it could!)
Router::macro('inertiaPages', function ($uri, $directory) { $pages = array_map(function (SplFileInfo $file) { // Kebab is nice for urls. ScifiBooks.vue would become scifi-books return Str::kebab($file->getBasename('.vue')); }, File::allFiles($directory));});
Each individual page lives at its own /lists/{page}
url. To allow for a /lists
url with no page, you can create a Default.vue
that will be served when there is no page param.
Router::macro('inertiaPages', function ($uri, $directory) { $pages = array_map(function (SplFileInfo $file) { return Str::kebab($file->getBasename('.vue')); }, File::allFiles($directory)); // See if there's a default page if ($default = Arr::where($pages, fn($value) => $value === 'default')) { // Remove it from the pages array so /lists/default isn't a valid route. Arr::pull($pages, head(array_keys($default))); $this->match( ['GET', 'HEAD'], // Remove the page param from the uri, turning it into just /lists Str::chopEnd($uri, '/{page}'), fn() => Inertia::render('Lists/Default') ); } });
Finally, we create the route to match all the pages. Importantly, we only create routes that have valid pages. We use
Laravel's whereIn
route constraint to ensure that that's the case.
Router::macro('inertiaPages', function ($uri, $directory) { $pages = array_map(function (SplFileInfo $file) { return Str::kebab($file->getBasename('.vue')); }, File::allFiles($directory)); if ($default = Arr::where($pages, fn($value) => $value === 'default')) { Arr::pull($pages, head(array_keys($default))); $this->match( ['GET', 'HEAD'], Str::chopEnd($uri, '/{page}'), fn() => Inertia::render('Lists/Default') ); } return $this ->match( ['GET', 'HEAD'], $uri, // ucfirst + camel = PascalCase fn($page) => Inertia::render('Lists/' . ucfirst(Str::camel($page))) ) // Only match valid pages ->whereIn('page', $pages); });
And that's it! Now you can add pages as you see fit and they'll be auto-wired.
Please let me know on Twitter if you liked it or have any ideas on how to make it better!