numaxlab/nova-ckeditor5-classic
Integrate the CKEditor 5 Classic rich-text editor into Laravel Nova as a ready-to-use field. Provides a familiar WYSIWYG editing experience for content fields in Nova resources and forms.
Check the version constraint before anything else. Last release was 2021-01-12, so this is a Nova 3-era field. Run composer show numaxlab/nova-ckeditor5-classic --all and read its laravel/nova requirement. On Nova 4/5 you will almost certainly hit a resolution conflict — treat adoption as a small maintenance project (relax the constraint, then verify the field's Nova::script/Nova::style registration still works) rather than a drop-in.
composer require numaxlab/nova-ckeditor5-classicphp artisan vendor:publish (select the package's provider/tag) and, if needed, php artisan nova:publish. Confirm public/vendor/nova-ckeditor5-classic/ exists and the JS/CSS appear in the browser network tab when a form loads.src/ and read the Field class — it will be something like Numaxlab\NovaCKEditor5Classic\CKEditor5Classic. That file, plus the built bundle in dist/ (or resources/js/field.js), is where the toolbar, plugin list, and upload config live. Those three things are what you'll actually need to change.longText — CKEditor emits full HTML with attributes; a MySQL TEXT column (64 KB) is exceeded by a medium-length article with pasted formatting.First real use case — long-form article body:
use Numaxlab\NovaCKEditor5Classic\CKEditor5Classic;
CKEditor5Classic::make('Body')
->rules('required', 'max:65535')
->hideFromIndex();
Verify a round-trip: create → save → reopen → confirm the toolbar state and content survive without the editor double-instantiating.
Wrap the field in your own class. Because the package is archived, don't sprinkle CKEditor5Classic::make() across resources — subclass it once in app/Nova/Fields/BodyEditor.php and have every resource call your class. When you eventually fork or swap to a maintained CKEditor field, you change one file.
Configure via metadata, not by editing vendor files. Pass toolbar/height/plugin settings through ->withMeta([...]) and have field.js read this.field props at editor-init time. Anything you hard-code in dist/js/field.js is lost the next time someone runs vendor:publish --force.
Sanitize server-side. The editor constrains what authors can type, not what's in the request. Pair the field with a purifier (mews/purifier) as an Eloquent mutator or an observer on save, with an explicit allowlist (p, a[href], ul, ol, strong, em, h2, h3, blockquote, figure, img[src]). This also protects you if the bundled CKEditor build has a stale paste-from-Word or HTML-embed path.
Display deliberately. On detail views, render raw HTML only after sanitization (->asHtml() or {!! $model->body !!}); on index views, truncate hard:
->displayUsing(fn ($value) => Str::limit(strip_tags($value ?? ''), 80))
Custom build when the default toolbar isn't enough. The bundled bundle is a frozen CKEditor 5 Classic build. The durable pattern is: publish the package JS, add @ckeditor/ckeditor5-build-classic and the plugins you need as dev dependencies in your app, edit resources/js/field.js to import your configured build, and npm run prod. Commit the compiled asset. Never rely on the package's dist/ after that.
Locale fan-out. For multi-language content, one field per locale (Body (EN), Body (FR)) bound to body->en via ->resolveUsing(). Don't try to run one editor instance across locales.
Image handling. Out of the box this is text-only unless an upload adapter is wired up. If you need images, the usual integration is an adapter that POSTs to a Nova controller and returns a URL, or defer to a dedicated media field (Spatie Media Library) plus a CKEditor plugin pointing at those URLs. Falling back to base64-embedded images "works" but silently multiplies row size and fights your longText/max: rules.
--force and hard-refresh; check that mix-manifest/asset paths in the package don't collide with your own nova:publish output.->rules('max:65535') protects the column, but PHP's max rule counts characters while MySQL's TEXT limit counts bytes — UTF-8 content with emoji or CJK text can pass validation and still truncate. Prefer longText (4 GB) and stop worrying.<p> </p> in the payload, which defeats required. Add a prepareForValidation-style normalizer (or a Nova fillUsing that strips empty paragraph tags) if required needs to actually mean something.field.js explicitly watches the value prop and calls editor.setData() with a guard against feedback loops. This is the most common thing you'll have to patch.id attributes or a shared editorInstance singleton variable will break them. Check field.js for a module-level editor reference if only the first editor initializes.public/vendor/; if you deploy with Vapor or serve assets from S3, ensure that path is included in your asset pipeline, otherwise the field silently degrades to a plain textarea in production but works locally.nova.css rather than patching the package.->withMeta(['options' => [...]]) — dump this.field in the browser console once and you'll have the whole surface area. That, plus the upload adapter hook, is effectively the entire API; there's nothing else to learn in the repo.How can I help you explore Laravel packages today?