nevadskiy/nova-quill-field
A Laravel Nova field that integrates the Quill WYSIWYG editor into your admin panel, making it easy to add rich text content fields to Nova resources. Customize the toolbar, store content as HTML, and enjoy simple, flexible setup for forms and detail views.
Architecture fit. The package is a single Laravel Nova custom field that wraps the Quill editor: a thin PHP field class (src/Quill.php) plus a Vue component under resources/js/components. There is no service provider to register, no migrations, no domain logic, and no public API beyond Quill::make() and withMeta(). Architecturally that is close to the ideal shape for a third-party dependency — the blast radius is one field on one Nova resource, and the data contract is simply "HTML in a text column." It is not a content platform, a sanitization layer, an asset/media pipeline, or a customer-facing editor strategy, and it should never be scoped as one. The correct framing for planning purposes: this is a UI convenience that shortens the critical path on an internal admin feature by roughly a few days to a week, purchased with a small, well-bounded dependency risk.
Integration feasibility. Low effort for a team already on Nova: Composer install (auto-discovered), one longText column, one field declaration, one Blade render. Because the entire public surface is two files, a spike can be credibly timeboxed at half a day to two days including QA — not a sprint. Two items drive real effort and belong in the plan, not in the "discovered later" column:
{!! $content !!} is an XSS vector. That means a purifier dependency, an allowlist matching Quill's tag set, a write-time mutation path (mutator or observer), and test coverage for it. Small, but genuine engineering — and it is a prerequisite, not a follow-up.Technical risk. High relative to value, and the 0.1.2 patch changes the character of that risk only marginally. Signals: 4 stars, 0 dependents, a single 0.x line, last release May 2023, and the latest release is a maintainer bug fix rather than evidence of an active roadmap.
{!! !!} does not. Sanitizing only at render time leaves every other consumer — API, mail, RSS, Nova's own HTML detail view — exposed. Write-time sanitization is the only defensible posture, and it is our code, not the vendor's.class, most style, merges adjacent tags, and drops unsupported elements. Any imported legacy CMS content or Word paste visibly mutates the first time an editor opens and saves. This is product-visible behavior, not a bug, and it needs a policy decision before the field is enabled on existing content.withMeta() and read by the Vue component, so meta-key mismatches fail silently ("the option doesn't work"). Custom buttons or Quill modules require Quill.register(...) in the Vue layer — there is no PHP-only path, so those are fork-scoped, not config-scoped.Key questions to close before committing.
spatie/nova-translatable or another field wrapper? Thin field packages are exactly where wrappers break; this must be verified, not assumed.Stack fit. Laravel + Laravel Nova + Composer auto-discovery. The hard prerequisite is an existing Nova license and an established Nova resource-authoring practice; if the team is not already on Nova, this package is irrelevant and the conversation should stop. PHP/Laravel/Nova version support is undocumented, so the first spike task is establishing a tested matrix on our actual stack rather than trusting the README.
Migration path. Treat the field as greenfield-per-column wherever possible.
longText nullable, wire Quill::make(), render behind sanitization. Clean.p, br, strong, b, em, i, u, s, h1–h6, ol, ul, li, blockquote, a, img, pre, code) before enabling the field, or keep import-only content off this field entirely and expose it read-only.content_text companion column (populated in the same observer via strip_tags + truncation) should ship with the first migration, not later. LIKE '%term%' against HTML matches tag names and attributes; search, excerpts, meta descriptions, and word-count rules all need the plain-text column, and retrofitting it means a backfill.^0.1.2 at minimum. On 0.1.1, a Quill field inside a Nova draft resource can attach to the wrong form state — symptoms are content that won't persist or a second editor silently losing its value.Compatibility. Three surfaces need explicit verification during the spike, each of which is currently undocumented: (a) Nova major version and asset pipeline — confirm the package's compiled dist assets are actually served, and whether nova:publish is required; (b) draft resources — re-test after every upgrade; (c) translation wrappers, if used. Record the results as our own tested matrix in the repo, because the upstream project will not maintain one for us.
Sequencing. Recommended four-phase path with explicit gates:
withMeta() keys the Vue component reads, and capture the tested version matrix. Gate: editor mounts and round-trips on our Nova version.content_text derivation, excerpt column on index with the Quill field onlyOnForms()/hideFromIndex(), image button disabled or replaced, and ->rules(['required','string','max:65535']) as a rough guardrail. Gate: security review of the write path passes; no base64 rows in the pilot table.Quill subclass with the toolbar/placeholder baked in, rather than copy-pasted withMeta() chains across resources.Cross-cutting workstreams that must be staffed regardless of phase: dependency pinning, sanitization, media policy, markup-normalization QA, and the Nova-upgrade smoke test.
Maintenance. Sporadic and unpredictable. Pin the exact version in composer.json (not a floating ^ range) and treat every Nova major upgrade as a mandatory re-validation event — Nova's asset pipeline has broken third-party fields before, and there is no upstream commitment to fix it. The mitigating factor is genuinely favorable: the entire integration surface is two files, so forking the Vue component or the PHP field into the app is a bounded, well-understood task rather than a rewrite. Budget the fork as a plausible outcome, not a last resort, and note that preset subclasses in our own codebase reduce the amount of fork surface we'd inherit.
Support. No vendor, no SLA, no issue-response guarantee, no documentation beyond the README. Internal ownership is required from day one: a named engineer for the fork, a named owner for the sanitization allowlist (it will need to evolve), and a written record of tested versions. Escalation for "editor doesn't mount" is our own team reading src/Quill.php and the Vue component — which is tractable, but it is our time, not someone else's.
Scaling. The relevant axis is breadth of adoption, not traffic. Cost scales with number of resources, number of editors, and number of locales. Multi-locale means one Quill field per locale column — predictable, but it multiplies the sanitization and validation surface. The one place it scales badly on data volume is base64 images: row size grows with pasted screenshots, index pages slow down, saves slow down, and database backups quietly inflate. Keeping the field off index lists and forcing images through an upload endpoint are the two controls that make scaling a non-issue.
Failure modes and controls.
| Failure mode | Signal | Control |
|---|---|---|
| Stored XSS via rendered HTML | Security review finding; injected <script>/onerror survives a save |
Sanitize at write time with an allowlist; treat stored value as untrusted outside Blade |
| Base64 image bloat | Multi-MB rows; slow saves; index timeouts | Replace or disable the image handler in v1; add a size rule |
| Silent markup loss on edit | Editors report formatting "disappearing" after opening/saving imported content | Pre-convert imported HTML to Quill's tag set, or exclude import-only content from the field |
| Draft attachment misbehavior | Content not persisting in draft resources; second editor appears to lose its value | Require 0.1.2+; explicit create/attach/save/reload smoke test |
| Field fails to mount after Nova upgrade | Blank or non-interactive editor | Pin version; smoke-test on every upgrade; verify dist assets are served |
| Config silently ignored | "I set the toolbar and nothing changed" | Inspect the Vue component for the exact meta key; document keys we rely on |
| Detail view renders blank/read-only | QA on the detail page | Pair with Text::make('Content')->asHtml() on detail, hide the Quill field there |
| Custom button/module needed | Requirement exceeds config surface | Fork the Vue component and override component(); no PHP-only path exists |
Ramp-up. A Nova-experienced developer reaches productive use in half a day; the documentation gap adds an hour of meta-key guessing if they skip the source. Reduce this to near-zero with two artifacts: a field-preset subclass (ArticleBody extends Quill) that encodes the toolbar and options once, and a one-page internal note listing the meta keys we use, the tested version matrix, and the exit path. QA ramp-up is the larger cost — reviewers need a normalization checklist (open, save, diff the stored HTML) because Quill's markup rewriting is invisible until someone compares before and after.
Bottom line for the plan. Fund the pilot, not a rollout. The value is a few days of avoided editor work on an internal admin surface; the cost is a pinned dependency plus roughly a day of plumbing (sanitization, plain-text companion, image policy) that the package does not provide. Commit to expansion only after the pilot's security review and upgrade smoke test pass — and document the fork-or-swap exit path while the integration is still one resource wide.
How can I help you explore Laravel packages today?