Why GrapesJS fits Laravel
GrapesJS is a framework-agnostic, MIT-licensed editor that runs entirely in the browser, so it pairs cleanly with Laravel: Laravel serves the editor page and exposes a couple of routes to load and store content, while GrapesJS handles the drag-and-drop UI on the client. This guide builds a working setup — a Blade view that hosts the editor, a controller that persists content with CSRF protection, and HTML/CSS export.
1. Load GrapesJS in a Blade view
The fastest start is a CDN include. Create resources/views/editor.blade.php
and add a CSRF meta tag so the save request can authenticate.
<!doctype html>
<html>
<head>
<meta name="csrf-token" content="{{ csrf_token() }}">
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet">
</head>
<body>
<div id="gjs"><h1>Edit me in GrapesJS</h1></div>
<script src="https://unpkg.com/grapesjs"></script>
<script src="/js/editor.js"></script>
</body>
</html>
Prefer a bundled asset pipeline? npm install grapesjs and import it
through Vite instead of the CDN — the editor code is identical.
2. Initialise the editor with remote storage
In public/js/editor.js, read the CSRF token from the meta tag and
configure the Storage Manager to POST to Laravel.
const csrf = document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
const editor = grapesjs.init({
container: '#gjs',
height: '100vh',
fromElement: true,
storageManager: {
type: 'remote',
stepsBeforeSave: 3,
options: {
remote: {
urlStore: '/editor/save',
urlLoad: '/editor/load',
headers: { 'X-CSRF-TOKEN': csrf },
},
},
},
});
3. Add the Laravel routes and controller
Register the page plus the load/store endpoints in routes/web.php:
use App\Http\Controllers\EditorController;
Route::get('/editor', [EditorController::class, 'show']);
Route::get('/editor/load', [EditorController::class, 'load']);
Route::post('/editor/save', [EditorController::class, 'save']);
The controller stores the GrapesJS project JSON (and optionally the rendered HTML/CSS) on a model:
class EditorController extends Controller
{
public function show()
{
return view('editor');
}
public function load()
{
$page = Page::firstOrCreate(['key' => 'home']);
// GrapesJS expects the stored project data back as JSON.
return response()->json($page->project ?? []);
}
public function save(Request $request)
{
$data = $request->all();
Page::updateOrCreate(
['key' => 'home'],
[
'project' => $data, // full editable project
'html' => $data['gjs-html'] ?? null,
'css' => $data['gjs-css'] ?? null,
]
);
return response()->json(['status' => 'ok']);
}
}
Because the request carries the X-CSRF-TOKEN header, Laravel's
web middleware group accepts the POST without disabling CSRF
protection. Store project as a JSON/`json` column so it round-trips
cleanly.
4. Render the exported page
To publish, read the stored HTML and CSS in a Blade view:
<style>{!! $page->css !!}</style>
{!! $page->html !!}
Treat stored markup as trusted only if your editors are trusted; sanitise on output if end users can edit pages.
Prerequisites
You'll need PHP 8.1+ and a Laravel 10 or 11 application. Node.js is optional — you can load GrapesJS from a CDN in a Blade view, or bundle it through Vite if you prefer an asset pipeline. No Laravel-specific GrapesJS package is required; the editor is plain browser JavaScript that talks to your routes over HTTP. A working understanding of Blade, routes, controllers, and CSRF is enough to ship this integration.
Add custom blocks to the editor
Give your editors something to drag onto the canvas by registering blocks with
the Block Manager after grapesjs.init:
editor.BlockManager.add('hero', {
label: 'Hero section',
category: 'Sections',
content: '<section class="hero"><h1>Headline</h1><p>Copy</p></section>',
});
editor.BlockManager.add('cta', {
label: 'Call to action',
category: 'Sections',
content: '<div class="cta"><a href="#">Get started</a></div>',
});
For a richer set of production-ready blocks, presets, and storage helpers, pull plugins from GJS.Market rather than rebuilding common patterns by hand.
Store the project in a JSON column
Persist the full editable project so a page re-opens in the editor exactly as it was, and keep the rendered HTML/CSS in their own columns for fast serving. A migration:
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->json('project')->nullable(); // full GrapesJS project
$table->longText('html')->nullable();
$table->longText('css')->nullable();
$table->timestamps();
});
public function save(Request $request)
{
$data = $request->all();
Page::updateOrCreate(
['key' => 'home'],
[
'project' => $data,
'html' => $data['gjs-html'] ?? null,
'css' => $data['gjs-css'] ?? null,
]
);
Cache::forget('page:home'); // bust the published cache
return response()->json(['status' => 'ok']);
}
Authorize the save route
CSRF protects against forged requests, but you still need authorization so only the right users can edit a page. Gate the route with a policy or middleware:
Route::post('/editor/save', [EditorController::class, 'save'])
->middleware(['auth', 'can:edit-pages']);
Render the published page (cached)
Serve the stored HTML/CSS from cache so visitors never pay the editor's render cost, and invalidate it on save:
public function show()
{
$page = Cache::remember('page:home', 3600, fn () =>
Page::where('key', 'home')->first()
);
return view('page', ['page' => $page]);
}
Performance tips
Load GrapesJS only on the editor screen, not site-wide, and defer its script so
it never blocks first paint. Cache the rendered output (Laravel cache or a CDN)
and bust it on save. If exporting or post-processing HTML is heavy, push it to a
queued job so the save request returns immediately. Store project as
a real json/jsonb column so partial reads stay cheap.
Security considerations
Keep CSRF protection on — send the token in the X-CSRF-TOKEN header
rather than disabling VerifyCsrfToken. Authorize every state-changing
route. Treat stored markup as untrusted if non-admins can edit pages: sanitise it
on output (for example mews/purifier) instead of echoing raw HTML.
Validate the incoming payload size so a huge project can't exhaust memory.
Troubleshooting common errors
419 Page Expired means the CSRF token is missing — confirm the
csrf-token meta tag is present and sent as X-CSRF-TOKEN.
An unstyled canvas means the GrapesJS stylesheet didn't load.
A blank editor usually means the container selector didn't match
an element on the page. 413 Payload Too Large means the project
JSON exceeds your upload/body limit — raise post_max_size or the
framework limit.
When to use GrapesJS with Laravel
GrapesJS fits when your Laravel app needs an embedded visual editor your users control — landing pages, email templates, a white-label page builder — backed by your own database and served from your own infrastructure. For simple rich-text inside a form field, a lightweight WYSIWYG is enough; for full-page composition with layout, styling, and clean HTML/CSS export, GrapesJS is the stronger, MIT-licensed, self-hosted choice.
Production tips
A few choices make a Laravel + GrapesJS setup production-ready. Store the editor project in a json column (or jsonb on PostgreSQL) so it round-trips losslessly, and keep the rendered html/css in separate columns you can serve directly. Cache that rendered output (Laravel cache or a CDN) and bust it on save, so visitors never wait on the editor. Keep CSRF protection on — send the token in the X-CSRF-TOKEN header rather than disabling VerifyCsrfToken. Finally, if editors can be untrusted, sanitise stored markup on output (e.g. mews/purifier) instead of echoing raw HTML, since GrapesJS faithfully stores whatever was pasted in.
Next steps
Add blocks and plugins to match your design system. See the related GrapesJS + React and GrapesJS + Vue guides, browse storage adapter plugins and the full GrapesJS marketplace, or start from the GJS.Market home page.
FAQ
How do I save GrapesJS content to a Laravel backend?
Configure the Storage Manager with type: 'remote' and point
urlStore/urlLoad at Laravel routes. Send the CSRF token
in the X-CSRF-TOKEN header, then store the html, css, and project
JSON on a model.
Does GrapesJS need a build step in Laravel?
No. Load it from a CDN in a Blade view for the simplest setup, or import it via Vite/Laravel Mix if you prefer a bundled pipeline. The editor is plain browser JavaScript either way.
How do I handle CSRF when GrapesJS posts to Laravel?
Add a csrf-token meta tag to your layout and pass its value as the
X-CSRF-TOKEN header in the Storage Manager's headers
option. Laravel's web middleware then accepts the save request.