jawad-topdot/laravel-grapesjs
GrapesJS integration for Laravel that adds a visual drag-and-drop page builder to your app. Design HTML templates in the browser, save them to Eloquent models, and render them in Blade. Ships with controllers, routes, migrations, and views for quick setup.
Install and publish the package assets/config:
composer require jawad-topdot/laravel-grapesjs
php artisan vendor:publish --provider="JawadTopdot\LaravelGrapesjs\LaravelGrapesjsServiceProvider"
php artisan storage:link
Where to look first, in order:
config/grapesjs.php — this is the control panel. It decides which GrapesJS plugins/presets load, editor height, and the storage/upload wiring. Read this before writing any editor code.resources/views/vendor/...) — the editor is bootstrapped there; you will almost always copy or override it rather than call grapesjs.init() yourself.php artisan route:list | grep -i grapes) — upload/storage endpoints are usually registered by the service provider. Put them behind auth middleware.Minimal first use case — a "Page builder" CRUD:
php artisan make:model Page -mcr
// migration
$table->string('title');
$table->longText('html')->nullable();
$table->longText('css')->nullable();
$table->json('project_data')->nullable(); // to re-open the editor later
{{-- resources/views/pages/edit.blade.php --}}
<form method="POST" action="{{ route('pages.update', $page) }}" id="page-form">
@csrf @method('PUT')
<input type="hidden" name="html" id="page-html">
<input type="hidden" name="css" id="page-css">
<input type="hidden" name="project_data" id="page-project">
{{-- the editor component/partial shipped by the package --}}
<x-grapesjs :height="700" />
</form>
Then wire the submit handler (see next section). The first win is: load the editor, drag a few blocks, hit save, and {!! $page->html !!} renders on a public route. Get that loop working before touching plugins or custom blocks.
1. Save the two representations, not one. Save getHtml() + getCss() for rendering, and getProjectData() (JSON) for editing. HTML/CSS alone loses component structure — users can't cleanly re-edit blocks.
const editor = grapesjs.init({ container: '#gjs', /* config from package */ });
// hydrate on edit
editor.setComponents(@json($page->html ?? ''));
editor.setStyle(@json($page->css ?? ''));
// serialize on submit
document.getElementById('page-form').addEventListener('submit', () => {
document.getElementById('page-html').value = editor.getHtml();
document.getElementById('page-css').value = editor.getCss();
document.getElementById('page-project').value = JSON.stringify(editor.getProjectData());
});
// PageController@update
$data = $request->validate([
'html' => ['nullable', 'string'],
'css' => ['nullable', 'string'],
'project_data' => ['nullable', 'json'],
]);
$page->update($data);
2. Render through a single partial. Never dump html/css in several places. One resources/views/pages/render.blade.php that scopes CSS:
<div class="gjs-content">
<style>{!! $page->css !!}</style>
{!! $page->html !!}
</div>
Because GrapesJS emits bare selectors (h1 { ... }, .card { ... }), wrap and prefix: str_replace(['h1','.card'], ['.gjs-content h1','.gjs-content .card'], $css) or, more robustly, render inside a sandboxed iframe route.
3. Configure plugins in config, not in Blade. Day-to-day work happens by editing config/grapesjs.php:
'plugins' => [
'grapesjs-preset-webpage',
'grapesjs-blocks-basic',
'grapesjs-plugin-forms',
'grapesjs-tabs',
],
'pluginsOpts' => [
'grapesjs-preset-webpage' => ['modalImportTitle' => 'Import'],
],
Load the matching JS/CSS from npm/CDN consistently — plugin versions must match the bundled GrapesJS version.
4. Storage manager for image uploads. Point GrapesJS at the package's upload route (or your own) so images land in storage/app/public/pages:
const editor = grapesjs.init({
container: '#gjs',
storageManager: {
type: 'remote',
stepsBeforeSave: 1,
options: {
remote: {
urlStore: '/grapesjs/upload', // confirm exact route name
urlLoad: '/grapesjs/assets',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
fetchOptions: (opts) => ({ ...opts, credentials: 'same-origin' }),
},
},
},
assetManager: { upload: '/grapesjs/upload', uploadName: 'files[]', autoAdd: true },
});
Store only relative paths in the DB (/storage/pages/abc.png) and run them through Storage::url() at render time so a domain move doesn't break every image.
5. Custom blocks backed by Blade partials. The highest-leverage pattern: expose your app's data to the editor.
editor.BlockManager.add('latest-posts', {
label: 'Latest posts',
category: 'Dynamic',
content: '<div data-dynamic="latest-posts">Loading…</div>',
});
Then a small JS on('load') pass (or a dedicated Blade partial renderer) replaces [data-dynamic] placeholders on the public side, so editors place "components" while the server supplies the data. Keep the placeholder contract stable — it's the seam between editor and app.
6. Revisions for a page builder. Because a stray drag can wreck a layout, add page_revisions and write a snapshot on every save (or on a throttled editor.on('update') debounce). A "Restore" button is a 10-line controller method and saves you support time.
7. Embedding in Livewire/Filament. The editor must not be re-rendered by Livewire. Wrap the container in wire:ignore, initialize GrapesJS once, and push state into Livewire with @this.set('html', editor.getHtml()) on a debounced update event. Otherwise the iframe is destroyed on every render.
8. Auth + authorization first. Editor routes (create, edit, upload) should sit behind auth and a policy ($this->authorize('update', $page)). Public rendering goes through a separate read-only route.
grapesjs-preset-webpage, grapesjs-blocks-basic, etc.) in package.json and verify against the version the package's view loads. Plugin/preset mismatches are the #1 source of "editor loads but blocks are blank" reports.X-CSRF-TOKEN header or a fetch without credentials: 'same-origin'. Add a <meta name="csrf-token"> and pass it via headers.editor.on('load') plus postMessage, or drive the host via editor.Commands.getCss() produces global selectors. Without scoping or an iframe render, your saved page CSS will rewrite the surrounding app's typography. Always render inside a wrapper class (prefix selectors) or a sandboxed iframe.{!! !!}. Either restrict editing to trusted admins, or pass content through HTMLPurifier (mews/purifier) before render. Do not skip this if editors are end users.getHtml() vs getProjectData(). HTML/CSS is for output; project JSON is for editing. Saving only HTML means "re-open in editor" starts from flat markup and blocks break.http://localhost:8000/storage/... into saved content. Normalize on save (str_replace(config('app.url'), '', $html)) and resolve at render.config/grapesjs.php, run php artisan config:clear; after overriding the package view, php artisan view:clear. A stale published view is a common "my change did nothing" cause.php artisan route:list before adding your own /grapesjs/* routes, and if you need custom behavior, disable/override the package route in config/grapesjs.php rather than shadowing it.['web', 'auth'] in config or re-register it in your own routes/web.php.beforeunload when editor.getDirtyCount() > 0. Editors lose work on accidental back-navigation otherwise.storage:link and APP_URL. Broken images in the asset manager are usually an unlinked storage dir or an APP_URL that doesn't match where you serve. Check Storage::url() output first.public/vendor/grapesjs and reference them locally.@push('scripts') after grapesjs.init.How can I help you explore Laravel packages today?