The InertiaJS Response Handler you Didn’t Know You Needed... Until now....
December 1, 2025
4 minutes
We’re big fans of IntertiaJS here at Jump24, it makes developing fully featured Vue or React single page applications with a Laravel backend so easy it feels like cheating, in our Laravel controllers we can just pass a Vue page name, and some props, and Inertia takes care of the rest and will render a full Vue page for the user in their browser.
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Models\Blog; 6use Inertia\Inertia; 7 8class HomeController 9{10 public function __invoke()11 {12 return Inertia::render('Index', [13 'meta' => [14 'title' => config('app.name'),15 ],16 'latest_blogs' => Blog::query()->latest()->take(3)->get(),17 ]);18 }19}
But I’ve always felt that there’s something missing, let's take this example a little further, let's imagine we have a main Layout file, and that expects a page title, maybe some meta information like keywords, a description, and maybe even some OG information for social media… suddenly, we have to remember to send all of that data as props, with the correct key names every single time from every single Controller.
1<script setup lang="ts"> 2type MetaProps = { 3 title: string; 4 currentUrl: string; 5 description: string; 6 keywords: string; 7 image: string; 8 siteName: string; 9}10 11defineProps<{12 meta: MetaProps13}>();14</script>15 16<template>17 <!doctype html>18 <html lang="en">19 <head>20 <meta charset="UTF-8">21 <meta name="viewport" content="width=device-width, initial-scale=1">22 23 <title>{{ meta.title }}</title>24 25 <meta26 name="description"27 :content="meta.description"28 />29 30 <meta31 name="keywords"32 :content="meta.keywords"33 />34 35 <meta36 property="og:type"37 content="article"38 />39 40 <meta41 property="og:locale"42 content="en_GB"43 />44 <meta45 property="og:site_name"46 :content="meta.siteName"47 />48 <meta49 property="og:description"50 :content="meta.description"51 />52 <meta53 property="og:title"54 :content="meta.title"55 />56 <meta57 property="og:image"58 :content="meta.image"59 />60 <meta61 property="og:image:type"62 content="image/jpeg"63 />64 <meta65 property="og:url"66 :content="meta.currentUrl"67 />68 </head>69 <body>70 <slot />71 </body>72 </html>73</template>
Now, let's imagine we have a couple of controllers, one to show a blog, and one to list all blogs, we have to remember to send each of those props, every single time…
1<?php 2 3namespace App\Http\Controllers\Blogs; 4 5use App\Models\Blog; 6use Illuminate\Http\Request; 7use Inertia\Inertia; 8 9class ShowController10{11 public function __invoke(Blog $blog, Request $request)12 {13 return Inertia::render('Blog/Show', [14 'meta' => [15 'title' => $blog->title . ' - ' . config('app.name'),16 'currentUrl' => $request->url(),17 'description' => $blog->description,18 'keywords' => $blog->keywords,19 'image' => $blog->image,20 'siteName' => config('app.name'),21 ],22 'blog' => $blog,23 ]);24 }25}26 27 28//...29 30<?php31 32namespace App\Http\Controllers\Blogs;33 34use App\Models\Blog;35use Illuminate\Http\Request;36use Inertia\Inertia;37 38class IndexController39{40 public function __invoke(Request $request)41 {42 return Inertia::render('Blog/Index', [43 'meta' => [44 'title' => config('app.name'),45 'currentUrl' => $request->url(),46 'description' => config('app.description'),47 'keywords' => config('app.keywords'),48 'image' => config('app.default_og_image'),49 'siteName' => config('app.name'),50 ],51 'blogs' => Blog::query()->latest()->paginate(),52 ]);53 }54}
Notice the duplication? As previously alluded to, this is full of risks, you have to remember to send the meta props each time, and remember to always make sure you’re using the same keys, sure you could guard around it in the Vue side with v-if’s, and fallbacks, but even then, you’ve got to remember to use the right keys each time.
So what can we do to help? Insert the InertaJS response handler you didn’t know you needed… until now.
Imagine an Inertia class, one you can just add into the controller arguments to inject each time you need it, imagine it has fluent, syntactic methods for chaining your page title, your meta information, everything you need… well imagine no longer…
1<?php 2 3namespace App\Http\Responses; 4 5use Inertia\Inertia as BaseInertia; 6use Inertia\Response; 7 8class Inertia 9{10 public function __construct()11 {12 $this->setDefaultMetas();13 }14 15 protected function setDefaultMetas(): void16 {17 BaseInertia::share('metas.baseUrl', config('app.url'));18 BaseInertia::share('metas.title', config('app.title'));19 BaseInertia::share('metas.description', config('app.description'));20 BaseInertia::share('metas.tags', config('app.keywords'));21 BaseInertia::share('metas.image', config('app.default_og_image'));22 BaseInertia::share('metas.currentUrl', request()->url());23 }24 25 public function title(string $title): self26 {27 $defaultTitle = config('app.name');28 29 BaseInertia::share('metas.title', "{$title} - {$defaultTitle}");30 31 return $this;32 }33 34 public function metaDescription(string $description): self35 {36 BaseInertia::share('metas.description', $description);37 38 return $this;39 }40 41 public function metaKeywords(array $tags, bool $merge = true): self42 {43 if ($merge) {44 /** @var string[] $defaultTags */45 $defaultTags = config('app.keywords');46 47 $tags = array_merge($tags, $defaultTags);48 }49 50 BaseInertia::share('metas.tags', $tags);51 52 return $this;53 }54 55 public function metaImage(string $image): self56 {57 BaseInertia::share('metas.image', $image);58 59 return $this;60 }61 62 /** @param array<string, mixed> $props */63 public function render(string $component, array $props = []): Response64 {65 return BaseInertia::render($component, $props);66 }67}
So in the constructor, we’re just setting some fallback defaults for a site title, keywords, default OG image etc, and then we expose several methods to set anything we need to set fluently, and there’s no magic going on here, we’re simply just passing what we need to Inertia, and then Inertia will take care of the rest and send the data we need as props to the Vue file and the response.
Now lets refactor our two controllers from before to use this new Inertia class
1<?php 2 3namespace App\Http\Controllers\Blogs; 4 5use App\Http\Responses\Inertia; 6use App\Models\Blog; 7 8class ShowController 9{10 public function __invoke(Blog $blog, Inertia $inertia)11 {12 return $inertia13 ->title($blog->title)14 ->metaDescription($blog->description)15 ->metaKeywords($blog->keywords)16 ->metaImage($blog->image)17 ->render('Blog/Show', [18 'blog' => $blog,19 ]);20 }21}22 23//...24 25<?php26 27namespace App\Http\Controllers\Blogs;28 29use App\Http\Responses\Inertia;30use App\Models\Blog;31 32class IndexController33{34 public function __invoke(Inertia $inertia)35 {36 return $inertia37 ->title('Latest Blogs')38 ->render('Blog/Index', [39 'blogs' => Blog::query()->latest()->paginate(),40 ]);41 }42}
Ok, so it might be a little longer line wise, but it's much more readable, you instantly know what you’re passing as the page title, descriptions, whether the page has an OG image, you don’t need to worry about forgetting to send them, or using the wrong keys.
And the usability of the Inertia response handler doesn't end there, here’s are a few more examples.
Say you have some pages you don’t really want Google to track, using the no index header, just add it as a method in the Inertia handler, and account for it on the front end.
1<?php 2 3namespace App\Http\Responses; 4 5use Inertia\Inertia as BaseInertia; 6use Inertia\Response; 7 8class Inertia 9{10 // ...11 12 public function doNotTrack(): self13 {14 BaseInertia::share('metas.doNotTrack', true);15 16 return $this;17 }18}
1<template> 2 <!doctype html> 3 <html lang="en"> 4 <head> 5 // ... 6 7 <meta 8 v-if="meta.doNotTrack" 9 name="robots"10 content="noindex"11 />12 </head>13 <body>14 <slot />15 </body>16 </html>17</template>
Or maybe you have a site announcements/alerts system, and on every page you want to check if there’s any announcements to place at the top of your layout, as before, just call it in the constructor, and if there is, pass it along to the Vue layer.
1<?php 2 3namespace App\Http\Responses; 4 5use App\Models\Anouncement; 6use Illuminate\Support\Str; 7use Inertia\Inertia as BaseInertia; 8use Inertia\Response; 9 10class Inertia11{12 public function __construct()13 {14 $this->setDefaultMetas();15 $this->checkForSiteAnnouncement();16 }17 18 protected function checkForSiteAnnouncement(): void19 {20 $announcement = Announcement::query()21 ->where('live', true)22 ->where('expires_at', '>', now())23 ->first();24 25 if ($announcement) {26 BaseInertia::share('meta.announcement', [27 'title' => $announcement->title,28 'text' => Str::markdown($announcement->text),29 ]);30 }31 }
Perhaps your website has an online shop, and on any pages under the shop section you always want to pass the users current basket information, like maybe to always show ‘you have X items in your basket’ in a card near the header, again, can easily be done with this approach.
1<?php 2 3namespace App\Http\Responses; 4 5use App\Actions\ResolveBasketAction; 6use App\Models\Anouncement; 7use Illuminate\Support\Facades\Request; 8use Illuminate\Support\Str; 9use Inertia\Inertia as BaseInertia;10use Inertia\Response;11 12class Inertia13{14 public function __construct()15 {16 $this->setDefaultMetas();17 $this->checkForSiteAnnouncement();18 19 if (Request::routeIs('shop.*')) {20 $this->includeBasket();21 }22 }23 24 protected function includeBasket(): void25 {26 [$items, $subtotal] = app(ResolveBasketAction::class)->handle(...);27 }
You can even just add support for Interia deferred props, and merge props to your handler, to keep everything together.
1<?php 2 3namespace App\Http\Responses; 4 5use App\Actions\ResolveBasketAction; 6use App\Models\Anouncement; 7use Illuminate\Support\Facades\Request; 8use Illuminate\Support\Str; 9use Inertia\DeferProp;10use Inertia\Inertia as BaseInertia;11use Inertia\MergeProp;12use Inertia\Response;13 14class Inertia15{16 // ...17 18 public static function defer(callable $closure): DeferProp19 {20 return BaseInertia::defer($closure);21 }22 23 public static function merge(mixed $value): MergeProp24 {25 return BaseInertia::merge($value);26 }27}
Ok, you’ve convinced me, are there any downsides?
The only downsides I’ve personally experienced with this are more with IDE support, using something like LaravelIDEA in PHPStorm, that fully understands calling inertia() or Inertia::render() from your controller and gives you full auto complete over your Vue page names, but this approach unfortunately doesn't, LaravelIDEA isn’t smart enough to know that calling ->render() on our Inertia handler is expecting a valid Inertia page component, and I haven’t found a way to let LaravelIDEA know that.
But, if you can live without a little bit of autocompletion in your IDE, and if you work a lot with Inertia and have to pass a lot of shared props to each page, give this approach a try, it might have been the thing you didn’t realise you were missing all this time.
Need help with a Vue Project?
Looking for help with your Vue Project? We’re passionate about creating top-quality web applications that stand out from the crowd. Let our skilled development team work with you to bring your ideas to life! Get in touch today and let’s get started!