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

tbmatuka/editorjs-bundle

Laravel bundle for the Editor.js block-style editor. Store, parse and render Editor.js JSON content, converting blocks into clean HTML for display. Simplifies integrating Editor.js with Laravel models and Blade views.

View on GitHub
Deep Wiki
Context7

Getting Started

tbmatuka/editorjs-bundle is a Symfony bundle that wraps Editor.js v2. It does three things:

  1. Registers a Symfony form type (EditorjsType) that renders the Editor.js container and its assets.
  2. Maps the form field to an Editorjs value object wrapping the {time, blocks, version} JSON.
  3. Converts that JSON to HTML server-side through pluggable block parsers (paragraph, header, list, image, etc.).

It does not give you a Laravel package. There is no service provider, no Blade directive, no Eloquent cast. If you are on Laravel, treat the repo as a reference implementation for the server-side half (JSON model + block parsers + upload contract) and write the client half yourself — the JS is stock Editor.js either way.

First steps if you are actually in a Symfony app:

composer require tbmatuka/editorjs-bundle
# config/bundles.php
EditorjsBundle\EditorjsBundle::class => ['all' => true],

Copy/install the bundle's assets (bin/console assets:install), then include Editor.js and the bundle's form theme in your layout. Look first at:

  • src/Editorjs/Editorjs.php — the value object (getBlocks(), getHtml(), toJson()).
  • src/Editorjs/EditorjsHtmlConverter.php — the JSON→HTML entry point.
  • src/Editorjs/BlockParser/ — one parser per block type; this is where you extend.
  • Resources/config/ — the tool registration YAML you will be editing constantly.

First use case: swap a <textarea>/CKEditor field in an admin form for EditorjsType, persist the raw JSON, and render converted HTML on the front end.

// Entity
#[ORM\Column(type: 'json')]
private array $body = [];

// Form
$builder->add('body', EditorjsType::class, [
    'tools' => ['header', 'list', 'image', 'delimiter'],
]);

Laravel equivalence, roughly: a text/json column, a Blade component mounting Editor.js, a POST /editorjs/upload endpoint, and a EditorjsToHtml converter class you own.

Implementation Patterns

Store JSON, render HTML. Never store the converted HTML as the source of truth — you cannot round-trip it back into blocks for editing. Keep the raw JSON (Doctrine json column, MySQL JSON, or longText if you are on older MySQL), and convert on output:

$editorjs = new Editorjs($post->body);           // array from the json column
$html     = $converter->convert($editorjs->getBlocks());

If read performance matters, cache the HTML in a second column and invalidate on save. That is a two-line prePersist/observer hook.

Data transformers / casts. The bundle ships a transformer so the form binds an Editorjs object to a JSON column. In Laravel the equivalent is a custom cast:

protected $casts = ['body' => EditorjsCast::class];
// get: new Editorjs(json_decode($value, true));
// set: json_encode($value->toArray(), JSON_UNESCAPED_UNICODE);

Custom block parsers. Every block type you enable in the editor needs a matching server-side parser, or the block silently disappears (or throws, if the converter is in strict mode). In Symfony you implement EditorjsBlockParserInterface and tag the service:

class WarningBlockParser implements EditorjsBlockParserInterface
{
    public function getSupportedBlockType(): string { return 'warning'; }

    public function parse(array $blockData): string
    {
        $title = htmlspecialchars($blockData['title'] ?? '', ENT_QUOTES);
        $msg   = htmlspecialchars($blockData['message'] ?? '', ENT_QUOTES);
        return "<div class=\"warning\"><strong>{$title}</strong><p>{$msg}</p></div>";
    }
}
App\Editorjs\WarningBlockParser:
    tags: ['editorjs.block_parser']

In Laravel this is just a match ($block['type']) in a converter class or a BlockParser registry keyed by type. Do not skip the whitelist — unknown block types are attacker-controlled keys.

Tool configuration is split across two places. The YAML config defines which tools the client gets and their JS options; the parser registry defines how they render. Adding table, embed, or code means: add the JS bundle to your layout, add the tool entry to the bundle config with its class and options, and add a parser. Miss one of the three and you get either a JS console error or a dropped block.

Upload endpoints. Image/file tools POST to a URL you configure; the expected response shape is Editor.js's standard:

{ "success": 1, "file": { "url": "https://cdn.example.com/abc.jpg" } }

The Symfony bundle leaves the controller to you. In Laravel:

Route::post('/editorjs/upload', [EditorjsUploadController::class, 'store'])
    ->middleware(['auth', 'can:write-posts']);

public function store(Request $request)
{
    $request->validate(['image' => ['required', 'image', 'max:4096', 'mimes:jpg,png,webp']]);
    $path = $request->file('image')->store('editorjs', 's3');
    return response()->json([
        'success' => 1,
        'file' => ['url' => Storage::disk('s3')->url($path)],
    ]);
}

Pass csrfToken (or X-CSRF-TOKEN) plus endpoints.byFile in the tool config; both Symfony and Laravel will 419/403 the XHR otherwise.

Rendering. In Twig the bundle wires a form theme and you can output {{ post.body.html|raw }} via the value object. In Blade, <div class="prose">{!! $post->body_html !!}</div>.

Validation beyond the form type. The bundle validates shape loosely. Add your own rules: max block count, max total JSON size, allowed type values, and the per-block data constraints. In Laravel a FormRequest with a nested blocks.*.type in: rule covers most of it.

Gotchas and Tips

  • It is stale. Last release March 2021, ~19 stars, zero dependents. It pins an older Editor.js 2.x and older tool versions. Expect to pin versions on both sides and to fork for anything nontrivial. The JS tool APIs have since standardized on async save()/render(); mixing a new tool with the bundle's bundled JS will break.
  • Symfony-only, hard. Pulling it into Laravel drags in symfony/form, symfony/twig-bridge, and the asset component. Do not attempt it — copy the Editorjs class and the block parsers (MIT licensed, small) and drop the Form/Twig plumbing.
  • XSS is entirely your job. Every field inside blocks[].data is user input: text (contains inline HTML from the inline toolbar), caption, items[], url, src, code. Store raw, escape/sanitize on convert, and whitelist URL schemes (http, https, mailto) — a javascript: in a link or image src is the classic Editor.js injection.
  • Unknown blocks vanish silently. If a user pastes content produced by a tool you did not register a parser for, the block is dropped from the HTML with no error. Log unknown types in your converter's default branch so you notice during development.
  • Empty/whitespace documents. Editor.js returns blocks: [] for an untouched editor. Guard against rendering <p></p> wrappers and against NOT NULL violations.
  • JSON column and encoding. Use JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES when encoding; otherwise you get \uXXXX soup in the DB that is painful to debug. Doctrine's json type hands back an array, not the Editorjs object — wrap it at the boundary.
  • Escaping the tool config. Tool options are serialized into a data-* attribute or an inline script. Use json_encode with JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT (or html_attr escaping in Twig). A </script> inside a tool config string will break the page.
  • Upload debugging. A 419/403 on image upload is almost always CSRF or session expiry, not the upload route. A success: 1 with no file.url renders a broken image with no client-side error. Log the raw request and response for the first integration.
  • Debugging the converter. Dump the raw JSON first (dd($post->body) / $editorjs->getBlocks()), then feed it into the converter in isolation. Most "missing content" reports are a parser that returns an empty string for a block type it does accept.
  • Extension points worth knowing: the block parser interface (add your own), the form theme override (change the wrapper markup), and the tool config YAML (only place to add client tools). There is no event system and no hook for post-conversion sanitization — wrap the converter in your own service if you need that.
  • Asset strategy. The bundle's install step assumes Symfony's asset component. In Laravel, skip it: npm i @editorjs/editorjs plus the individual tool packages, bundle with Vite, and mount the editor from a small Alpine component. That is less work than adapting the bundle's asset wiring.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource