Going further
Server rendering
Put a page's Vue, React, Solid and Svelte views in its first HTML. Search engines and link previews see them, the first paint shows them, and the browser hydrates them: it takes over the elements that are there rather than rendering them again.
On this page
Blade views are always rendered on the server: they are Nitro's own. A JavaScript view is different. Without server rendering, the page's HTML has the view's element and the component's state, and the browser renders the view once its code has loaded. With it, your app's render service, a small Node process, renders the view, Laravel puts the HTML in the page, and the browser picks up from there.
Optional, and safe to turn off
Nothing in a view changes for server rendering. Turn it on when a page's JavaScript views matter for search or for the first paint; leave it off for views behind a login, where neither does.
How it works
-
Laravel renders the page
Blade views render as always. For each JavaScript view, Laravel sends the render service the view's name and the component's state: the same state the browser gets.
-
The service renders the view
It renders the view through the same adapter code the browser uses, from the same state, and answers with its HTML. Laravel puts it in the component's element.
-
The browser hydrates it
When the component starts, its adapter hydrates the HTML instead of rendering into an empty element: Vue's
createSSRApp, React'shydrateRoot, Solid's and Svelte'shydrate. Event handlers attach, and the view is live.
Because both sides render from the same state through the same code, the service's HTML is what the browser's first render would be, and hydration finds what it expects.
Setting it up
The render service's entry
Name a renderer for each framework your views use. Each takes your views as import.meta.glob()
gives them, with { eager: true }: the service loads them all when it starts.
// resources/js/ssr.js
import { serve } from 'nitro/ssr';
import { renderVue } from 'nitro/vue/server';
import { renderReact } from 'nitro/react/server';
import { renderSolid } from 'nitro/solid/server';
import { renderSvelte } from 'nitro/svelte/server';
/** A renderer for each framework the app's views use; leave out the ones it doesn't. */
serve({
vue: renderVue(import.meta.glob(['../views/vue/**/*.vue', './vue/**/*.vue'], { eager: true })),
react: renderReact(import.meta.glob(['../views/react/**/*.{jsx,tsx}', './react/**/*.{jsx,tsx}'], { eager: true })),
solid: renderSolid(import.meta.glob(['../views/solid/**/*.{jsx,tsx}', './solid/**/*.{jsx,tsx}'], { eager: true })),
svelte: renderSvelte(import.meta.glob(['../views/svelte/**/*.svelte', './svelte/**/*.svelte'], { eager: true })),
});
A renderer takes the same options as the browser's registration: Vue's setup(app), React's
wrap(view). Create what they install per render (a new Pinia, a new query client): the service
renders for every visitor, so nothing should be shared between them.
Vite
Give Laravel's Vite plugin the entry as its ssr input:
// vite.config.js
laravel({
input: ['resources/js/app.js'],
ssr: 'resources/js/ssr.js',
refresh: true,
}),
Solid compiles a component differently for the browser and the server: turn on ssr: true in
vite-plugin-solid, so both builds are hydratable. Vue, React and Svelte need nothing more.
Build and start it
npx vite build && npx vite build --ssr
php artisan nitro:ssr
vite build --ssr bundles the entry to bootstrap/ssr/ssr.js.
php artisan nitro:ssr runs that bundle with Node, on the port in nitro.ssr.url.
Then turn server rendering on:
NITRO_SSR=true
Configuration
// config/nitro.php
'ssr' => [
'enabled' => env('NITRO_SSR', false),
/** Where nitro:ssr listens, and where Laravel asks. */
'url' => env('NITRO_SSR_URL', 'http://127.0.0.1:13714'),
/** Seconds a view may take to render before the browser is left to render it. */
'timeout' => 2,
/** What nitro:ssr runs: the bundle `vite build --ssr` made. */
'bundle' => base_path('bootstrap/ssr/ssr.js'),
],
| Option | Default | What it does |
|---|---|---|
enabled
|
false
|
Whether pages ask the render service. Off, the browser renders every JavaScript view |
url
|
http://127.0.0.1:13714
|
Where the service listens.
nitro:ssr
takes its port from here
|
timeout
|
2
|
Seconds a view may take before the page goes out without it |
bundle
|
bootstrap/ssr/ssr.js
|
What
nitro:ssr
runs.
php artisan nitro:ssr --bundle=...
runs another
|
The service listens on 127.0.0.1 only: Laravel is its one client. GET /health answers
with the engines it serves, for a health check.
When it can't render
Server rendering never holds a page up, and never breaks one. Each case falls back to what the page does without it:
| When | What happens |
|---|---|
| The service isn't running | The connection is refused at once. The request's views go to the browser, and Laravel logs a warning |
| It is slow |
After
timeout
seconds the view goes to the browser. The rest of the request doesn't ask again
|
| A view throws while it renders | That view goes to the browser, and the error is logged. The other views are served |
| The entry names no renderer for an engine | Those views are the browser's. That is not an error: serve only the frameworks you want rendered |
| The state can't be sent as JSON | The view goes to the browser, with a warning naming it |
Writing views that render on the server
- A view only reads while it renders. Calling a method,
set(),upload()ordispatch()during the render throws on the server, and the view is left to the browser. Do them in event handlers, as you would anyway. - Browser-only code (
window,document,localStorage, measuring an element) goes where each framework runs code only in the browser: Vue'sonMounted, React'suseEffect, Solid'sonMount, Svelte's$effectoronMount. - The first render must match. A value that differs between the server and the browser (the current time, a random number) makes hydration find something else. Work it out after mounting, or put it on the component, where both sides read the same value.
- Validation messages are in the HTML. A page rendered after a failed form shows its
errorsin the first paint, as Blade does.
In production
Build both bundles in your deploy, then keep nitro:ssr running as a service, and restart it after
each deploy so it serves the new views. With Supervisor:
; /etc/supervisor/conf.d/nitro-ssr.conf
[program:nitro-ssr]
command=php /var/www/app/artisan nitro:ssr
directory=/var/www/app
user=www-data
autostart=true
autorestart=true
stdout_logfile=/var/www/app/storage/logs/ssr.log
redirect_stderr=true
Run supervisorctl restart nitro-ssr as a deploy step. Pages keep working while it restarts: their
views are rendered by the browser for those few moments.
A view engine of your own
The service serves any engine named in serve(). A renderer is an object with
render(view, handle) that returns the view's HTML; handle is what the browser's first
render reads (handle.get(name), handle.errors(), the component's names), made from the
state Laravel sent:
// resources/js/ssr.js: a renderer for a view engine of your own
import { serve } from 'nitro/ssr';
import { renderToString } from 'my-framework/server';
const views = import.meta.glob('../views/mine/**/*.mine', { eager: true });
serve({
mine: {
/** The view's name, and the component as the browser's first render reads it: handle.get('title'). */
async render(view, handle) {
const module = Object.entries(views).find(([path]) => path.endsWith(`/${view}.mine`))?.[1];
return renderToString(module.default, { get: handle.get, errors: handle.errors() });
},
},
});
On the browser side, the engine's mount() is told { hydrate: true } when the element
holds the server's HTML. See a view engine of your own.