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

Nova4 Tinymce Editor Laravel Package

murdercode/nova4-tinymce-editor

TinyMCE editor field for Laravel Nova 4. Adds a full-featured WYSIWYG field to your Nova resources with configurable toolbars, image and media support, and easy customization, making it a drop-in upgrade over the default textarea field.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up in three steps.

composer require murdercode/nova4-tinymce-editor:2.0.3
php artisan storage:link

Pin the exact version. The package is single-maintainer with zero dependents; you want upgrades to be a deliberate act, not a composer update side effect.

Add the column (plain HTML goes in a normal Eloquent column — no new tables, no JSON schema):

$table->longText('body')->nullable();

Register the field on a resource. Check vendor/murdercode/ (or the README) once for the exact namespace before importing:

use Murdercode\NovaTinyMCE\TinymceEditor; // verify the namespace from the package's src/

public function fields(NovaRequest $request)
{
    return [
        TinymceEditor::make('Body')
            ->rules('required', 'max:65000'),
    ];
}

Where to look first. Three files, in this order:

  1. vendor/murdercode/nova4-tinymce-editor/config/ — the publishable config is the source of truth for disk, MIME types, toolbar defaults, and (post-v2.0.3) the upload route settings. Read it before you write any ->options([...]).
  2. src/ — the field class and the service provider. The provider shows you how assets are registered and, critically, how the v2.0.3 upload route is booted and which middleware group it sits in.
  3. routes/ inside the package — confirm the upload endpoint's prefix, name, and middleware yourself rather than trusting the changelog.

First use case. A Post resource with a Body field is the fastest way to validate the whole chain: editor renders → HTML saves → HTML renders back in Nova's detail view. Pair it with a read-only display field:

Text::make('Body')->asHtml()->onlyOnDetail()

Once that round-trip works, exercise the upload path with a real image before you consider the spike done. The editor is the easy part; v2.0.3's upload route is the piece that turns this from a widget into server-side surface area you now own.

Implementation Patterns

Centralize editor options, don't repeat them. Per-field ->options([...]) is great for blast-radius control, but copying the same toolbar into six resources is how you get drift. Use a small factory:

class RichText extends TinymceEditor
{
    public static function body(string $attribute = 'Body'): static
    {
        return static::make($attribute)
            ->options([
                'menubar'    => false,
                'height'     => 500,
                'plugins'    => 'link lists code table image',
                'toolbar'    => 'bold italic | link bullist numlist | image code',
            ])
            ->rules('nullable', 'string', 'max:65000');
    }
}

Then resources read as intent: RichText::body('Description').

Sanitize on output, not on save. TinyMCE emits raw HTML, and sanitizing at write time means you can never tighten your policy retroactively without a data migration. Store what the editor produced, sanitize where you render:

use Mews\Purifier\Casts\CleanHtml; // or an accessor / Blade directive

protected $casts = ['body' => CleanHtml::class];

Keep ->rules(...) for shape and length only.

Exclude the field from index queries. A longText HTML column on the resource index is a silent performance tax — every row ships the full markup to the browser.

TinymceEditor::make('Body')->hideFromIndex()

Same for search: ->searchable() on a raw HTML column matches tag names and attributes. If search matters, keep a stripped body_text column maintained by an observer and search that.

Conditional editors. Combine with dependsOn/hideWhenCreating the same way you would any Nova field. Nova re-renders the field when dependencies change, and TinyMCE re-initialises — verify this early if you use it inside FormPanel/FieldContainer layouts, since iframe-based editors are the usual casualty of a re-render.

The upload route (v2.0.3+). Treat enabling it as a feature change with a security review, not a patch bump:

  • Confirm it inherits Nova's auth middleware. Read the provider; don't assume.
  • Enforce MIME type, size cap, and filename sanitisation server-side. TinyMCE's valid_elements/images_upload_handler client checks are UX, not security.
  • Point it at a deliberate disk (config-driven, environment-specific), and decide public vs. private consciously.
  • Add rate limiting on the route if the maintainer didn't.
  • If you previously patched in a base64 images_upload_handler, remove it before upgrading — otherwise you double-handle uploads and silently bloat the DB with data: URIs.

Testing. Dusk/iframe testing against TinyMCE is painful. Set content through the editor API instead of the visible field:

$this->browse(fn ($b) => $b->script("tinymce.get('body').setContent('<p>Hi</p>')"));

Or assert on the model after saving rather than through the rendered iframe.

Gotchas and Tips

XSS is yours, not the package's. The package gives you an editor; it does not sanitize output. Any resource whose content reaches a Blade view with {!! !!} needs a sanitiser in the pipeline. This is the single most common way this package gets adopted badly.

Stale assets are the "it worked yesterday" regression. Third-party Nova assets are served from public/vendor/. After any upgrade, clear caches and verify the editor actually loads:

php artisan optimize:clear
php artisan vendor:publish   # only if the package's config/assets changed

If the toolbar looks wrong or the editor doesn't appear, check the browser network tab for a 404 on the package's JS before debugging your field definition.

Publishing config is a one-way door. Once you publish, your copy shadows the package's. Upgrades that change defaults won't reach you. Publish only if you need to change disk/MIME/toolbar defaults, and diff your published config against the package's on every upgrade.

base64 fallback means the integration doesn't fit. If the upload route is unavailable or blocked by policy and you're reaching for a base64 images_upload_handler, stop — you're storing megabytes of base64 in a longText column, and every list query drags it along. That's the signal to wire the route properly or pick a different field.

Model dirty-checking on large payloads. TinyMCE normalises markup on load (attribute order, <p> vs <br>, entity encoding). This can mark a huge HTML column dirty on save even when the operator changed nothing, which pollutes revisions and audit trails. If that matters, compare strip_tags-normalised content in a saving observer before letting the update through.

Multiple editors per form. Two TinyMCE instances in one Nova form mostly work, but each needs a distinct DOM id derived from the attribute name. If you alias attributes (->options(['selector' => ...])) or use resolveUsing, verify both editors initialise and save independently.

Version skew. Check the package's supported Nova/Laravel/PHP/TinyMCE matrix against yours at upgrade time, not at install time. Nova majors move fast and field packages lag.

Keep the rollback warm. Pin 2.0.3, note 2.0.2 as the known-good fallback, and write down what "rollback" means concretely — a composer require down-version plus a cache clear, not a code revert. Document the upload route's path, disk, and auth requirements alongside it so on-call isn't reverse-engineering vendor/ at 2am.

Instrument it. Log upload failures and watch editor asset 404s for the first few deploys. The failure mode here is quiet — editors that half-render, images that silently fail to upload — and it surfaces as user complaints a week later if you don't watch for 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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core