Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Editorjs Bundle Laravel Package

setono/editorjs-bundle

Editor.js integration for Symfony: renders and sanitizes Editor.js block content, offers form types and Twig helpers, and converts stored JSON into safe HTML for easy display in your templates.

View on GitHub
Deep Wiki
Context7

Getting Started

setono/editorjs-bundle is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no publishable config, no Blade directive, no facade. composer require setono/editorjs-bundle in a Laravel app installs an inert dependency: Laravel never boots Symfony bundles, and Laravel 12 pins Symfony 7.x transitively, so the v1.4.0 "allow Symfony 8" change is not even reachable from your framework. Do not add it as a dependency.

What you do with it is read it. The bundle is a compact, working reference for two things worth copying: the Editor.js OutputData JSON contract and the server-side render/sanitize path. Skim src/Form/ (form type + field mapping), src/DependencyInjection/ (the configurable tools array), and the Twig form theme — then reimplement that surface in ~150–300 lines you own.

Minimal path to first content:

npm i @editorjs/editorjs @editorjs/header @editorjs/list @editorjs/paragraph
php artisan make:migration add_body_json_to_posts --table=posts
// migration
$table->json('body')->nullable();

// app/Casts/Blocks.php
class Blocks implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): BlocksCollection
    {
        return BlocksCollection::fromJson($value ?? '{"blocks":[]}');
    }
    public function set($model, $key, $value, $attributes): array
    {
        return [$key => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)];
    }
}
{{-- resources/views/components/editorjs.blade.php --}}
@props(['name', 'value' => null])

<div class="editorjs" id="editorjs-{{ $name }}"></div>
<input type="hidden" name="{{ $name }}" id="input-{{ $name }}"
       value="{{ old($name, $value?->toJson()) }}">
@error($name) <p class="error">{{ $message }}</p> @enderror

@push('scripts')
<script type="module">
  import EditorJS from '@editorjs/editorjs';
  import Header from '@editorjs/header';
  import List from '@editorjs/list';

  const holder = document.getElementById('editorjs-{{ $name }}');
  const input  = document.getElementById('input-{{ $name }}');

  new EditorJS({
    holder: holder.id,
    tools: { header: Header, list: List },
    data: input.value ? JSON.parse(input.value) : undefined,
    placeholder: 'Write something…',
    onChange: async (api) => { input.value = JSON.stringify(await api.saver.save()); },
  });
</script>
@endpush

First use case: a blog/Post body field in an admin form. Mount the editor, save() JSON into the hidden input, validate against a block whitelist, persist to the json column, render through a Blade partial that switches on block type.

Implementation Patterns

Treat the bundle's FormType as a spec, not an import. Its job in Symfony is: render a container div, ship the editor assets, hydrate from a stored JSON string, and expose the payload as a form field value. In Laravel that decomposes cleanly:

Symfony bundle piece Laravel equivalent
EditorJSType form type Blade component + hidden input
Form field mapping / hydration Blocks cast + old() value
DI tools config array config/editorjs.php
Twig form theme + renderer Blade partial per block
Validator constraints Rule object

Config mirroring the bundle's tools array — keep the block whitelist and the JS tool names in one place, since they must agree:

// config/editorjs.php
return [
    'tools' => [
        'header'    => 'App\Blocks\HeaderBlock',
        'paragraph' => 'App\Blocks\ParagraphBlock',
        'list'      => 'App\Blocks\ListBlock',
    ],
];

Validation is the whole ballgame. Never trust the client's JSON — the hidden input is trivially editable. One Rule that checks shape and whitelists types:

class ValidBlocks implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $data = is_string($value) ? json_decode($value, true) : $value;
        if (!is_array($data) || !isset($data['blocks']) || !is_array($data['blocks'])) {
            $fail('Invalid Editor.js payload.'); return;
        }
        if (count($data['blocks']) > 500) { $fail('Too many blocks.'); return; }

        $allowed = array_keys(config('editorjs.tools'));
        foreach ($data['blocks'] as $b) {
            if (!in_array($b['type'] ?? '', $allowed, true)) {
                $fail("Block type [{$b['type']}] is not permitted."); return;
            }
            if (!isset($b['data']) || !is_array($b['data'])) {
                $fail('Malformed block data.'); return;
            }
        }
    }
}

Rendering: switch on type, never {!! !!} the payload. Editor.js stores text, not HTML; the HTML is yours to produce. Map type → renderer class so adding a tool is one file, not a Blade edit:

interface BlockRenderer { public function render(array $data): string; }

// then, in a Blade partial:
@foreach ($post->body->blocks() as $block)
    @if ($renderer = app(\App\Blocks\Registry::class)->for($block['type']))
        {!! $renderer->render($block['data']) !!}  {{-- renderer escapes internally --}}
    @else
        <div class="block-unsupported" data-type="{{ $block['type'] }}"></div>
    @endif
@endforeach

Inside each renderer, escape explicitly: e($data['text']) for paragraph/header, e() per items[] for list, and nl2br(e(...)) only where you actually want line breaks. The bundle's Twig renderer follows the same discipline — the payload is untrusted, the output is escaped.

Read-only show pages: mount the same component with readonly: true and skip the hidden input/onChange, or skip Editor.js entirely and use the server-side renderer. Rendering is the cheaper path for public pages — don't ship the editor JS to visitors.

Livewire / Inertia variants. Livewire: put the editor in wire:ignore, call editor.destroy() in the component's teardown, and re-init on remount — otherwise you leak instances across polls. Inertia: keep the component as-is, POST the hidden input value as a JSON string or JSON.parse it into the payload; a Blocks cast handles either.

Assets via Vite, not CDN. @vite(['resources/js/editorjs.js']) with the editor bundled. The bundle vendors/points at its assets through Symfony's asset pipeline; the Laravel analogue is Vite, and it avoids a runtime third-party fetch.

Gotchas and Tips

Don't try to make the bundle work. No auto-discovery, no config/bundles.php in Laravel, and the Symfony 8 constraint is unreachable from Laravel's pinned Symfony 7. Any "adapter" you write is bespoke and unmaintained — worse than the component you'd write instead. Read the source; copy the contract.

editor.save() is async and can be called too early. It returns a Promise; calling it before isReady resolves yields an empty or partial document. Gate saves on onChange, and add a final await editor.save() in the form's submit handler as a belt-and-braces before posting.

Multiple editors on one page need unique holders and inputs. The bundle's form type gets uniqueness from the Symfony form field name; your Blade component must derive ids from $name (as above), not a constant. Two editors sharing document.getElementById('editorjs') will silently mount one into the wrong div.

JSON round-trip flags must be consistent. JSON_UNESCAPED_UNICODE on write, plain json_decode on read. Mismatched flags produce payloads that differ byte-for-byte for identical content — which breaks content hashing, ETags, and dirty-checking. Also be aware PHP re-encodes objects as arrays if you decode with true; preserve blocks[].data as-is rather than reshaping it.

Pin the Editor.js JS version and upgrade deliberately. The block-tool API has broken across majors, and the bundle's Symfony 8 support says nothing about which Editor.js JS version it pairs with — those tracks are independent and the changelog is silent on it. Lock the npm version in package.json and treat bumps as reviewed changes.

Unknown block types must render as a placeholder, not disappear. If you remove a tool from config/editorjs.php, stored documents still contain that block. Filtering on read silently destroys content the next time the post is saved through the editor (Editor.js round-trips only the blocks it loaded). Render a safe placeholder and log it.

Enforce payload limits at two layers. max: on the request (ValidatePostSize / post_max_size) and a block-count cap inside the rule. Long documents with many blocks produce multi-hundred-KB JSON bodies; a hostile client can post 20MB of nested blocks.

CSP and inline scripts. The mount script above is inline; if you have a strict script-src, move it into the Vite bundle and pass data-* attributes on the holder div. Symptom is a rendered-but-empty editor with console CSP violations — easy to miss because the hidden input still posts its old value.

Debugging tip: when a save "doesn't stick," log the raw JSON string (Log::debug($request->input('body'))), not a decoded array — the array looks fine while the string is "" because onChange never fired. And remember old('body') returns the JSON string; don't feed it to the cast.

Extension points you actually have: the tools config (block whitelist), the renderer registry (type → HTML), and the Blocks value object (serialization). Those three are your contract. Document the block schema — it survives tool swaps and is the only artifact that matters if Editor.js is ever replaced.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console