Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Quill Field Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require nevadskiy/nova-quill-field

The package auto-discovers. There is no service provider to register and no config to publish for the basic field. Three steps get you a working editor:

// database/migrations/xxxx_create_posts_table.php
$table->longText('content')->nullable();

// app/Nova/Post.php
use Nevadskiy\NovaQuillField\Quill;

public function fields(NovaRequest $request)
{
    return [
        Quill::make('Content'),
    ];
}
{{-- resources/views/posts/show.blade.php — after sanitizing, see Gotchas --}}
{!! $post->content !!}

Where to look first. Read src/Quill.php and the Vue component under resources/js/components before you configure anything. Those two files are the API: the PHP class defines which meta keys the field accepts, and the component defines which of those keys it actually reads. There is no docs site beyond the README, and the README does not enumerate the meta keys. Fifteen minutes in the source saves an hour of guessing why a toolbar option is ignored.

First use case. A body or description column on a resource your own team authors — a blog post body, an FAQ answer, an internal doc page. Admin-only editing, one or two resources, rendered on a detail page. That is the sweet spot: low blast radius, easy to rip out or replace.

If your first use case is user-submitted content rendered to the public, stop and read the XSS and image sections below before you ship anything.

Implementation Patterns

Extract presets as a field subclass. All configuration flows through withMeta(). Instead of copy-pasting toolbar arrays across resources, centralize them:

namespace App\Nova\Fields;

use Nevadskiy\NovaQuillField\Quill;

class ArticleBody extends Quill
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);

        $this->withMeta([
            'toolbar' => [
                ['header' => [2, 3, false]],
                ['bold', 'italic', 'underline'],
                ['link', 'blockquote', 'code-block'],
                [['list' => 'ordered'], ['list' => 'bullet']],
                ['clean'],
            ],
            'placeholder' => 'Write the article…',
        ]);
    }

    public function height(string $css): static
    {
        return $this->withMeta(['height' => $css]);
    }
}

Verify each meta key against the Vue component source — a typo'd key fails silently.

Sanitize on write, not on render. This is the single most important pattern. Sanitizing only in Blade leaves your API, mail templates, RSS feed, and Nova's HTML detail view exposed. Store clean HTML once via a mutator or observer:

// composer require mews/purifier
protected static function booted(): void
{
    static::saving(function (Post $post) {
        $post->content = Purifier::clean($post->content, 'quill');

        $post->content_text = Str::limit(trim(strip_tags($post->content)), 10000);
    });
}

Allowlist matching Quill's output: p, br, strong, b, em, i, u, s, h1–h6, ol, ul, li, blockquote, a, img, pre, code. Add rel="noopener" and target restrictions on a, and src/alt/width/height on img. Once the stored value is clean, {!! !!} is safe everywhere and every consumer is protected.

Derive a plain-text companion column. LIKE '%term%' against HTML matches attributes and tag names, not just content. The content_text column above earns its keep for search, excerpts, meta descriptions, character counts, and word-count validation rules.

Keep the editor out of index lists. Nova renders the field read-only on the index, which means shipping full HTML for every row. Hide it and show an excerpt:

Quill::make('Content')
    ->onlyOnForms(),

Text::make('Excerpt', fn () => Str::limit(strip_tags($this->content), 100))
    ->onlyOnIndex(),

For the detail page, pair with an explicit HTML field so you get predictable rendering:

Text::make('Content')->asHtml()->onlyOnDetail(),

Replace or disable the image handler. Quill's default image button inserts base64 data URIs straight into the HTML — two pasted screenshots can mean a multi-megabyte database row and a slow index page. Point the toolbar's image handler at an upload endpoint (Spatie Media Library, or a small Nova controller returning { "url": "..." }) and drop the base64 behavior. If you cannot build that in the first pass, remove 'image' from the toolbar array entirely.

Validation measures the wrong thing by default. ->rules(['required', 'string', 'max:65535']) is a rough guardrail against runaway rows, but max counts markup. For "no more than N words" or a real character limit, write a rule against strip_tags($value):

public function passes($attribute, $value): bool
{
    return str_word_count(strip_tags($value)) <= 2000;
}

Multi-locale. One Quill field per locale column is predictable and works. If you use spatie/nova-translatable, verify the wrapper's per-locale rendering against this field's Vue component in a spike before committing — thin third-party fields are exactly where translation wrappers break, and a failure here is silent (values save to the wrong locale or not at all).

Smoke-test the draft flow separately. Nova's draft resources (nova:draft-resource) exercise a different code path from the edit form. Create-with-attachment before the first save is where bugs live. Test: create → attach content → save → reload → confirm the attachment resolves with the right value.

Gotchas and Tips

  • XSS is entirely yours. Nova escapes by default; {!! !!} does not. A stored-XSS hole here is not a rendering bug — it is a security incident. Sanitize on write and treat the stored value as untrusted outside Blade (some APIs and mail clients will re-render it in a different context).

  • Quill normalizes markup on round-trip. It strips class, most style, merges adjacent tags, and drops elements it does not support. Legacy CMS imports and Word-pasted content will visibly mutate the first time someone opens and saves. If you are migrating existing HTML, convert it to Quill's supported tag set before enabling the field on it — or keep that content out of Quill entirely.

  • Base64 images are the classic failure mode. Multi-megabyte rows, timeouts on index pages, bloated backups. Disable the image button or wire it to real uploads.

  • Use 0.1.2+ if you use Nova drafts. 0.1.2 fixed field attachments to use Nova's draftId instead of the resource key. On 0.1.1, a Quill field in a draft resource can fail to persist or appear to lose its value on a second editor instance. Pin ^0.1.2 and re-test any draft-based editorial workflow after upgrading.

  • 0.1.2 is a small fix on a quiet 0.x line (last release May 2023). Pin the exact version in composer.json, not a caret range you never look at again. Smoke-test on every Nova major upgrade — Nova's asset pipeline changes have broken third-party fields before, and this one has nobody watching.

  • When an option "doesn't work," it is almost always a meta-key mismatch. Configuration flows through withMeta() into the Vue component. Open the component, find the key it reads, use that exact key. When in doubt, dd() the meta array in a test or inspect the mounted component's props in the browser.

  • Editor not mounting? Check that the package's compiled assets are being served, whether nova:publish was run, and whether your Nova version is supported. Asset compilation and version compatibility are undocumented, so keep your own tested matrix in the README of the project using it.

  • Custom toolbar buttons and Quill modules require JavaScript. Quill.register(...) has to happen in the Vue layer; there is no PHP-only path. Fork the component into your app and override component() rather than fighting the package's build. This is the escape hatch for any requirement beyond stock Quill.

  • Detail view quirks. If the field renders as a blank or read-only editor on the detail page (rather than formatted HTML), follow the pattern above: hide the Quill field on detail and add Text::make('Content')->asHtml().

  • Three extension points cover nearly everything: a model observer for sanitizing and plain-text derivation, a field subclass for reusable presets, and a forked Vue component for toolbar and media behavior. The package is intentionally minimal — plan to extend it, and keep a fork-or-swap note somewhere your team will find it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator