Sign up for early access for MySQL for Developers, a video course I'm working on.

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

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

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 V6
mix.alias({
ziggy: path.resolve('vendor/tightenco/ziggy/src/js'),
})
 
// Mix V5
mix.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;"
]);
}
Code highlighting powered by torchlight.dev (A service I created!)

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 the event.

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.

<?php
class 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:

Reach out to me at twitter.com/aarondfrancis if you have any thoughts or questions!

Thanks for reading! My name is Aaron and I'm the Marketing Engineer at Tuple — the best remote pair programming app.

My main project focus is Hammerstone, where we build components for your Laravel and Rails applications. I currently do a podcast.

If you ever have any questions or want to chat, I'm always on Twitter
Copyright 2013 - 2022, Aaron Francis.