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

Laravel Jodit Laravel Package

nasirkhan/laravel-jodit

Laravel Jodit integrates the Jodit WYSIWYG editor through one reusable Blade component, working in plain Blade templates, view components, and Livewire. Includes a bundled file browser and uploader, CDN assets, and a publishable config.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish config

composer require nasirkhan/laravel-jodit
php artisan vendor:publish --tag=jodit-config

Service provider is auto-discovered. intervention/image ^4.0 comes along as a hard dependency (it powers the resize/crop buttons), so expect that in your composer.lock even if you never touch cropping.

2. Fix your layout — this is the step people skip

The component pushes its CDN <script>/<link> tags into Laravel asset stacks. If your layout doesn't render them, you get a plain <textarea> and zero JS errors to explain it:

{{-- <head> --}}
@stack('after-styles')

{{-- before </body> --}}
@stack('after-scripts')

If you're on a layout you don't control (vendor layout, Inertia shell, some admin themes), either add the stacks or change the names in config/jodit.php under assets.styles_stack / assets.scripts_stack to match stacks that already exist.

3. First use case — the boring one

Start with a plain Blade form, not Livewire. It exercises everything: CDN load, toolbar, textarea name, and the file browser connector.

<form method="POST" action="{{ route('posts.store') }}">
    @csrf
    <x-jodit::editor
        name="content"
        :value="old('content', $post->content ?? '')"
        :required="true"
        placeholder="Write your post here…"
    />
    <button type="submit">Save</button>
</form>

Submit it. If the editor rendered and content shows up in $request->input('content'), the integration is sound. Then wire up the file browser (step 4), because that's where the config actually matters.

4. Storage link, or images will 404

The connector writes to disk => 'public' under base_path => 'jodit'. If public/storage doesn't exist, uploads succeed and the editor shows broken images:

php artisan storage:link

5. Where to look first when something breaks

  • config/jodit.php — CDN URLs, stacks, route name/middleware, disk, mimes, defaults, toolbar buttons.
  • The compiled JoditConnectorController route: php artisan route:list | grep jodit. By default it's jodit/jodit-connector behind ['web', 'auth'].
  • Browser devtools Network tab — the connector responds in Jodit's filebrowser JSON protocol, so a 302 to /login or a 419 is immediately visible there.

A realistic assessment: 2 GitHub stars and 0 dependents means you are one of the first handful of users. Read the connector controller source before you ship it on anything authenticated-but-not-admin. The upside is that the component surface is tiny, so "read the source" is a 10-minute job, not a day.

Implementation Patterns

Livewire two-way binding (the main reason to pick this package)

<div>
    <x-jodit::editor name="content" :value="$content" wire-model="content" />
</div>

The component applies wire:ignore to its own wrapper automatically and flushes the editor value back through the Jodit JS API on a debounce. You do not need to add wire:ignore yourself — and you shouldn't, since duplicating it can prevent the sync.

The debounce is configurable per instance:

<x-jodit::editor name="body" :value="$body" wire-model="body" :debounce="800" />

Longer debounce = fewer Livewire roundtrips on a heavy component; shorter = fresher validation feedback. 300 ms default is fine for most forms. Note the flush is debounce-driven, so validate on submit rather than reacting to every keystroke.

Form objects / validation

The editor posts an HTML string. Treat it like any other text input, but be deliberate about validation:

$data = $request->validate([
    'content' => ['required', 'string', 'min:10'],
]);

required on the component adds the HTML attribute to the textarea, which is not a guarantee — empty Jodit content is often <p><br></p>, which passes required on the client. Enforce it server-side, and strip empty-paragraph noise if your rules are strict about non-empty content.

Multiple editors on one page

The default id is jodit_{name}. Two editors with the same name, or re-rendering a Livewire component that swaps name dynamically, will collide. Give each a stable explicit id:

<x-jodit::editor name="summary" id="editor_summary" :value="$summary" wire-model="summary" :height="200" />
<x-jodit::editor name="body"    id="editor_body"    :value="$body"    wire-model="body"    :height="500" />

Toolbar profiles: prefer presets, fall back to explicit arrays

{{-- Rich preset --}}
<x-jodit::editor name="content" profile="full" />

For deliberate control, pass a PHP array (recommended over the string forms):

<x-jodit::editor
    name="comment"
    :buttons="['bold', 'italic', '|', 'align', '|', 'ul', 'ol', '|', 'link', 'undo', 'redo']"
    :height="240"
/>

Two useful conventions:

  • align collapses left/center/right/justify into one dropdown — use it whenever toolbar width matters.
  • \n starts a new toolbar row, | is a separator. On narrow admin layouts, splitting into two rows beats horizontal scrolling.

For consistency across an app, define the toolbar once in config/jodit.php instead of repeating the array in every view. Reserve per-view buttons for genuinely different editors (comment box vs. article body).

Restricting the editor per field

Small fields shouldn't expose the file browser, source view, or font pickers:

<x-jodit::editor
    name="excerpt"
    :value="old('excerpt')"
    :file-browser="false"
    :height="180"
    :buttons="['bold', 'italic', 'link', '|', 'undo', 'redo']"
/>

file-browser="false" removes the browser and upload entry points entirely — cheaper than relying on authorization in the connector.

Custom connector route (do this for admin-only uploads)

The default middleware is ['web', 'auth'], which is broad. If uploads should be admin-only, disable the package route and register it yourself:

// config/jodit.php
'route' => ['enabled' => false, 'name' => 'backend.jodit.connector'],
// routes/web.php
Route::middleware(['web', 'auth', 'role:admin'])
    ->prefix('admin')
    ->group(function () {
        Route::any('jodit-connector', [JoditConnectorController::class, 'handle'])
            ->name('backend.jodit.connector');
    });

Once route.name matches your registered route name, the component picks it up without any prop. Only use :connector-url="route(...)" when you need a per-instance override (e.g. multitenant prefixes).

Keeping editor content aligned with the model on edit screens

value is initial content only — Jodit doesn't watch it. If a Livewire action loads a different record, the editor keeps showing the old HTML unless the element is re-keyed:

<x-jodit::editor
    :key="'editor-' . $postId"
    name="content"
    :value="$content"
    wire-model="content"
/>

The :key forces Livewire to destroy and recreate the component (and thus re-init Jodit) on record change. Without it you get classic "edited post A, saved into post B" bugs.

Sanitizing on output

The package stores whatever Jodit produces. Store raw, sanitize on render with an allowlist (HTMLPurifier or a Str::of()->sanitize()-style helper in your stack), because allowed_mimes includes svg, and Jodit's paste defaults (insert_clear_html) are a UX setting, not a security boundary.

Gotchas and Tips

Blank textarea, no JS errors. The asset stacks. Nine times out of ten it's @stack('after-styles') / @stack('after-scripts') missing from the layout, or a layout whose stack names differ from config/jodit.php. Check the rendered HTML for jodit.min.js before debugging anything else.

CDN means your editor is a third-party dependency. Default URLs point at unpkg.com/[email protected]. That's a hard external dependency at page load: air-gapped deployments, strict CSP (script-src), and CDN outages all break the editor. For production, vendor the assets into public/ (or your Vite build) and repoint cdn_css/cdn_js. Pinning the version in config is good; pinning it and self-hosting is better.

Connector 302s to /login in the file browser. The default ['web', 'auth'] middleware plus an unauthenticated context (or a Livewire route outside the auth guard) yields a redirect that Jodit renders as "no files". Reproduce the connector URL in a new tab — you'll see the redirect immediately.

Uploads land in storage but images 404. Run php artisan storage:link. Also verify APP_URL — the connector returns absolute URLs derived from it, and a wrong APP_URL produces correct files with broken URLs. This bites hardest behind a proxy or on Docker where APP_URL still says http://localhost.

Disk assumptions. disk => 'public' + base_path => 'jodit' is shared by all editors and all users. Every uploaded image from every author ends up in one flat directory tree. If you need per-user or per-model scoping, overriding config per request won't cut it — you'll need a custom connector (set file_manager.backend => 'custom' and connector-url) that namespaces paths.

allowed_mimes allows svg and archives. SVG can carry script. If the connector's directory is reachable from the same origin, that's stored XSS. Narrow allowed_mimes for user-facing editors, and consider serving the jodit path from a separate subdomain or with Content-Disposition: attachment for non-image types.

wire-model is debounced — don't assert immediately. In browser tests, typing then instantly checking $wire.content is flaky. Either wait past the debounce (300 ms default) or use ->set('content', ...) in Livewire tests, which bypasses the editor entirely. Unit tests won't exercise the Jodit sync at all; that path needs Dusk/Playwright.

Livewire wire:ignore is automatic but shallow. The component handles its own wrapper. It can't protect you from a parent re-render that changes the editor's name, id, or buttons — those are baked in at init. Combine :key with stable name/id and don't compute them from volatile state.

Buttons prop parsing is lenient, which hides typos. Arrays, JSON strings, and PHP-style array strings are all accepted (:buttons="[...]", buttons="[...]", buttons='["..."]'). A malformed string may silently fall back to the config default rather than erroring, so you'll think your toolbar is being ignored. Use the :buttons PHP-array form and confirm the rendered HTML/JS output once.

Route caching and route.enabled. After flipping route.enabled => false and registering your own route, run php artisan route:clear (or optimize:clear). Cached routes make it look like the package route is still winning.

profile="full" isn't in the config reference. The README documents it under usage but the published config shows only buttons. If you rely on profiles, verify in the source what full resolves to, and don't assume other profile names exist.

Extension points are limited, and that's the design. You get: config overrides, per-instance props, custom connector URL/route/backend, and connector-url for per-form overrides. There is no documented view-publishing or event system. Anything beyond that — a custom upload endpoint, path scoping, thumbnails — means writing your own connector and setting file_manager.backend => 'custom'. Plan for that if your requirements are more than "one shared upload folder".

Version pinning. Releases jump quickly (v1.7 → v2.0 with a dependency note change) and the package is very young. Pin an exact version in composer.json, and re-read the connector controller after upgrades rather than trusting semver on a 2.x from a two-star package. If the upload path matters to you, consider that controller the one piece of code you actually own — fork it or wrap it early.

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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table