Server Side Rending
Laravext offers two ways to server side render you application: a javascript runtime and a blade engine based. If for some reason the need arises, you can even use both at the same time for different routes.
To sum up, the javascript runtime is quite similar to how Inertia.js' Server Side Rendering works. You create a ssr.(js|ts|jsx|tsx)
file which will be used by a node process to render the page being visited and sending it to the client, where it'll be replaced when the javascript loads up.
The blade engine based is quite similar to how you'd traditionally render a blade view, and the contents are later replaced when the javascript starts running. Although handy, it can become cubersome for complex pages where you'll have to essentially replicate whatever you have for you React or Vue components in blade.
Note: You can also use the loading.html file convention for simpler html loadings while your javascript doesn't kick in.Blade Engine Based
The blade engine based methods you can use are througly explained in the Tools/Blade Directives/@startNexus and @endNexus, Tools/Blade Directives/@startStrand and @endStrand, Tools/Nexus Response/withViewSkeleton and Tools/Nexus Response/withHtmlSkeleton sections of this documentation.
Javascript Runtime
In order to use the Javascript Runtime mode of SSR, you'll need to modify/create some files.
Vite Configuration
First, you'll have to add the ssr
attribute of your laravel
plugin configuration to point to the file that will be used to render the page, like ./resources/js/ssr.(js|jsx|ts|tsx)
.
import { defineConfig, loadEnv } from "vite";
import laravel, { refreshPaths } from "laravel-vite-plugin";
// For React
import react from "@vitejs/plugin-react";
// For Vue
import vue from "@vitejs/plugin-vue";
export default function ({ mode }) {
const env = loadEnv(mode, process.cwd());
const host = env.VITE_APP_ENV == "local" ? env.VITE_APP_DOMAIN : undefined;
return defineConfig({
server: { host },
plugins: [
laravel({
input: [
"resources/css/app.css",
"resources/js/app.(js|jsx|ts|tsx)",
],
refresh: [...refreshPaths, "resources/js/**", "app/**"],
// Add this line below to your configuration, and change the name and extension to match your file, of course
ssr: "resources/js/ssr.(js/jsx/ts/tsx)",
}),
// For React
react(),
// For Vue
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});
}
SSR Entry Point
You'll then need to create a file, like ssr.(js|jsx|ts|tsx)
in your resources/js
folder (needless to say, you can change the name and extension to whatever you want, as long as it matches the configuration above). This file must contain a serve
function that will be called by the node process to render the page. It'll receive an object with the window
and cookies
objects, and must return a createLaravextSsrApp
function, which will be used to render the page.
The window
object will contain the __laravext
object, which will contain all the attributes mentioned in the Concepts/Laravext Prop section of this doc, such as the file conventions, props, etc. The cookies
object will contain the cookies sent by the client, so if you use it for something, like for example to keep track of unauthenticated users' language preference, you can use it here (the examples below show how to use it for internationalization using the i18next
/vue-i18n
libraries).
The createLaravextSsrApp
function will receive an object with the following mandatory attributes (assuming you'll make use of the @nexus
and @strand
blade directives):
nexusResolver
: A function that will receive the name of the nexus component and must return the component. You can use theresolveComponent
function from the@laravext/react/tools
or@laravext/vue3/tools
to help you with that.strandsResolver
: A function that will receive the name of the strand component and must return the component. You should use the sameresolveComponent
function as above.laravext
: The__laravext
object from thewindow
object.document
: Thedocument
object from thewindow
object.
There're some optional attributes included in the examples below that either commented or have comments explaining what they do. You can use them to set up global variables, internationalization, cookies, etc.
- React
- Vue
ssr.jsx
:
import { createLaravextSsrApp } from "@laravext/react";
import { resolveComponent } from "@laravext/react/tools";
import { serve } from "@laravext/react/server";
import { route } from "../../vendor/tightenco/ziggy/src/js";
import i18n from "i18next";
import { initReactI18next, I18nextProvider } from "react-i18next";
import pt from "../../lang/pt.json";
serve(({ window, cookies }) =>
createLaravextSsrApp({
// This is optional, the default is renderToString from 'react-dom/server',
// but you can use renderToStaticMarkup if you want
// render: renderToString,
nexusResolver: (name) =>
resolveComponent(
`./nexus/${name}`,
import.meta.glob("./nexus/**/*")
),
strandsResolver: (name) =>
resolveComponent(
`./strands/${name}.jsx`,
import.meta.glob("./strands/**/*.jsx")
),
// The beforeSetup function is executed once, before any of the setups.
// You can use this to set up global variables or anything else, such as
// internationalization, cookies, etc.
beforeSetup: ({ laravext }) => {
if (laravext?.page_data?.shared_props?.ziggy) {
global.route = (name, params, absolute) =>
route(name, params, absolute, {
...laravext.page_data.shared_props.ziggy,
url: laravext.page_data.shared_props.ziggy.url,
});
global.Ziggy = laravext.page_data.shared_props.ziggy;
}
let user = laravext.page_data?.shared_props?.auth?.user;
const i18nInstance = i18n.createInstance();
i18nInstance.use(initReactI18next).init({
resources: {
pt: {
translation: pt,
},
},
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
i18nInstance.changeLanguage(
user?.locale ?? cookies["locale"] ?? "en"
);
laravext.app.i18n = i18nInstance;
},
// This setup is applied to all components, including nexus and strands
setup: ({ component, laravext }) => {
return (
<I18nextProvider i18n={laravext.app.i18n}>
{component}
</I18nextProvider>
);
},
// This setup is applied to all components, including nexus and strands
// setup: ({ component, laravext }) => {
// return <AnyComponentOrProvider>
// {component}
// </AnyComponentOrProvider>
// },
/// The setupNexus function is applied only to the nexus component, after the 'setup'
// function, unless reverseSetupOrder is true
// setupNexus: ({ nexus, laravext }) => {
// return <AnyComponentOrProvider>{nexus}</AnyComponentOrProvider>;
// },
// The setupStrand function is applied only to the strand components, after the 'setup' function,
// unless reverseSetupOrder is true.
// The 'strandData' parameter is the data passed to the strand component from the blade
// where it was located, if applicable.
// setupStrand: ({strand, laravext, strandData}) => {
// return <AnyComponentOrProvider>{strand}</AnyComponentOrProvider>
// },
// If you want to reverse the order of the setup functions, set this to true
// reverseSetupOrder: true,
// Don't forget to pass the window object to the laravext object
laravext: window.__laravext,
document: window.document,
})
);
ssr.js
:
import "./bootstrap";
import "../css/app.css";
import { createI18n } from "vue-i18n";
import pt from "./../../lang/pt.json";
import {
plugin as fkPluging,
defaultConfig as fkDefaultConfig,
} from "@formkit/vue";
import VueSweetalert2 from "vue-sweetalert2";
import fkConfig from "./../../formkit.theme.js";
import "sweetalert2/dist/sweetalert2.min.css";
import { resolveComponent } from "@laravext/vue3/tools";
import { serve } from "@laravext/vue3/server";
import { createLaravextSsrApp } from "@laravext/vue3";
import { route } from "../../vendor/tightenco/ziggy/src/js";
serve(({ window, cookies }) =>
createLaravextSsrApp({
// This is optional, the default is renderToString from '@vue/server-renderer',
// but you can use renderToStaticMarkup if you want
// render: renderToString,
nexusResolver: (name) =>
resolveComponent(
`./nexus/${name}`,
import.meta.glob("./nexus/**/*")
),
strandsResolver: (name) =>
resolveComponent(
`./strands/${name}.vue`,
import.meta.glob("./strands/**/*.vue")
),
// The beforeSetup function is executed once, before any of the setups.
// You can use this to set up global variables or anything else, such as
// internationalization, cookies, etc.
beforeSetup: ({ laravext }) => {
if (laravext?.page_data?.shared_props?.ziggy) {
global.route = (name, params, absolute) =>
route(name, params, absolute, {
...laravext.page_data.shared_props.ziggy,
url: laravext.page_data.shared_props.ziggy.url,
});
global.Ziggy = laravext.page_data.shared_props.ziggy;
}
let user = laravext.page_data?.shared_props?.auth?.user;
let locale = user?.locale ?? cookies["locale"] ?? "en";
const i18n = createI18n({
legacy: false,
locale: locale,
fallbackLocale: "en",
messages: {
pt,
},
});
laravext.app.i18n = i18n;
},
// This setup is applied to all components, including nexus and strands
setup: ({ app, laravext }) => {
app.use(laravext.app.i18n);
app.use(VueSweetalert2);
app.use(fkPluging, fkDefaultConfig(fkConfig));
return app;
},
// The setupNexus function is applied only to the nexus component, after the 'setup'
// function, unless reverseSetupOrder is true
// setupNexus: ({ nexus, laravext }) => {
// // Anything you want to do with the nexus app instance
// nexus.use(Something)
// return nexus;
// },
// The setupStrand function is applied only to the strand components, after the 'setup' function,
// unless reverseSetupOrder is true.
// The 'strandData' parameter is the data passed to the strand component from the blade
// where it was located, if applicable.
// setupStrand: ({strand, laravext, strandData}) => {
// // Anything you want to do with the strand app instance
// strand.use(SomethingElse)
// return strand
// },
// If you want to reverse the order of the setup functions (for some reason), set this to true
// reverseSetupOrder: true,
// Don't forget to pass the window object to the laravext object
laravext: window.__laravext,
document: window.document,
})
);
Updating you npm script
Modify your package.json
file to include the vite build --ssr
command, like so:
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
// "build": "vite build", // This is how the default build script probably looks like
"build": "vite build && vite build --ssr" // This is the build script with SSR
}
}
Activate Laravext's SSR in the config file
In your ./config/laravext.php
file, set the ssr.enabled
to true
(or add an environment variable to control it, like LARAVEXT_JAVASCRIPT_SERVER_SIDE_RENDERING_ENABLED
), and set the ssr.url
to the URL where the server side rendering will be done.
// ./config/laravext.php
<?php
return [
// the rest of the laravext config
'ssr' => [
'enabled' => true,
// the rest of the laravext.ssr config
],
];
Starting you entry point
Now all you have to do is run this artisan command to start the node process that will render the page:
php artisan laravext:start-ssr
Make sure you have this command set up in your deployment pipeline, and that you've set it up as a background process with Supervisor or something similar.
Additionally, you should also stop the process in between deployments, by running:
php artisan laravext:stop-ssr
Granular Control
Let's say that for some reason you want to deactivate SSR for some routes, or activate it only for some routes. You can do that by setting the laravext.ssr.enabled
to 'only'
or 'except'
, and then setting the laravext.ssr.uris
or laravext.ssr.route_names
to the URIs or route names that should or shouldn't be SSR'd. Internaly, this is checked using the request()->is(config('laravext.ssr.uris', []))
or request()->routeIs(config('laravext.ssr.route_names', []))
, respectively.
A good example of this is when you have a component that "doesn't bahave well" with SSR, and you want to deactivate it for that page. A "real world" example is in the React example project, where the markdown editor for the /new
page doesn't work well with SSR, so it's deactivated for that page, and the laravext.ssr.enabled
is set to 'except'
and the laravext.ssr.uris
is set to ['new']
.
// ./config/laravext.php
return [
// the rest of the laravext config
'ssr' => [
/**
* You can also set it as 'only' or 'except' to specify the URIs that should(n't) be SSR'd,
* if for some reason you need this kind of control.
*/
'enabled' => 'except',
/**
* The URIs that should/should not be SSR'd. This validation is dependent on the enabled config.
*
* If enabled is set to 'only', the URIs listed here will be the only ones that will be SSR'd.
* If enabled is set to 'except', the URIs listed here will be the ones that won't be SSR'd.
*
* If enabled is set to true, this config will be ignored.
*
* Internally this is checked using the request()->is(config('laravext.ssr.uris', []))
*/
'uris' => [
'new',
// 'example/{uri}/pattern/*',
// 'another-example/{uri}'
],
/**
* The route names that should/should not be SSR'd. This validation is dependent on the enabled config.
*
* If enabled is set to 'only', the route names listed here will be the only ones that will be SSR'd.
* If enabled is set to 'except', the route names listed here will be the ones that won't be SSR'd.
*
* If enabled is set to true, this config will be ignored.
*
* Internally this is checked using the request()->routeIs(config('laravext.ssr.route_names', []))
*/
'route_names' => [
// 'route.name',
// 'route.name_pattern.*'
],
],
];