mattbromley/inertia-statamic
Bridges Inertia.js and Statamic by serving Statamic content through Inertia responses, so you can build Laravel-driven, JavaScript-rendered front ends on top of Statamic entries without hand-wiring the root view and routing yourself.
This is a small fork (4 stars, no dependents) of Adam Campbell's Statamic↔Inertia adapter, so treat it as a reference implementation you may need to patch rather than a drop-in framework. Read the fork's README and diff it against the upstream before relying on it — the value of the fork is usually a version bump (Statamic 4/5, Laravel 10/11) rather than new features.
Minimal setup:
composer.lock:
composer require mattbromley/inertia-statamic --prefer-dist
npm i @inertiajs/vue3 # or @inertiajs/react / @inertiajs/svelte
php artisan inertia:middleware # creates App\Http\Middleware\HandleInertiaRequests
Register HandleInertiaRequests in the web middleware group in bootstrap/app.php (Laravel 11) or app/Http/Kernel.php.php artisan vendor:publish --provider="MattBromley\InertiaStatamic\ServiceProvider"
@inertia and @vite(['resources/js/app.js']); the adapter either ships one or expects you to point the config at resources/views/app.blade.php./blog/{slug}, resolve the entry through the Entry facade, pass the augmented data as props, and render Blog/Show on the front end. That single flow exercises routing, augmentation, prop serialization, and client-side navigation — if it works, the integration is sound.Where to look first in the source: the service provider (to see what it registers — a route macro, a middleware, a driver), then the class that actually builds the Inertia response. That class is the seam you'll extend.
Route → Inertia response. The adapter's job is to let you do this without fighting Statamic's front-end router:
Route::get('/blog/{slug}', function (string $slug) {
$entry = Entry::query()->where('collection', 'blog')->where('slug', $slug)->firstOrFail();
return Inertia::render('Blog/Show', [
'entry' => $entry->toAugmentedArray(),
'related' => Entry::query()->where('collection', 'blog')->limit(3)->get()->toAugmentedArray(),
]);
});
Prefer toAugmentedArray() / augmentedValue() over passing the entry model directly. Raw Entry objects carry the full YAML/JSON content stack and won't serialize predictably; augmented values go through Bard/Markdown/asset transformations and match what Antlers would render.
Define routes before the Statamic catch-all. Statamic registers a fallback route for its URL-driven front end. Inertia routes must be registered above it (Route::statamic(...) and the fallback both live late in the route file). If your Inertia route returns an Antlers view instead of a JSON/X-Inertia response, route ordering is almost always the cause.
Shared data via HandleInertiaRequests. Use the standard Inertia share() method for nav, globals, and site settings rather than repeating them per page:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'nav' => fn () => Nav::tree('main')->toAugmentedArray(),
'globals' => fn () => GlobalSet::findByHandle('site')->toAugmentedArray(),
'flash' => fn () => ['success' => session('success')],
]);
}
Wrap in closures so partial reloads don't re-query on every visit.
Partial reloads for listing pages. Statamic entry queries are cheap per-request but the full page payload isn't. On filter/pagination changes, do:
router.get(`/blog?page=${page}`, {}, { only: ['entries', 'meta'], preserveState: true, preserveScroll: true })
Persistent layouts. Use Inertia's persistent-layout pattern for the site chrome so navigation doesn't remount headers/nav — this is the main perceived-performance win over Antlers.
Forms. Statamic's Form addon is server-rendered; bridge it with a normal Laravel controller that calls Statamic\Facades\Form::find('contact')->makeSubmission(), returns validation errors as a 422, and let useForm() handle the rest. Don't try to reuse Antlers form tags inside Inertia pages.
Static site / cache interplay. If you enable Statamic's static caching, Inertia's JSON responses (X-Inertia: true) get cached incorrectly. Either scope the cache to non-Inertia requests or disable static caching for Inertia routes.
adamcampbell's adapter) and the official Inertia docs; when a version conflict arises you'll likely be the one fixing it. Keep a local patch or a vcs repository entry in composer.json so you can rebase.php artisan route:cache refuses closure routes. Convert every Inertia route to an invokable controller before deploying to a cached production environment.web middleware group must run this adapter's middleware after session/CSRF. A misplaced registration produces 419s on router.post or missing shared props on hard reloads but not on client-side visits — a signature mismatch worth remembering.$entry->toAugmentedArray() inside a shared-prop closure is fine; calling it at controller top-level for a large listing eagerly resolves assets and can be slow. Use ->select() and paginate at the query level.Vite::useHotFile / dev server mismatch shows up as a blank page with no console error — verify the resources/js/app.js entry exists and that @vite points at the same build the Inertia resolvePageComponent glob scans.inertia_asset_version (or call Inertia::version(fn () => md5_file(public_path('build/manifest.json')))) as part of your deploy.Inertia::render(...)->toResponse($request) returns a JsonResponse for X-Inertia requests and a full HTML response otherwise — inspect response()->getStatusCode() and the X-Inertia header to see which branch ran. dd() inside a shared prop closure won't render; log instead.Inertia\Testing\AssertableInertia, not assertSee, since content lives in JSON:
$this->get('/blog/hello')->assertInertia(fn ($page) =>
$page->component('Blog/Show')->has('entry.title')->where('entry.slug', 'hello'));
HandleInertiaRequests::share() are where 90% of customization lands. If the adapter's default prop mapping doesn't fit your content model, subclass it rather than editing vendor/.How can I help you explore Laravel packages today?