waynestate/nova-ckeditor4-field
A Laravel Nova field that integrates the CKEditor 4 WYSIWYG editor into your admin panel. Add rich text editing to any Nova resource with configurable toolbars, image and file uploads handled through Nova, and straightforward customization for content-heavy applications.
Prerequisites
composer.json for the supported Nova/PHP versions before installing).TEXT, MEDIUMTEXT, or LONGTEXT depending on expected content size.Install
composer require waynestate/nova-ckeditor4-field
If package discovery is disabled, register the package service provider in config/app.php. Most Nova field packages also require publishing their frontend assets:
php artisan vendor:publish --provider="Waynestate\NovaCKEditor4Field\FieldServiceProvider"
php artisan nova:publish
php artisan optimize:clear
The exact provider name and publish tag are in the package README — verify them in vendor/waynestate/nova-ckeditor4-field/ rather than guessing.
Where to look first
vendor/waynestate/nova-ckeditor4-field/src/ — the Nova Field class, which tells you the exact class name, available fluent methods, and how options are passed to CKEditor.vendor/waynestate/nova-ckeditor4-field/resources/js/ — the Vue component that calls CKEDITOR.replace(); useful when debugging initialization or adding custom plugins.vendor/waynestate/nova-ckeditor4-field/dist/ — compiled assets actually served to Nova.README.md and CHANGELOG.md — especially for Nova major-version compatibility notes and the 2.0.x fillAttribute() change.composer.json — Nova version constraints and autoloaded service providers.First use case
Add the field to a Nova resource for an existing body / content column:
use Waynestate\NovaCKEditor4Field\CKEditor4Field;
public function fields(Request $request)
{
return [
CKEditor4Field::make('Body', 'body')
->rules('required', 'max:65535'),
];
}
Create or edit a record in Nova and confirm the editor renders. If it does not, check the browser console and the Nova asset registration before changing application code.
Basic resource integration
Treat the field as a form input that stores raw HTML. The package handles the editor UI; you remain responsible for storage, validation, display, and sanitization.
CKEditor4Field::make('Content')
->help('Rich text content displayed on the public site.')
->rules('required', 'string', 'max:65535'),
Configuring the toolbar and editor behavior
CKEditor 4 is configured through a configuration array. The field typically exposes a fluent method that forwards options to CKEDITOR.replace() — inspect the field class for the exact method name (options(), config(), or similar). A typical pattern:
CKEditor4Field::make('Body')
->options([
'height' => 400,
'toolbar' => [
['Source', '-', 'Bold', 'Italic', 'Underline', 'Strike'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'],
['Link', 'Unlink', 'Image', 'Table', 'HorizontalRule'],
['Format', 'Styles', 'RemoveFormat'],
['Maximize'],
],
'allowedContent' => true,
'extraPlugins' => 'justify,colorbutton',
]);
Keep shared toolbar definitions in a small helper class or config file so multiple resources and fields stay consistent:
class EditorConfig
{
public static function basic(): array
{
return ['height' => 300, 'toolbar' => [...]];
}
}
Fill behavior and the 2.0.1 change
Since 2.0.1, the field’s fillAttribute() returns the parent Nova result by default instead of swallowing it. In practice this means the normal Nova fill pipeline runs for the field: the request value is handed to the model, so attribute mutators, casts, and any other fill logic you rely on are applied as expected when a record is saved.
Consequences worth knowing:
fillAttribute() only to work around the missing return value, you can usually delete that override and let the parent handle it.fillAttribute() for real reasons, make sure you still return a value (or explicitly delegate to parent::fillAttribute()), otherwise the attribute may not be filled at all.// Model: sanitization applied on write
public function setBodyAttribute($value): void
{
$this->attributes['body'] = (new HTMLPurifier)->purify($value);
}
Displaying the stored HTML outside the form
The package is a form field. For Nova index/detail views, use Nova’s built-in display fields instead:
Text::make('Body')
->asHtml()
->onlyOnDetail(),
Text::make('Body')
->displayUsing(fn ($value) => Str::limit(strip_tags($value), 80))
->onlyOnIndex(),
asHtml() does not sanitize. Only render HTML you have sanitized or that was authored by trusted administrators.
Sanitization and content pipeline
A practical pattern is to sanitize on write and store the cleaned HTML:
// In a FormRequest, observer, or model mutator
use HTMLPurifier;
public function setBodyAttribute($value): void
{
$this->attributes['body'] = (new HTMLPurifier)->purify($value);
}
If HTML must be rendered unescaped in Blade, consider Laravel’s AsHtmlString cast after sanitization, rather than sprinkling {!! !!} throughout views.
Image and file uploads
CKEditor 4 does not ship a working upload backend. If the toolbar includes Image or Upload, you must provide an endpoint and configure CKEditor’s file browser settings:
->options([
'filebrowserUploadUrl' => route('ckeditor.upload'),
'filebrowserUploadMethod' => 'form',
])
The endpoint must validate MIME types, size, and authorization, and it must return the JSON/HTML response CKEditor expects. Many teams avoid this entirely by using a separate Nova File / media library field and referencing uploaded URLs in the editor.
Validation and database choices
longText for realistic article content.max: matching the column capacity.required only if empty content is truly invalid; CKEditor can submit empty markup.Testing
At minimum, assert the resource exposes the field and that stored HTML survives a save round-trip — including through any fillAttribute() override you add. If you add custom upload endpoints or sanitization, cover them with feature tests rather than testing the CKEditor UI itself.
CKEditor 4 is end-of-life
CKEditor 4 reached end-of-life in 2023 and no longer receives security patches. This is the most important risk with this package. For new projects, evaluate CKEditor 5 or a maintained alternative. If you must keep CKEditor 4, restrict editor access to trusted administrators and sanitize output aggressively.
fillAttribute() behavior changed in 2.0.1
Version 2.0.0 and earlier could drop the parent result when filling the attribute; 2.0.1 returns it by default. If you upgraded and now see different stored values (for example, sanitized output where you previously got raw input), that is why. Audit any custom field subclass that overrides fillAttribute() — both upgrading to 2.0.1 and staying on 2.0.0 carry risk here, so pin the exact version you have tested.
Nova version compatibility
This is a small package with a low release cadence relative to Nova major versions. Pin both laravel/nova and this package in composer.json, read the CHANGELOG before upgrading Nova, and test in a staging environment. A Nova upgrade can break field assets or the Vue component even when Composer resolves successfully.
Field renders blank or unstyled
Usually an asset registration issue. Run:
php artisan nova:publish
php artisan optimize:clear
Then hard-refresh the browser and check the Network tab for 404s on the package’s dist/ files. Also confirm the service provider is registered if discovery is disabled.
Content Security Policy conflicts
CKEditor 4 uses inline styles and, depending on plugins, dynamic script evaluation. A strict CSP can prevent initialization. Options:
basePath.Duplicate editor instances
If you wrap or extend the field, ensure the CKEditor instance is destroyed on component teardown (CKEDITOR.instances[id].destroy()). Nova modals and InlineRelationships can mount/unmount fields repeatedly, leading to “editor already exists” errors or duplicated toolbars.
allowedContent is not a security boundary
CKEditor’s content filtering controls editor behavior, not server-side safety. A user can submit HTML directly to your API or bypass the editor. Always sanitize on the server — and note that the 2.0.1 fill fix makes mutator-based sanitization behave consistently, which is a reason to prefer it over per-controller cleanup.
Upload endpoint CSRF and response shape
CKEditor’s uploader expects specific response formats and may not include Laravel’s CSRF token automatically. If uploads fail silently, inspect the Network response and configure filebrowserUploadMethod, CSRF headers, or switch to a separate upload field.
Config cache surprises
Laravel’s config cache can retain stale package configuration after install or upgrade. Run php artisan config:clear when changing published config files.
Large content and MySQL limits
Very large HTML payloads can hit max_allowed_packet, request body limits (PHP post_max_size, nginx client_body_buffer_size/client_max_body_size), or validation limits before the database complains. Align these limits with your max: rule and column type.
Debugging checklist
dist/ assets, failed upload requests.php artisan route:list: confirm any custom upload route exists and uses the correct middleware.vendor/: confirm the option method name, how options reach CKEDITOR.replace(), and what fillAttribute() returns.Extension points
The practical extension points are the CKEditor configuration array (toolbar, plugins, height, upload URLs) and the surrounding Laravel pipeline (mutators, observers, FormRequests, sanitization, display fields). The fillAttribute() hook is a supported seam since 2.0.1 — always return the parent result unless you have a reason not to. If you need deeper changes, copying the field’s Vue component into your own Nova field package is usually safer than modifying vendor/ directly.
How can I help you explore Laravel packages today?