alaminfirdows/laravel-editorjs
A Laravel package for integrating Editor.js, the block-style content editor. It helps you store, parse, and render Editor.js JSON output in Laravel apps, making block-based content easier to use in models, controllers, and Blade views.
Install and publish
composer require alaminfirdows/laravel-editorjs
php artisan vendor:publish --tag=editorjs-config
php artisan vendor:publish --tag=editorjs-assets
Verify the service provider is auto-discovered (it is, in 2.x) and that config/editorjs.php exists — that file is the single most important thing to read first, since it defines the tool set, CDN/local asset paths, and any renderer mappings.
Where to look first
config/editorjs.php — the tools array is where every behavior decision lives.resources/views/vendor/editorjs — the Blade component that boots the JS.EditorJs cast / renderer class if you need to understand how stored JSON becomes HTML.First use case: a blog post body
// migration
$table->json('content')->nullable();
// model
protected function casts(): array
{
return ['content' => 'array'];
}
{{-- form --}}
<x-editorjs name="content" :value="$post->content" />
{{-- show page — new in v2.4.0 --}}
@editorJsRender($post->content)
That's the whole loop: component writes JSON blocks into a hidden input, the cast stores it as an array, and the blade directive renders it back to HTML server-side. v2.4.0's @editorJsRender() is the headline ergonomic win — it removes the need to inject the renderer service by hand in every view.
Rendering: directive vs. service
Prefer @editorJsRender($model->content) in views. Reach for the underlying renderer service when you need the HTML as a string — for emails, PDF generation, API resources, or search indexing:
$html = app(\AlaminFirdows\LaravelEditorJs\EditorJs::class)->render($post->content);
Because the directive outputs raw HTML (it must, to render markup), treat it exactly like {!! !!} and make sure the input is trusted or sanitized.
Cache the rendered HTML, not the JSON
Editor.js JSON is cheap to store but comparatively expensive to render on every request, especially with many blocks. A common pattern:
public function getContentHtmlAttribute(): string
{
return Cache::rememberForever(
"post:{$this->id}:html:{$this->updated_at->timestamp}",
fn () => app(EditorJs::class)->render($this->content ?? [])
);
}
Keying on updated_at gives you free invalidation. If you'd rather store it, add a content_html column and refresh it in a model observer on saved.
Custom tools
Every non-default block type needs two halves: a JS tool registration (client) and a renderer mapping (server). Add the JS to config/editorjs.php under tools and register the corresponding renderer so unknown blocks don't silently render as empty strings.
Warning tool (new in v2.4.0)
Editor.js's warning/callout block is now supported out of the box. If you maintain a custom warning renderer from before this release, remove it — leaving a duplicate key can shadow the bundled implementation and produce mismatched markup or styling between old and new content.
Livewire / Alpine
The component mounts on DOMContentLoaded. In Livewire, wrap the re-init in Livewire.hook('morph.updated', ...) or dispatch a browser event, otherwise a re-rendered form leaves you with a dead editor surface. Also give each instance a unique id — two editors on one page with the default ID will fight over the same mount point.
Validation
Editor.js output is a JSON array of blocks. Validate structurally rather than with json alone:
'content' => ['nullable', 'array'],
'content.*.type' => ['required', 'string'],
'content.*.data' => ['required', 'array'],
This stops a client from posting {"evil": "payload"} that your renderer might pass through unchecked.
Unknown block types render as nothing — silently. This is the number one debugging trap. If an author pastes content from another instance with a tool your config doesn't know about, the block vanishes from the output with no error. Add a dev-only assertion that logs unknown type values, and consider a fallback renderer that emits a visible placeholder in non-production environments.
Asset publishing after upgrade. v2.4.0 adds the warning tool and new Laravel/PHP targets. If you have published assets, re-publish (--force) or you'll be running an older JS bundle against a newer PHP renderer. Version skew between the two halves is the cause of most "renders fine locally, broken in prod" reports.
JSON column quirks. On MySQL the json column is fine; on SQLite it's text and returns a string. The array cast handles both, but raw DB::table() queries bypass the cast and hand you a string — always go through the model.
Empty and null content. @editorJsRender(null) and an empty block array should be guarded in your views. Wrap in @if(!empty($post->content)) rather than relying on the renderer to produce a sensible empty state.
XSS. The directive does not sanitize by default. If any content originates from untrusted users, pipe the output through an HTML sanitizer (e.g. mews/purifier) before echoing, and configure an allowlist that matches the tags your tools actually emit — including the warning block's markup.
Composer constraints on Laravel 12/13. v2.4.0 widens the framework constraint and adds PHP 8.4 support. If you're pinned via composer.lock behind an older Laravel, upgrading the package alone won't move the needle — run composer update alaminfirdows/laravel-editorjs -W and expect the framework to move with it.
Renderer extension point. The config's tool map is the intended extension hook; subclassing the renderer works but is fragile across minor releases. Prefer config + a small custom renderer class registered there.
Tests are now in the package (v2.4.0). Useful as executable documentation of expected block shapes. If you write custom tools, mirror the package's test fixtures rather than inventing your own JSON structure — it's the fastest way to catch a data-shape mismatch before your users do.
Debugging a blank editor. In order: (1) confirm the assets are published and the script tag is present in the rendered page, (2) check the browser console for a missing tool module rather than a PHP error, (3) verify the hidden input name matches your validation rule. Failures here are almost always client-side, and Laravel's logs will tell you nothing.
How can I help you explore Laravel packages today?