pekhota/nova-hugerte
A Laravel Nova field that integrates the HugeRTE rich text editor into your admin panel, giving resources a configurable WYSIWYG editing experience with a clean, TinyMCE-compatible workflow and straightforward setup.
pekhota/nova-hugerte is a Laravel Nova form field that swaps the default textarea/Trix field for the Hugerte editor (the MIT-licensed community fork of TinyMCE 6/7). You reach for it when a Nova resource needs rich HTML authoring — CMS pages, blog posts, email templates, product descriptions — and you want a TinyMCE-class toolbar without the license questions that came with TinyMCE 7.
Minimal path:
composer require pekhota/nova-hugertephp artisan nova:publish (run it again after every package upgrade — this is the single most common "blank editor" cause).use Pekhota\NovaHugerte\Hugerte;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
Hugerte::make('Body', 'body')
->rules('required', 'max:65000')
->hideFromIndex(),
];
}
Where to look first: the package README for the exact field class name and the supported ->options() surface, then the source of the field class itself (it's small — one PHP class plus a JS component), then the Hugerte docs at hugerte.org. Hugerte's configuration object is TinyMCE-compatible, so the entire TinyMCE options reference applies to what you pass into the field.
First real use case: a Post model with a body longText column, edited in Nova, rendered back on the front end. Get that working end-to-end before touching toolbars or uploads.
On v1.2.0: the release only adds Laravel 13 support. There are no API, config, or asset changes, so nothing about the above workflow changes. If you're on Laravel 11/12 the upgrade is a no-op; if you're moving to Laravel 13, this is the version that unblocks you.
Baseline field wiring. Hugerte::make('Body') resolves the attribute by snake_case of the label; pass the second argument explicitly when your column name differs. Keep ->rules() on the field — the editor is a client-side convenience, not validation.
Centralising editor config. Rather than repeating ->options([...]) on every resource, define a base field or a small factory:
class ArticleBody extends Hugerte
{
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute, $resolveCallback);
$this->options([
'height' => 500,
'menubar' => false,
'plugins' => 'lists link table code fullscreen',
'toolbar' => 'undo redo | bold italic | bullist numlist | link table | code fullscreen',
'branding' => false,
]);
}
}
This is the pattern that pays off at scale: one place to change toolbar policy, one place to enforce a plugin allowlist, and per-resource subclasses only override what genuinely differs (a short "summary" field vs a long "body" field).
Rendering the saved HTML. Nova's index/detail views will happily dump raw markup as escaped text. Use Text::make('Body')->asHtml() for a read-only preview, or ->displayUsing(fn ($value) => Str::limit(strip_tags($value), 80)) for a summary column. hideFromIndex() is often the better call on large content columns anyway.
Sanitising on the way in. Store raw HTML, sanitise on output (or in a model mutator/observer) with HTMLPurifier or mews/purifier. Do not assume the editor's paste-cleanup is a security boundary — a crafted POST bypasses the editor entirely. Pair ->rules() with a SanitizesHtml cast or a FormRequest prepareForValidation().
Image and file handling. Hugerte's image/link dialogs can point at a URL out of the box, but uploads need a backing endpoint. Either configure the file_picker/image_upload style options to hit a route in your app, or (simpler and more common in Nova) use Nova's File/Image fields alongside and insert those URLs manually. Decide this early — retrofitting an upload pipeline into an editor already in production is painful.
Multiple editors on one form. Use distinct attribute names and let the field generate its own keys; don't hand-roll IDs. Two or three editors on a page is fine, but see the performance note below.
Testing. The editor renders into an iframe/contenteditable region, so $browser->type('body', '...') will not work. In Dusk, drive the underlying textarea or use ->script() to set content via the editor instance. For feature tests, bypass the UI and POST the attribute directly — that's the layer where your validation and sanitisation actually live.
Deployment. Add php artisan nova:publish to your deploy script after composer install and after any package bump. It's idempotent and cheap; skipping it is how you get an editor that works locally and silently fails in staging.
Version matrix, not just Laravel version. Nova field packages are constrained against Nova majors, and Nova majors track Laravel versions. v1.2.0 adding "Laravel 13 support" really means "compatible with the Nova release that ships alongside Laravel 13." Check your nova constraint in composer.json before assuming the bump applies to you — a Laravel 12 app on an older Nova gets nothing from this release. Conversely, if you're planning a Laravel 13 upgrade, this is the package version to pin.
Blank editor after upgrade. Almost always unpublished assets. php artisan nova:publish --force then hard-refresh. If it's still blank, open the browser console: a 404 on /nova-vendor/.../*.js confirms it, and a JS error means a stale cached bundle.
->options() merges shallowly. Passing ['plugins' => 'link table'] replaces the whole plugins string; it does not merge with the default. Same for toolbar and any nested array. When overriding, reproduce the full value you want, or read the package defaults first and append.
Raw JS callbacks can't travel through PHP. TinyMCE's setup, file_picker_callback, and similar options are functions. You can only pass JSON-serialisable values through ->options(). For genuine JS customisation, register your own script with Nova::script() and hook the editor instance there — budget for this rather than fighting the options array.
Content sync timing. The editor writes back to the hidden textarea on form submit. If you add custom submit handlers, wire:model bindings inside the same form, or conditional field visibility that unmounts the editor, you can lose the last keystrokes. Test the actual save path, not just typing.
CSP will bite you. The self-hosted Hugerte bundle injects inline styles and (depending on config) an iframe. A strict Content-Security-Policy needs at least style-src 'unsafe-inline' and frame-src 'self' for the editor to render correctly. Symptom: editor loads but is unstyled or zero-height.
Dark mode. Nova's dark theme doesn't automatically propagate to the editor skin. If your team uses dark mode admin, set the appropriate skin/content-CSS option in your base field class rather than per-resource.
Bundle size and page weight. Hugerte is a substantial JS payload. Several editors on one resource noticeably slow the form. For long-tail admin screens, prefer a plain Textarea with ->rows(10); reserve the rich editor for the two or three resources that genuinely need it.
Extension points. Three useful ones: subclass the field (shown above) for shared config; a model cast/mutator for sanitisation and HTML round-tripping; and Nova::script() for the rare JS callback. There's no plugin system in the package itself — anything beyond config lives in your app.
Upgrade path from v1.1.0. No breaking changes, no migrations, no config republishing. Bump the constraint, run composer update pekhota/nova-hugerte, re-run nova:publish, smoke-test one resource with an editor, done.
How can I help you explore Laravel packages today?