bayareawebpro/nova-field-ckeditor
CKEditor field for Laravel Nova that adds a full-featured rich text editor to resource forms. Supports image and file uploads, configurable toolbar and editor options, and seamless model storage, giving Nova users a familiar, powerful content editing experience.
Requirements first. This field targets Laravel Nova 3 (Vue 2). It was last released 2022-02-09 and the repository is archived, so treat compatibility as a hard gate before you invest any time:
Install and publish:
composer require bayareawebpro/nova-field-ckeditor
php artisan vendor:publish --provider="Bayareawebpro\NovaFieldCkEditor\FieldServiceProvider"
Publishing should give you a package config file (CKEditor build/toolbar options, upload disk and path, and the media/link/snippet browser settings) plus compiled assets under public/vendor/. Run php artisan storage:link if you plan to upload media to the public disk.
Where to look first, in this order:
README.md — the only documentation; screenshot-driven, so read the code for exact method namessrc/CkEditor.php — the field class; every macro/method you can chain on CkEditor::make() lives here or in a traitconfig/ in the package — the real source of truth for browser and upload behaviorresources/js/field.js and the Vue components — where the media/link/snippet browser modals and CKEditor instance are wireddist/ — prebuilt assets; you will not rebuild these unless you forkYour first use case — a long-form article body with inline images:
use Bayareawebpro\NovaFieldCkEditor\CkEditor;
public function fields(Request $request)
{
return [
CkEditor::make('Content')
->rules('required', 'string')
->hideFromIndex()
->help('Rich text; images are uploaded to the public disk.'),
];
}
Store it in a longText column, and render it on the front end with {!! $post->content !!} only after sanitizing (see Gotchas). Add ->hideFromIndex() immediately — dumping raw HTML into the index table is a bad first impression.
Field-level configuration stays in the resource. Keep toolbars/options close to the field so reviewers can see what editors can do:
CkEditor::make('Body')
->rules('required', 'string', 'max:65535')
->options([
'toolbar' => ['heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'imageUpload'],
])
->hideFromIndex()
->onlyOnForms();
Verify the exact chainable names (options, mediaBrowser, linkBrowser, snippetBrowser, withFiles) against src/CkEditor.php. Treat the resource as the contract; if a method is missing, add it via a small custom subclass instead of scattering config through AppServiceProvider.
Centralize shared editor config. If five resources use the same toolbar and upload disk, wrap it:
class RichText extends CkEditor
{
public function __construct(string $name, ?string $attribute = null, ?string $resolveCallback = null)
{
parent::__construct($name, $attribute, $resolveCallback);
$this->options(config('ckeditor.toolbar'))
->hideFromIndex()
->rules('nullable', 'string');
}
}
Then RichText::make('Body') everywhere. This is the single biggest maintainability win with an unmaintained package — you get one place to patch when you eventually migrate off it.
Uploads: bind the browser to a real disk and path. Configure the upload disk/path in the published config, and keep it consistent with what the front end can serve. A pragmatic layout:
storage/app/public/articles/{id}/...
If uploads are tied to a model, do the disk/path resolution in an observer or a dedicated MediaStorage service rather than in the field, and delete orphaned files in the model's deleting event. Otherwise you accumulate unreferenced files forever, because CKEditor only knows what's in the HTML.
Media/library browser. The browser is a modal that lists assets — in practice you'll point it at a curated directory, an uploads table, or a Spatie Media Library collection. Whatever you pick, make the browser's source and the field's upload target the same backing store. A common failure mode is "uploads go to disk, browser reads a DB table," and then new images never appear in the picker until a sync job runs.
Snippets. Two viable strategies:
[[cta-primary]]) and resolve it at render time via a Blade partial map. More work, but the snippet definitions stay version-controllable in code, and output stays consistent.Choose placeholders if the snippet has brand/legal significance; choose HTML if it's purely decorative.
Rendering and sanitization. The field only produces HTML; the pipeline after it is yours:
use HTMLPurifier;
public function getRenderedContentAttribute(): string
{
return app(HTMLPurifier::class)->purify($this->content ?? '');
}
Cache the sanitized output (a rendered_content column or a cache key) — purifying large documents on every request is noticeable. On the index view, never show raw HTML:
Text::make('Body', fn ($post) => Str::limit(strip_tags($post->content), 80))
->onlyOnIndex(),
Integration tips.
$table->longText('content')->nullable(); — CKEditor documents get big fast; MySQL TEXT (64KB) will truncateck-content) don't ship with your app CSS — either import CKEditor's content styles or write your own typographic CSS for the rendered HTMLArchived status is the headline gotcha. Last release February 2022, 15 stars, no maintainer. There will be no security patches, no Nova 4 support, and no fixes for CKEditor 5 breaking changes. Concretely:
composer.json ("bayareawebpro/nova-field-ckeditor": "1.x.x"), never ^repositories entry — you will need to touch it eventuallyNova/Vue version mismatch. The field is a Vue 2 component compiled against Nova 3's asset pipeline. Installing it in a Nova 4 app typically produces a blank field or a console error like a failed component resolution — it won't fail at composer install, so test the actual form page before merging.
Asset publishing is a two-step trap. vendor:publish copies prebuilt assets to public/vendor/.... If you edit anything in resources/js, you must rebuild (npm run dev/prod inside the package) and re-publish — and the build toolchain (Laravel Mix, Node version) is from 2022. If assets look stale: php artisan nova:publish, php artisan view:clear, hard-refresh with cache disabled.
CKEditor 5 licensing. CKEditor 5 is GPL2+ or commercial. If your product is closed-source, you need a commercial license key/or you're relying on the GPL build. This is a legal question, not a technical one — flag it before shipping, it's easy to miss when the field "just works."
Silent config failures. If the media/link/snippet browser opens empty rather than erroring, the usual cause is a config key pointing at a nonexistent disk, directory, or route. Check storage/logs/laravel.log and the network tab for the browser's XHR; the modals often swallow 404/403s and just render nothing.
Upload 403s / broken images. In order: is storage:link run, is the disk public (not local), is the file visibility set to public, and — on S3 — is CORS configured for your Nova domain? For signed URLs, CKEditor will happily cache an expired URL in the document HTML; store the storage path, not the URL, and resolve at render time.
XSS is real. CKEditor sanitizes some input, but stored HTML from a compromised editor account (or a hand-crafted API request bypassing Nova) is executable. Always sanitize server-side on save or render. Never {!! !!} raw $request->input() from the same attribute.
Toolbar/option errors surface only in the browser console. An invalid toolbar item name or an option belonging to a CKEditor plugin not in the build produces a JS exception and a dead editor. Open devtools first; the field gives you no PHP-side validation.
displayUsing and other Nova field decorators do nothing here — the field renders a Vue component, not a formatted string. Use ->hideFromIndex() plus a separate Text field for summaries.
Extension points worth knowing. The field class is a normal Nova Field, so subclassing (as shown above) is the cleanest extension point. The Vue component is the place for toolbar/browser behavior changes, but that means maintaining a fork's build. Prefer config + subclassing; avoid patching dist/ by hand, since it's overwritten on the next publish.
Debugging checklist when the field renders blank: confirm the package's service provider is auto-discovered (php artisan package:discover), confirm assets exist in public/vendor/, confirm the resource's field isn't ->onlyOnDetail(), check the browser console for the CKEditor build error, then check storage/logs for upload errors. Nine times out of ten it's unpublished assets or a Nova version mismatch.
How can I help you explore Laravel packages today?