unisharp/laravel-ckeditor
Integrates CKEditor into Laravel with simple configuration, Blade helpers, asset publishing, and file upload support. Add a full-featured WYSIWYG editor to your forms quickly, with customizable toolbar and upload handling out of the box.
This is a thin Laravel wrapper that vendors the CKEditor 4 JavaScript distribution and adds Blade/config conveniences on top. The 4.7.2 release is purely an upstream asset merge — the package's PHP surface is unchanged — so existing installs keep working; you just get a refreshed ckeditor.js/plugin bundle.
composer require <vendor>/<package> — then check composer show <vendor>/<package> to see whether the release tag maps to a Laravel-version constraint you satisfy.php artisan vendor:publish --provider="Vendor\Package\ServiceProvider"
You should end up with config/ckeditor.php (toolbar, height, extra plugins, file-browser URLs) and a copy of the editor under public/vendor/ckeditor/.config/ckeditor.php and the package's Blade view/helper — that's where 90% of day-to-day customisation happens. Then read resources/views/vendor/... to see exactly what markup is emitted (it's usually a <textarea> + an inline CKEDITOR.replace() call).<textarea name="body" id="body">{{ old('body', $post->body ?? '') }}</textarea>
@ckeditor('body') {{-- or the package's helper/directive --}}
Then in the controller, validate and persist the HTML, and sanitise it on output (see Gotchas — this matters far more than usual here).Profile-based configuration. Don't inline toolbar arrays in every view. Define named profiles in config/ckeditor.php (simple, full, comment) and select per form. Keeps the WYSIWYG consistent and lets you tighten the toolbar where the content is user-generated.
Single reusable Blade component. Wrap the textarea + init in <x-rich-text name="body" profile="full" /> so you control id generation, old() repopulation, and error-class styling in one place. Multiple instances on one page are common (e.g. short + long description) — make the component derive a unique DOM id from name.
File uploads / browse server. The package config typically exposes filebrowserUploadUrl / filebrowserBrowseUrl. Point these at your own routes:
Route::middleware(['auth', 'can:manage-media'])->group(function () {
Route::post('ckeditor/upload', [EditorUploadController::class, 'store']);
Route::get('ckeditor/browse', [EditorBrowseController::class, 'index']);
});
In the upload controller, validate with a Form Request (image, mimes:jpg,png,webp, max:4096), store on a dedicated disk (Storage::disk('public')->putFile('editor', $file)), and return CKEditor's expected callback shape — either a JSON {uploaded: 1, url: ...} payload or the legacy window.parent.CKEDITOR.tools.callFunction(...) HTML. Never echo the user-supplied filename back into that script.
CSRF. CKEditor's XHR uploads don't automatically carry your token. Either add the meta tag and set it in config:
config.extraPlugins = '...';
config.filebrowserUploadMethod = 'xhr';
config.headers = { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content };
…or exclude only the upload route from CSRF (weaker — prefer the header).
Content storage and rendering. Store HTML in a text/longText column. Sanitise on render as well as on save, using mews/purifier or league/html-sanitizer, then output with {!! $sanitised !!}. Consider storing a plain-text body_excerpt derived via strip_tags() for search and card previews — saves you querying inside markup later.
Livewire / Inertia. The editor mutates the DOM behind Livewire's back. For Livewire, wire CKEDITOR.instances[id].on('change', ...) to @this.set('body', ...), and re-init in wire:ignore + Livewire.hook('morph.updated'). For Inertia, initialise in onMounted and destroy in onUnmounted to avoid leaking instances across visits.
Testing. Feature tests just assert the HTML round-trips to the DB. Cover the upload endpoint with real HTTP tests (auth, mime rejection, size limits) — that's where the security surface is. Use Dusk only for smoke-testing that the toolbar renders; don't try to drive the editor's iframe in Pest/PHPUnit.
4.7.2 is a 2018 build — treat the bundled editor as a security liability. CKEditor 4 reached end of life in mid-2023, and 4.7.x predates a long list of XSS/HTML-injection CVEs fixed in later 4.x releases (dialogs, protected source handling, the htmlwriter and various plugin parsers). This release tells you the package is tracking a stale upstream tag, not that it's current. Concretely:
auth and a capability check; don't expose it on public comment forms without sanitisation.Re-publish assets after every update. vendor:publish won't overwrite existing files unless you pass --force. If the toolbar doesn't change, or you still see old behaviour after upgrading, you're almost certainly running a stale public/vendor/ckeditor/. Verify with a hash or the version in the editor's about dialog.
Browser caching. ckeditor.js is served from public/, not through Vite's manifest, so it gets no cache-busting hash. Add a ?v= query string from your config version, or set long-lived immutable headers and a versioned path, otherwise users see mixed old/new plugin code.
Publishing into public/ vs. public/vendor/. If the package publishes somewhere your deploy pipeline doesn't sync (some pipelines only sync public/build), the editor 404s in production but works locally. Add the vendor path to your deploy script or cp -r step.
Advanced Content Filter will silently eat your markup. Styles, classes, and data-* attributes get stripped unless allowed. If editors complain that their <div class="callout"> disappears, fix it in config.allowedContent / extraAllowedContent rather than disabling ACF wholesale — allowedContent = true is the classic route to a stored-XSS incident.
Inline config vs. package config. Values set in the Blade init call override config/ckeditor.php, and CKEditor's own config.js can override both. When a setting "doesn't apply," log the resolved CKEDITOR.instances[name].config in the console — it's almost always precedence, not a bug.
CSP. If you ship a strict Content-Security-Policy, the editor's inline init script and the upload callback's inline <script> will be blocked. Either allow 'unsafe-inline' for the editor routes (narrowly) or move init into a bundled JS file and use the JSON upload response rather than the script-tag callback.
Path handling in the browser/upload endpoints. Never accept a client-supplied path or filename. Generate storage names (Str::random(40) . '.' . $ext) and return the URL from Storage::url(). Also decide public-vs-private disk deliberately: a public disk means every uploaded image is world-readable by URL guess; a private disk means you need signed/temporary URLs in the browse endpoint.
Extension points to prefer. The config file and the published Blade view are the intended seams. Avoid forking the package's JS — subclass/publish instead, so the next upstream merge (like this one) is a no-op for you.
Reading the changelog. "Merge from official CKEDITOR 4.7.2 tag" means the PHP side is untouched: no new config keys, no signature changes, no deprecations. A composer update is safe; the only real action item is re-publishing assets and re-checking your sanitisation posture against a 2018-era editor.
How can I help you explore Laravel packages today?