On this page
Inertia.js lets you build the public side of a website as a React, Vue or Svelte app while keeping the routing and data on the server. Routes return page components with their data instead of HTML, so there is no separate API to build. October CMS 4 is built on Laravel, so Inertia's Laravel adapter works with it directly. October CMS provides the admin panel, plugins, Tailor and the data, and Inertia with React and Vite renders the website in place of an October CMS theme.
This article is based on the October CMS Inertia template by Biz-Mark, and includes every file it adds, so you can set up the same project from the steps below.
How It Works
In a standard October CMS project, the website is built from a theme, with CMS pages, layouts and partials. This approach replaces the theme with a React app:
- The website's pages are React components in
resources/js/pages, rendered by Inertia. - Routes in
app/routes.phpdecide which page to show and pass it data. - There is no
themesdirectory, and the CMS and Editor modules aren't installed. - The
package.jsonfile belongs to the React app, rather than October's own asset build.
October CMS still provides everything on the server: the configuration, routing, middleware and console commands, the admin panel, plugins, Tailor and the database.
Project Structure
These are the files the setup adds to an October CMS project, or replaces in it.
| File | Purpose |
|---|---|
composer.json |
The PHP packages, with October's modules and Inertia's Laravel adapter |
package.json |
The front-end packages and the scripts that run Vite |
vite.config.js |
Builds the React app into app/assets/build |
app/routes.php |
Routes that return Inertia pages. October CMS loads this file automatically. |
app/http/middleware/HandleInertiaRequests.php |
Inertia's middleware, which names the root view and shares data with every page |
app/views/app.blade.php |
The root view, which loads the scripts and mounts the app |
resources/js/app.js |
The entry point of the React app |
resources/js/layouts/default.jsx |
A layout shared by the pages |
resources/js/pages/index.jsx |
The home page |
resources/js/pages/about.jsx |
An about page |
resources/css/app.css |
The website's styles |
Create the Project
Create a new October CMS project with Composer.
composer create-project october/october myproject
cd myproject
The new project includes copies of the CMS and Editor modules, the demo theme and the demo plugin, which this approach doesn't use. Remove them.
rm -rf modules/cms modules/editor themes plugins/october
The October CMS modules are installed from the October CMS gateway. Set your License Key, which adds the gateway as a Composer repository along with your credentials for it, and links the project to your license so the admin panel can install plugins and updates.
php artisan project:set YOUR-LICENSE-KEY
Install the modules the project uses, which leaves out the CMS and Editor modules, along with Inertia's Laravel adapter.
composer require october/system:^4.4 october/backend:^4.4 october/media:^4.4 october/tailor:^4.4 october/dashboard:^4.4 inertiajs/inertia-laravel:^3.0
The require section of composer.json then reads as follows.
"require": {
"php": "^8.2",
"october/rain": "^4.4",
"laravel/framework": "^12.0",
"october/system": "^4.4",
"october/backend": "^4.4",
"october/media": "^4.4",
"october/tailor": "^4.4",
"october/dashboard": "^4.4",
"inertiajs/inertia-laravel": "^3.0"
}
Composer installs the modules into the modules directory, so add it to .gitignore along with vendor, and run composer install to recreate both on another computer.
/modules
Set the database connection in the .env file, then create the database tables.
php artisan october:migrate
Tip: To add Inertia to an existing project that keeps its theme instead, skip the steps that remove the modules and theme, and only install
inertiajs/inertia-laravel.
Add the Front-End Packages
Replace the package.json file in the project root, which belongs to October's own asset build in a new project, with the following.
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build --emptyOutDir",
"dev": "vite"
},
"devDependencies": {
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.0.0",
"vite": "^8.0.0"
},
"dependencies": {
"@inertiajs/react": "^3.0.3",
"@inertiajs/vite": "^3.0.3",
"@vitejs/plugin-react": "^6.0.1",
"react": "^19.2.5",
"react-dom": "^19.2.5"
}
}
Then install the packages.
npm install
Add the Middleware
Inertia's middleware names the root view and lists the data shared with every page. Create it at app/http/middleware/HandleInertiaRequests.php. The commented lines share the signed-in user, once the RainLab.User plugin is installed.
<?php namespace App\Http\Middleware;
//use Auth; <-- uncomment after rainlab.user installed
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
*
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'name' => config('app.name'),
'auth' => [
// 'user' => Auth::getUser() <-- uncomment after rainlab.user installed
]
];
}
}
Add the Routes
Create app/routes.php with routes that render pages. Each route uses the web middleware group, for sessions and CSRF protection, along with the Inertia middleware. The name passed to Inertia::render is the page component, so about renders resources/js/pages/about.jsx.
<?php
Route::group(['middleware' => ['web', \App\Http\Middleware\HandleInertiaRequests::class]], function () {
Route::get('', fn () => \Inertia\Inertia::render('index'));
Route::get('about', fn () => \Inertia\Inertia::render('about'));
});
Add the Root View
The root view is the HTML page that loads the scripts and styles, and holds the first page's data. Create it at app/views/app.blade.php. October CMS looks for views in app/views, so the app root view named in the middleware resolves to this file.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<meta name="domain" content="{{ config('app.url') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
@inertiaHead
{{
\Illuminate\Support\Facades\Vite::useHotFile('app/assets/hot')
->useBuildDirectory('app/assets/build')
->reactRefresh()
}}
{{
\Illuminate\Support\Facades\Vite::useHotFile('app/assets/hot')
->useBuildDirectory('app/assets/build')
->withEntryPoints(['resources/css/app.css', 'resources/js/app.js'])
}}
</head>
<body>
<script id="inertia-page" type="application/json">
{!! json_encode(
$page,
JSON_HEX_TAG
| JSON_HEX_AMP
| JSON_HEX_APOS
| JSON_HEX_QUOT
| JSON_UNESCAPED_UNICODE
| JSON_INVALID_UTF8_SUBSTITUTE
) !!}
</script>
<div id="app"></div>
</body>
</html>
The view points Vite at the app/assets directory: app/assets/hot while the development server runs, and app/assets/build for the built files. The first page's data is written as JSON in a script tag, which the app reads when it starts.
Configure Vite
Create vite.config.js in the project root. The Laravel plugin reloads the browser when the views or front-end files change. Its build directory is relative to the app/assets directory, so the built files are written to app/assets/build and their addresses start with /app/assets/build.
import inertia from '@inertiajs/vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
publicDirectory: './app/assets',
buildDirectory: '../../app/assets/build',
refresh: [
'./resources/**',
'./app/views/**'
],
}),
react(),
inertia(),
],
});
Create the React App
The entry point at resources/js/app.js reads the first page's data from the root view and starts the app. Inertia's Vite plugin finds the page components in resources/js/pages, so no page resolver is needed.
import { createInertiaApp } from '@inertiajs/react'
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
// Replace the default attribute placement of data inside the script with clean JSON.
const parseInitialInertiaPage = () => {
if (typeof document === 'undefined') {
return undefined;
}
const initialPageElement = document.getElementById('inertia-page');
const payload = initialPageElement?.textContent;
if (!payload) {
return undefined;
}
try {
return JSON.parse(payload);
} catch (error) {
console.error('Failed to parse initial Inertia payload', error);
return undefined;
}
};
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
page: parseInitialInertiaPage(),
strictMode: true
});
The layout at resources/js/layouts/default.jsx wraps each page. Inertia's Link component changes pages without a full reload.
import { Link } from "@inertiajs/react"
export default function Layout({ children })
{
return (
<main>
<header>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
</header>
<article>{children}</article>
</main>
)
}
Each page is a component in resources/js/pages. The @ prefix points to resources/js. The home page is resources/js/pages/index.jsx.
import Layout from "@/layouts/default";
import { Head } from "@inertiajs/react";
export default function Index()
{
return (
<Layout>
<Head title="Welcome" />
<h1>Welcome</h1>
<p>Hello, welcome to your first Inertia + October CMS app!</p>
</Layout>
);
}
The about page at resources/js/pages/about.jsx keeps a counter in React state, which stays in the browser as you click.
import Layout from "@/layouts/default";
import { Head } from "@inertiajs/react";
import {useState} from "react";
export default function About()
{
const [clicks, setClicks] = useState(0);
return (
<Layout>
<Head title="About" />
<h1>About</h1>
<p>This is basic about page with counter!</p>
<p>Current counter {clicks}</p>
<button onClick={() => setClicks(clicks + 1)}>add 1</button>
</Layout>
);
}
The styles go in resources/css/app.css, which the root view loads with the app.
body {
background-color: #d3d3d3;
}
main {
background-color: #ffffff;
padding: 1rem;
}
header {
display: flex;
gap: .5rem;
}
Run the Development Server
Start Vite while you work on the website. It writes the app/assets/hot file, so the root view loads the scripts from the development server with hot reloading.
npm run dev
The October CMS admin panel stays at /admin, or the backend URI set in the .env file. Open it to create your first administrator account.
Passing Data to Pages
A route passes data to its page as props, which the component receives as arguments.
Route::get('about', fn () => \Inertia\Inertia::render('about', [
'title' => 'About Us',
]));
export default function About({ title })
{
return <h1>{title}</h1>;
}
Data that every page needs, such as the signed-in user, belongs in the middleware's share method, as in the commented lines above.
Building for Production
Build the app before deploying. The files are written to app/assets/build, and the root view loads them from there when the development server isn't running.
npm run build
The .htaccess file in October CMS 4 already serves files in app/assets publicly. On Nginx, add a location for the directory alongside the other static file locations, or the built files return a 404 error.
location ~ ^/app/(assets|resources) { try_files $uri 404; }
Keeping the Approaches Apart
The template recommends keeping a project to one approach, rather than mixing a theme with the React app:
- Keep the website's pages in
resources/js/pages, not in a theme. - Don't install the CMS and Editor modules again, unless a project has a specific need for them.
- Keep
package.jsonfor the front-end app, rather than October's own asset build.
See Also
- Inertia.js Documentation
- October CMS Inertia Template, the original source of this setup by Biz-Mark
- Directory Structure
- Web Server Configuration
Was this article helpful?