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
Code highlighting powered by torchlight.dev (A service I created!)

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.php
Route::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')
);
}
});
Code highlighting powered by torchlight.dev (A service I created!)

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!

Me

Thanks for reading! My name is Aaron and I write, make videos , and generally try really hard .

If you ever have any questions or want to chat, I'm always on Twitter.

If you want to give me money (please do), you can buy my course on SQLite at HighPerformanceSQLite.com or my course on screencasting at Screencasting.com . On the off chance you're a sophomore at Texas A&M University, you can buy my accounting course at acct229.com .

You can find me on YouTube on my personal channel . If you love podcasts, I got you covered. You can listen to me on Mostly Technical .