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.
Architecture fit
The package is a single-purpose Nova field: a thin PHP class (src/NovaTrumbowygField.php) that serializes a JSON-safe options array to a Vue component, plus compiled Trumbowyg JS/CSS in dist/. It inherits Nova's standard text resolution and fill behavior, so it drops onto any TEXT/LONGTEXT column with no schema change, no migration, and no publish step (service provider is auto-discovered).
From a TPM standpoint this is the right shape for the requirement: it is a configuration lever, not a subsystem. It does not introduce a content model, an API surface, or a storage abstraction. That means the durable asset is the HTML in the column, and the package is replaceable — which is what keeps the long-term risk bounded even though the package itself is thinly maintained.
The one architectural decision the package forces on us is the reintroduction of jQuery into an otherwise jQuery-free Nova 4/5 (Vue 3) frontend bundle. That is a real, if contained, coupling.
Integration feasibility
Low effort, high confidence on the happy path. Install via Composer, reference the field on a resource, validate the create → save → reload round-trip. The recommended pattern — a RichText base field class in App\Nova\Fields that centralizes btns, semantic, resetCss, height, and default rules — keeps resources declarative and gives us a single place to change toolbar policy. Extension should stay in our subclass plus our own Vue overrides, never in vendor/.
Feasibility caveats worth surfacing before commitment:
->options([...]) vs. dedicated helpers). A developer must read the class itself rather than trust documentation.Technical risk
| Risk | Severity | Mitigation |
|---|---|---|
| Bus factor 1 — 6 stars, 0 dependents, unverified release cadence | High | Pin 2.0.*; budget a bounded fork into a private package |
| XSS — field stores whatever HTML it receives | High | Server-side sanitizer (HTMLPurifier/mews/purifier) on write, plus render-side discipline |
| Silent mount failure after Nova upgrade | Medium | CI/post-deploy smoke check asserting the editor container mounts |
| jQuery collision with other Nova add-ons | Medium | Console watch for $/jQuery conflicts; pin/alias if needed |
CSP blocking inline init / unsafe-eval |
Medium | Test behind the admin CSP before rollout |
Asset/provider not discovered on --no-dev deploy |
Medium | php artisan package:discover in deploy checklist; lockfile check |
| Feature ceiling (no collaboration, media library, structured blocks) | Low now | Accept explicitly as a tactical bridge; HTML is the cheap migration path |
The material risks are ownership and sanitization — both are engineering-scoped and well understood, not open research questions. The unverified release date in the listing should be reconciled against actual tags before we pin a constraint; "released 2026-06-10" is a signal to check, not a fact to rely on.
Key questions
composer.json constraint actually cover it?spatie/laravel-translatable with content->{locale}), and does resolveUsing/fillUsing behave correctly on our Nova version?Stack fit
Laravel + Nova, PHP-side only, Composer-distributed, MIT. It fits the existing toolchain with zero new infrastructure and no external service dependency. The only friction points are frontend-environmental: jQuery reintroduction, CSP, and dark-mode styling against Trumbowyg's light-theme CSS.
It fits cleanly if and only if Nova is a settled platform decision. Because the field runs exclusively inside Nova, adopting it is a small implicit reaffirmation of that layer. If the team is concurrently evaluating Filament or a custom admin, that conversation should be closed first — a field package must not anchor an admin-panel decision.
Migration path
TEXT/LONGTEXT column. No migration, no backfill.Textarea: swap the field declaration; existing plain-text rows render as unstyled text in the editor. Acceptable for most cases.Compatibility
Verify against the installed Nova major before pinning; the failure mode of a mismatch is silent (blank field or uninitialized textarea), not a loud exception. Confirm jQuery has no conflicts, confirm the admin CSP permits the editor's init path, and add a small dark-mode override stylesheet in Nova assets rather than patching package CSS. For per-locale content, render one editor per configured locale using the content->{locale} attribute path and confirm resolution behavior against our Nova version.
Sequencing
saving hook or an observer. Do not defer this.Text/Textarea) and the one-line rollback.Deliberately defer plugins (colors, table, upload, paste) until a specific requirement justifies the added bundle weight and CSP surface.
Maintenance
This is the dominant operational cost, and it is organizational rather than technical. With 6 stars, 0 dependents, and a single maintainer, we should assume we are the maintainer of last resort. That means: pin an explicit constraint (2.0.*), track upstream release cadence, and pre-agree a fork trigger — for example, "if upstream has not shipped a Nova-compatibility fix within 30 days of a Nova major, we fork into a private package." The fork itself is bounded and small: a field class plus a Vue component plus compiled assets. Assign a named owner and put the fork trigger in the decision record.
No publish step simplifies deploys, but it also means failure surfaces at runtime, not at build time — hence the CI mount check.
Support
MIT, no vendor, no SLA, no support contract. Internal support burden is "whoever owns the field." Document the debug checklist in the runbook so on-call does not rediscover it:
php artisan package:discover — provider registered?Scaling
Two dimensions:
body_text column (tags stripped at index time) or strip at query time. This is a schema decision to make at rollout, not after search quality complaints.Failure modes
| Symptom | Likely cause | Blast radius |
|---|---|---|
| Editor renders as plain textarea | Provider not discovered (--no-dev deploy) or assets not served |
Authoring degraded, no data loss |
| Blank field, no error | Nova upgrade broke component mount; or options serialization failure | Silent, dangerous — needs CI guard |
| Toolbar button missing | Malformed btns options or plugin not enabled |
Cosmetic |
| Editor broken intermittently across resources | jQuery version collision with another Nova add-on | Cross-feature, hard to diagnose |
| Editor won't initialize behind admin CSP | Inline init / unsafe-eval blocked |
Environment-specific; staging may pass, prod fail |
| Stored XSS on public render | No server-side sanitizer | Security incident — the one failure that matters |
The critical asymmetry: cosmetic and mount failures are recoverable and visible to authors; unsanitized HTML is silent until exploited. Sanitization is non-negotiable and belongs in the same release as the field, not a follow-up ticket.
Ramp-up
Low for anyone who has touched Nova. The field is a one-liner; the real onboarding is two conventions — centralize configuration in a base field class, and never edit vendor/. Budget roughly half a day for a Nova-fluent developer to complete spike, round-trip validation, and sanitizer wiring; add time for translatable columns or image upload if in scope. Non-technical ramp-up for authors is near-zero: it's a familiar toolbar.
The documentation gap (README lagging the code) is a minor onboarding tax — mitigate by having the first implementer write a short internal README covering the actual option API, the sanitizer policy, and the debug checklist. That artifact is worth more than the package's own docs and survives a fork.
How can I help you explore Laravel packages today?