Using Ziggy with Inertia server side rendering
January 8, 2022
Inertia.js has just recently announced general availability for server-side rendering.
The SSR results are extremely impressive. It's wicked fast and pretty easy to set up. There are a few gotchas, one of which is popular Javascript libraries that rely on the window
object, which is obviously not available when running on the server.
Ziggy
One of those popular libraries that people are having trouble with is Ziggy, which allows you to use your Laravel routes in JavaScript.
It's an extremely helpful package that brings a Laravel-esque route
helper to the frontend, but it relies on the window object and a global Ziggy object as well. Fortunately it's relatively easy to get around that!
To see a fully working Laravel Jetstream + Inertia + Ziggy + Sidecar SSR demo repo, head over to github.com/hammerstonedev/sidecar-inertia-demo.
Sending the Routes Over
The first thing we need to do is get all the routes into Ziggy. In a standard Laravel app this is done with the @routes
Blade tag, which puts everything on the page. When using Inertia SSR you'll be rendering a Vue component, therefore the Blade tag isn't a viable option.
If you're using React, you should read this GitHub issue for more.
Ziggy exposes a couple of other options to get a hold of the routes, which is what we'll be using. We'll share it with Inertia using the Inertia::share
functionality in HandleInertiaRequests
middleware:
app\Http\Middleware\HandleInertiaRequests.php
<?phpclass HandleInertiaRequests extends Middleware{ public function share(Request $request) { $ziggy = new Ziggy($group = null, $request->url()); return array_merge(parent::share($request), [ // Add in Ziggy routes for SSR 'ziggy' => $ziggy->toArray() ]); }}
Inertia will now add a ziggy
key to your props
, which will be passed to your SSR function.
Webpack Setup
Before this can work, you have to make one tweak to your webpack config (because of course you do, it's webpack.)
You need to tell webpack where to find the 'ziggy'
import, because it's not a traditional NPM module, it's installed via composer.
webpack.ssr.mix.js
// Mix V6mix.alias({ ziggy: path.resolve('vendor/tightenco/ziggy/src/js'),}) // Mix V5mix.webpackConfig({ resolve: { alias: { ziggy: path.resolve('vendor/tightenco/ziggy/src/js'), }, }})
Configuration + Route Function
Inertia's SSR docs guide you through how to set up an ssr.js
, which I won't cover in detail here, but we will be modifying it a bit to include the new route
helper.
First we're going to cover the process using Inertia's Node server, and then we'll look at how to do it when using Sidecar to run SSR on Lambda.
Here's the basic ssr.js
setup if you're using the Inertia Node server as your SSR renderer:
resources/js/ssr.js (Inertia Server Version)
import {createSSRApp, h} from 'vue'import {renderToString} from '@vue/server-renderer'import {createInertiaApp} from '@inertiajs/inertia-vue3'import createServer from '@inertiajs/server' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: name => require(`./Pages/${name}`), setup({app, props, plugin}) { return createSSRApp({ render: () => h(app, props), }).use(plugin) },}))
The first thing we need to do is import the route
helper itself from Ziggy:
resources/js/ssr.js (Inertia Server Version)
import route from 'ziggy'; createServer((page) => createInertiaApp({ // ...}))
Next we build up the Ziggy configuration manually because it's not globally available to us like it is in the browser.
We'll pull it off of the props passed from Inertia and add an additional location
key, since there is no window.location
on the server side.
(Ziggy added support for this specifically to help with Inertia SSR. Make sure you're on at least version 1.3.0.)
resources/js/ssr.js (Inertia Server Version)
import {createSSRApp, h} from 'vue'import {renderToString} from '@vue/server-renderer'import {createInertiaApp} from '@inertiajs/inertia-vue3'import createServer from '@inertiajs/server'import route from 'ziggy' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: name => require(`./Pages/${name}`), setup({app, props, plugin}) { const Ziggy = { // Pull the Ziggy config off of the props. ...props.initialPage.props.ziggy, // Build the location, since there is // no window.location in Node. location: new URL(props.initialPage.props.ziggy.url) } return createSSRApp({ render: () => h(app, props), }).use(plugin) },}))
Now instead of using their provided function as the mixin, we're going to shim it a bit so that we can add the configuration object we just built, which gives us our final setup:
resources/js/ssr.js (Inertia Server Version)
import {createSSRApp, h} from 'vue'import {renderToString} from '@vue/server-renderer'import {createInertiaApp} from '@inertiajs/inertia-vue3'import createServer from '@inertiajs/server'import route from 'ziggy' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: name => require(`./Pages/${name}`), setup({app, props, plugin}) { const Ziggy = { // Pull the Ziggy config off of the props. ...props.initialPage.props.ziggy, // Build the location, since there is // no window.location in Node. location: new URL(props.initialPage.props.ziggy.url) } return createSSRApp({ render: () => h(app, props), }).use(plugin).mixin({ methods: { route: (name, params, absolute, config = Ziggy) => route(name, params, absolute, config), }, }) },}))
If you're using Inertia's Node process to handle your SSR then that's all you need to do! The next section will cover using Ziggy with Sidecar as your SSR provider.
Sidecar Configuration
Using Inertia and Ziggy with Sidecar SSR is very similar to the plain ol' Inertia.
Here's the standard Sidecar SSR function, before we add the Ziggy stuff.
resources/js/ssr.js (Sidecar Version)
import {createSSRApp, h} from 'vue'import {renderToString} from '@vue/server-renderer'import {createInertiaApp} from '@inertiajs/inertia-vue3' exports.handler = async function (event) { return await createInertiaApp({ page: event, render: renderToString, resolve: (name) => require(`./Pages/${name}`), setup({app, props, plugin}) { return createSSRApp({ render: () => h(app, props), }).use(plugin) }, });}
In the Sidecar version though, we'll be adding compiling the routes directly in the Lambda function so we don't have to send them over the wire each time.
The Sidecar SSR function will add your Ziggy configuration for you. Here's the relevant section:
SSR.php
protected function includeZiggy(Package $package){ $ziggy = json_encode(new Ziggy); Sidecar::log('Adding Ziggy to the package.'); // Include a file called "compiledZiggy" that simply exports // the entire Ziggy PHP object we just serialized. $package->includeStrings([ 'compiledZiggy.js' => "module.exports = $ziggy;" ]);}
Your Lambda function now includes a file named compiledZiggy.js
with the Ziggy configuration.
We'll need to adjust the webpack.ssr.mix.js
file to account for this new compiledZiggy.js
file.
webpack.ssr.mix.js
const path = require('path')const mix = require('laravel-mix') mix .js('resources/js/ssr.js', 'public/js') .options({ manifest: false }) .vue({ version: 3, useVueStyleLoader: true, options: { optimizeSSR: true } }) .alias({ '@': path.resolve('resources/js') }) .webpackConfig({ target: 'node', externals: { node: true, // Sidecar will ship a file called compiledZiggy as a part of // the package. We don't want webpack to try to inline it // because it doesn't exist at the time webpack runs. './compiledZiggy': 'require("./compiledZiggy")' }, resolve: { alias: { ziggy: path.resolve('vendor/tightenco/ziggy/src/js'), }, }, })
Finally, we can take a look at the SSR function and see how to use the new compiledZiggy
there.
Note also that instead of pulling the Ziggy config from the
props
, we pull it off of theevent
.
resources/js/ssr.js (Sidecar Version)
import {createSSRApp, h} from 'vue'import {renderToString} from '@vue/server-renderer'import {createInertiaApp} from '@inertiajs/inertia-vue3'import route from 'ziggy'; exports.handler = async function (event) { // This is the file that Sidecar has compiled for us if // this application uses Ziggy. We import it using // this syntax since it may not exist at all. const compiledZiggy = await import('./compiledZiggy'); return await createInertiaApp({ page: event, render: renderToString, resolve: (name) => require(`./Pages/${name}`), setup({app, props, plugin}) { const Ziggy = { // Start with the stuff that may be baked into this Lambda. ...(compiledZiggy || {}), // Then if they passed anything to us via the event, // overwrite everything that was baked in. ...event?.props?.ziggy, } // Construct a new location, since window.location is not available. Ziggy.location = new URL(Ziggy.url) return createSSRApp({ render: () => h(app, props), }).use(plugin).mixin({ methods: { // Use our custom Ziggy object as the config. route: (name, params, absolute, config = Ziggy) => route(name, params, absolute, config), }, }) }, });}
Now that we're including all of our application routes in the Lambda itself, we can adjust our Inertia middleware. In developement we'll still send everything over, but in production we'll just send the current URL, as that's still required.
<?phpclass HandleInertiaRequests extends Middleware{ public function share(Request $request) { $ziggy = new Ziggy($group = null, $request->url()); $ziggy = $ziggy->toArray(); // During development, send over the entire Ziggy object, so that // when routes change we don't have to redeploy. In production, // only send the current URL, as we will bake the Ziggy config // into the Lambda SSR package. $ziggy = app()->environment('production') ? Arr::only($ziggy, 'url') : $ziggy; return array_merge(parent::share($request), [ 'ziggy' => $ziggy ]); }}
That's it, you should be ready to use Ziggy with Inertia SSR. Here are some further links that may help:
- github.com/hammerstonedev/sidecar-inertia - Inertia SSR powered by Sidecar
- github.com/hammerstonedev/sidecar - Sidecar, a package to manage Lambda from your Laravel application
- github.com/tighten/ziggy - Ziggy.js
- inertiajs.com/server-side-rendering - Inertia SSR docs
Reach out to me at twitter.com/aarondfrancis if you have any thoughts or questions!