outl1ne/nova-trumbowyg-field
An Outl1ne Laravel Nova field that brings the lightweight Trumbowyg WYSIWYG editor into your admin resources. Add a clean, customizable rich-text input for editing HTML content without the weight of larger editor packages. Simple to install and use in Nova forms.
Install it with Composer. The service provider is auto-discovered and there's no publish step — assets come from the package's dist/ directory:
composer require outl1ne/nova-trumbowyg-field
Then point the field at an existing TEXT / LONGTEXT column on a Nova resource:
use Outl1ne\NovaTrumbowygField\NovaTrumbowygField;
public function fields(Request $request)
{
return [
NovaTrumbowygField::make('Content')
->rules('nullable', 'string'),
];
}
Where to look first
src/ — the field class. This is essentially the entire package; it's small enough to read in one sitting before you write any code. Do this first, because the README and the package description already disagree with each other: the description advertises Trumbowyg::make(), while the shipped class is NovaTrumbowygField under the Outl1ne\NovaTrumbowygField namespace. Trust the source for the exact class name, namespace, and whether configuration is a generic ->options([...]) passthrough or has dedicated helpers (->height(), ->toolbar()). Five minutes here saves an hour of guessing.dist/ — the compiled Trumbowyg JS/CSS and the Vue field component. When the editor renders as a bare textarea or with no styling, the browser Network tab tells you immediately whether these loaded.btns, plugins, lang, semantic, resetCss, autogrow, svgPath. The package is a thin wrapper; the real configuration surface is Trumbowyg's.First use case
Swap a plain Textarea for this field on your Post / Page / Article resource. Before touching plugins or uploads, validate the full round trip: create a record with bold text, a link, and a list; save; reload the edit form; confirm the same HTML repopulates the editor. Also check the detail and index views — confirm whether they render the HTML (via ->asHtml() or similar) or dump raw markup, and add ->displayUsing() if it looks wrong. This single smoke test catches the overwhelming majority of integration problems — missing assets, malformed options, silent value casting — before they compound.
Centralize configuration in your own base field class
Nearly all behavior is driven by an options array serialized to the frontend per field. Capture your toolbar and defaults once so resources stay declarative:
namespace App\Nova\Fields;
use Outl1ne\NovaTrumbowygField\NovaTrumbowygField;
class RichText extends NovaTrumbowygField
{
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute ?? str_replace(' ', '_', strtolower($name)), $resolveCallback);
$this->options([
'btns' => [
['viewHTML'],
['undo', 'redo'],
['formatting'],
['strong', 'em', 'del'],
['link'],
['unorderedList', 'orderedList'],
['removeformat'],
['fullscreen'],
],
'semantic' => false,
'resetCss' => true,
])->rules('nullable', 'string', 'max:65535');
}
}
Every resource then uses RichText::make('Body'), and a toolbar change is a one-file edit. Keep the options array JSON-safe — strings, numbers, arrays only. Closures, models, or Carbon instances will break serialization silently.
Sanitize on write, not only on render
The field stores exactly the HTML the editor emits. Any editor can be bypassed via devtools, a direct API call, or a paste from Word. Treat the column as untrusted at the boundary:
static::saving(function ($post) {
$post->body = clean($post->body); // mews/purifier / HTMLPurifier
});
Pair it with render-time escaping or a second sanitize pass on output, and define an explicit allowed-tag policy in config. Keeping the content model as plain HTML is also the cheap migration path to TipTap/ProseMirror later if you outgrow this.
Per-locale editors for translatable columns
The field inherits standard Nova text resolution, so with spatie/laravel-translatable you render one editor per locale:
foreach (config('translatable.locales') as $locale) {
$fields[] = RichText::make(__('Content')." ({$locale})", "content->{$locale}")
->rules('nullable', 'string');
}
Verify ->resolveUsing() / ->fillUsing() behavior against your Nova major before rolling this out broadly — dotted-attribute handling is where translatable integrations usually break.
Lean on validation and help text for guardrails
There's no built-in sanitization, so use rules for structure and length and communicate limits in the UI:
RichText::make('Body')
->rules('nullable', 'string', 'max:65535')
->help('Basic formatting only. Images and tables are disabled.');
Do XSS filtering in the sanitizer, never in the validation ruleset — validation is UX, sanitization is security.
Add plugins deliberately
Enable colors, table, upload, or paste only on the resources that need them. Each adds bundle weight, jQuery surface, and CSP risk. If you enable upload, you own storage, URL generation, and delete handling — Trumbowyg's upload is callback-based and does not know about Laravel's filesystem, so budget a small custom Nova route or an inline upload: { serverPath: ... } handler.
Derive a text column for search and previews
Rich HTML pollutes full-text search with tag syntax and truncates badly in listings. Maintain a body_text column populated in the same saving observer via strip_tags(), and search that instead:
$post->body_text = trim(preg_replace('/\s+/', ' ', strip_tags($post->body)));
Rollout sequence
Textarea.$ / jQuery conflicts and pin or alias if needed.{!! $model->body !!} on a public page without a sanitizer, and audit every endpoint that can write to the same column — including APIs that bypass Nova entirely.'unsafe-eval' from jQuery) may be blocked. Test behind your real CSP before assuming it works; the failure mode is a blank field with only a console violation.--no-dev. Run php artisan package:discover and check the lockfile.->options([...]) break the component silently or throw. Stick to primitives and arrays, and if a toolbar button mysteriously disappears, dump the serialized field payload first.composer.json against your installed major, then lock it:
"outl1ne/nova-trumbowyg-field": "2.0.*"
Assume you may need to fork into a private package; keeping a small Nova field aligned with the frontend API is a bounded task.vendor/. Options are a passthrough to Trumbowyg, so the clean extension is the base class above — plus, if truly needed, a custom Vue component registered from your own resources/js/components. Edits inside vendor/ vanish on composer update.LONGTEXT accordingly) and pollutes full-text indexes with tag syntax — see the derived text column pattern above.dist/? (2) Console — jQuery conflict or CSP violation? (3) DOM — is the textarea present but uninitialized (options serialization failure)? (4) php artisan package:discover — is the provider registered? (5) Lockfile — does the package's Nova constraint match your installed major?How can I help you explore Laravel packages today?