This guide will explain how to add server-side rendering (SSR) to an existing React Inertia application.
Note, if you're using the Ziggy library, you're going to run into issues with SSR. While it is technically possible to use Ziggy in SSR, it requires a bunch of extra configuration, which this guide won't cover.
Upgrade to the latest version of Inertia (version 0.9.0 or newer):
npm install @inertiajs/inertia@latest @inertiajs/inertia-react@latest @inertiajs/progress@latest
In order for <head>
tags such as <title>
and <meta>
to work in SSR mode, you need to use the new <InertiaHead>
component instead React Helmet, or similar libraries. To do this, add the following component to your pages:
import { InertiaHead } from '@inertiajs/inertia-react'
<InertiaHead>
<title>Your page title</title>
<meta name="description" content="Your page description" />
</InertiaHead>
You can also use the title prop shorthand:
import { InertiaHead } from '@inertiajs/inertia-react'
<InertiaHead title="Your page title" />
It's also available as just Head
if that's your style:
import { Head } from '@inertiajs/inertia-react'
<Head title="Your page title" />
It's possible to have multiple instances of the <InertiaHead>
component throughout your application. For example, maybe your layout component sets a default title and meta description tag, and then your pages themselves overide those defaults as necessary.
By default, Inertia will only ever render one <title>
tag. However, it's possible to stack other tags, such as <meta>
tags. To avoid duplicate tags in your <head>
, you can use the inertia
attribute, which will make sure the tag is only rendered once. For example:
// layout.js
import { InertiaHead } from '@inertiajs/inertia-react'
<InertiaHead>
<title>My app</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="description" content="This is my app's default description" inertia="description" />
</InertiaHead>
// about.js
import { InertiaHead } from '@inertiajs/inertia-react'
<InertiaHead>
<title>About - My app</title>
<meta name="description" content="This is my about page description" inertia="description" />
</InertiaHead>
In this example, only the <title>
and <meta>
tags from the page component will be rendered:
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>About - My app</title>
<meta name="description" content="This is my about page description" />
</head>
Please note that the method to configure the SSR server here is outdated, and will be replaced by https://github.com/inertiajs/server within the first days of January 2022.
Now we'll configure our SSR server. This is a light Express (Node) server that will run in the background and convert your React page components into HTML.
First, install Express:
npm install express
Next, create a resources/js/ssr.js
file:
touch resources/js/ssr.js
This file is going to look similar like your app.js
file, with the exception that it's not going to run in the browser, but rather in Node. Here's a complete example. Be sure to add anything that's missing from your app.js
file that makes sense to run in SSR mode. However, not everything needs to be included. For example, the InertiaProgress
library can be ommitted from this file, as it will never be used in SSR mode.
import React from 'react'
import express from 'express'
import ReactDOMServer from 'react-dom/server'
import { createInertiaApp } from '@inertiajs/inertia-react'
const server = express()
server.use(express.json())
server.post('/render', async (request, response, next) => {
try {
response.json(
await createInertiaApp({
page: request.body,
render: ReactDOMServer.renderToString,
resolve: name => require(`./Pages/${name}`),
setup: ({ App, props }) => <App {...props} />,
})
)
} catch (error) {
next(error)
}
})
server.listen(8080, () => console.log('Server started.'))
console.log('Starting SSR server...')
Note, do not use code splitting in ssr.js
, as it won't help anything. Instead, we want to generate just one SSR build file. You can, of course, still use code splitting for your client-side build (app.js
).
At the time of writing this, Laravel Mix does not support multiple webpack configurations within the same webpack.mix.js
file. So, instead we'll create a new webpack.ssr.mix.js
file for SSR.
Also, in order for our Webpack build to run properly on Node, we also need to install the webpack-node-externals
package:
npm install webpack-node-externals
With these packages installed, let's now create webpack.ssr.mix.js
:
touch webpack.ssr.mix.js
Here is a example configuration for this file. Note that it will look much like your webpack.mix.js
configuration, with the exception that you only compile your JavaScript, and not your CSS. Be sure to redefine any aliases used within your application. Using webpackConfig()
, be sure to set the target
to node
, and set externals
to [webpackNodeExternals()]
, which is the library we just installed.
const path = require('path')
const mix = require('laravel-mix')
const nodeExternals = require('webpack-node-externals');
mix
.options({ manifest: false })
.js('resources/js/ssr.js', 'public/js')
.react()
.alias({ '@': path.resolve('resources/js') })
.webpackConfig({
target: 'node',
externals: [nodeExternals()],
})
Please note that the method to configure the SSR directives server here is outdated, and will be replaced by just the @inertia
and @inertiaHead
directives within the first days of January 2022.
Next, we need to update our app.blade.php
to actually use the HTML rendered from our SSR server:
@php
try {
$ssr = Http::post('http://localhost:8080/render', $page)->throw()->json();
} catch (Exception $e) {
$ssr = null;
}
@endphp
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
@foreach($ssr['head'] ?? [] as $element)
{!! $element !!}
@endforeach
</head>
<body>
@if ($ssr)
{!! $ssr['body'] !!}
@else
@inertia
@endif
</body>
</html>
You now have two build processes you need to run—one for your client-side bundle, and another for your server-side bundle:
npx mix
npx mix --mix-config=webpack.ssr.mix.js
Run both of these build steps and correct any errors that are generated. Remember, you're now building an "isomorphic" app, which means your app runs both on the client (browser) and on the server (Node). To learn more about SSR in React, see their guide.
With the builds generated, you can now run the Node server:
node public/js/ssr.js
With that running, you should now be able to access your app within the browser, with server-side rendering enable. In fact, you should be able to disable JavaScript entirely and navigate around the app.
When deploying your SSR enabled app to production, you'll need to run both the client-side (app.js
) and server-side (ssr.js
) builds. One option here is to update the prod
script in package.json
to run both builds automatically:
"prod": "mix --production && mix --production --mix-config=webpack.ssr.mix.js",
To run the SSR server on Forge, create a new daemon that runs node public/js/ssr.js
in the root of your app. Take note of the daemon ID that is generated, as you'll need to use this in your apps deployment script. Whenever you deploy your application, you'll need to automatically restart the SSR server. Add the following to your deployment script, updating "123456" with your daemon ID.
# Restart SSR server
sudo supervisorctl restart daemon-123456:daemon-123456_00
To run the SSR server on Heroku, update your web
configuration in your Procfile
to first run the SSR server before starting your web server. Note, to do this you must have the heroku/nodejs
buildpack installed in addition to the heroku/php
buildback.
web: node public/js/ssr.js & vendor/bin/heroku-php-apache2 public/