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 Tinymce Laravel Package

emilianotisato/nova-tinymce

Nova TinyMCE adds a full-featured TinyMCE WYSIWYG editor field to Laravel Nova, enabling rich text editing with configurable toolbars, plugins, and media support for managing HTML content directly in Nova resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the field and publish its assets, then swap an existing Textarea on a single resource. That is the whole first-day loop:

composer require emilianotisato/nova-tinymce
php artisan vendor:publish --provider="Emilianotisato\NovaTinyMCE\FieldServiceProvider"
php artisan nova:publish
use Emilianotisato\NovaTinyMCE\NovaTinyMCE;
use Laravel\Nova\Fields\Textarea; // remove

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

Look here first, in this order:

  1. vendor/emilianotisato/nova-tinymce/src/ — the PHP field class is small; read it end to end to see which methods map to TinyMCE options and what withFiles() actually registers.
  2. vendor/emilianotisato/nova-tinymce/resources/js/field.js (or dist/js/) — the Vue component, to understand how the value is bound and where the upload handler lives.
  3. config/nova-tinymce.php (after publishing) — global editor defaults.
  4. The package README on the exact tag you pinned — option names have shifted between releases, so trust the source over blog posts.

First use case: a Post or Page resource with a body longText column. Replace the textarea, edit one record, verify the HTML round-trips (save → reload → render on the frontend). Do that before touching image uploads — uploads are where the real configuration lives.

Implementation Patterns

Wrap the field once, use it everywhere. The single highest-value pattern for a 116-star community package: a thin internal factory so a future swap touches one file.

// app/Nova/Fields/RichText.php
namespace App\Nova\Fields;

use Emilianotisato\NovaTinyMCE\NovaTinyMCE;

class RichText
{
    public static function make(string $name, ?string $attribute = null)
    {
        return NovaTinyMCE::make($name, $attribute)
            ->options([
                'height' => 500,
                'menubar' => false,
                'plugins' => 'lists link image table code fullscreen paste',
                'toolbar' => 'undo redo | bold italic | bullist numlist | link image table | code fullscreen',
                'valid_elements' => '*[*]', // tighten, don't loosen
            ])
            ->rules('nullable', 'string');
    }
}

Then RichText::make('Body') on every resource keeps toolbars consistent and makes the migration to a different editor a one-file change.

Image uploads. Wire the upload disk explicitly rather than relying on the default:

RichText::make('Body')->withFiles('public')   // or 's3'

Confirm against the README for your pinned version whether the argument is a disk name or a visibility flag — this has been the most-changed part of the API. Uploads should land on a configured Laravel disk so you can move to S3 later without code changes. Keep storage:link in your deploy script and never let the editor hold a path that outlives the deploy.

Sanitize on write, not on read. The editor's client-side filtering is a UI convenience, not a security boundary. Add a mutator or observer:

// Post model
public function setBodyAttribute($value): void
{
    $this->attributes['body'] = clean($value); // HTMLPurifier / mews/purifier
}

Render with {!! $post->body !!} only after that allowlist pass. Write a feature test that saves a <script> payload through Nova and asserts it's stripped — that test is your regression guard against a future config change.

Separate the storage column from the presentation. Keep body as raw HTML for the editor, and derive a stripped excerpt and a plain-text search_text for Nova index columns and full-text search. Don't try to render rich HTML in an index cell.

Text::make('Excerpt', fn () => Str::limit(strip_tags($this->body), 80))->onlyOnIndex(),
RichText::make('Body')->onlyOnForms()->hideFromIndex(),

Detail views. For a read-only resource detail page, don't instantiate the editor — use Text::make('Body', fn () => view('nova.partials.html', ['html' => $this->body]))->asHtml() or render a sanitized fragment. Editors are wasted weight on non-editable screens.

Versioning and drafts. Because the field is "just" an HTML string, drafts fit naturally: keep body and published_body columns, or a PostRevision model capturing the HTML on save. This is how you get editorial safety out of a field that offers none.

Per-role toolbars. Branch the options() call on $request->user()->hasRole(...) so marketing gets source and code while a support user gets bold/italic/link only. Same field class, different NovaTinyMCE::make() config.

Migration from Trix/Textarea. Existing content is structurally compatible but not byte-compatible. Run a one-time normalization over existing rows (sanitize, collapse stray <div>/<span style> into the allowed tag set) rather than assuming TinyMCE will round-trip it cleanly.

Gotchas and Tips

  • Asset publishing in containers. nova:publish and vendor:publish write to public/vendor. If your image build is read-only or you build before the package is baked in, the editor renders as an empty box with no obvious PHP error. Publish at build time and assert the directory exists.
  • TinyMCE sourcing is the top failure mode. If you use TinyMCE Cloud, a missing or exhausted API key degrades the field to a blank/broken editor with a browser-console error only. Self-host the TinyMCE bundle for admin panels — you do not want a third-party network call on the critical path of every form.
  • The options() array is passed to tinymce.init() mostly verbatim. A typo (plugins vs plugin, a trailing comma in a comma-joined string) fails silently or throws inside the editor, not in PHP. Test the toolbar in the browser after every config change.
  • ->rules() still matters. It's an HTML string and can be large; use longText/mediumText, not varchar(255), and add a max: rule that matches the column so MySQL truncation never surprises an author mid-save.
  • hideFromIndex() on rich-text fields. Shipping the editor config to the index page works but is wasteful and can bloat Nova response payloads on large tables.
  • Multiple editors per resource work, but each instance re-initializes TinyMCE. On forms with 4+ rich fields, expect a visible load cost; lazy-init or collapse rarely-used panels.
  • Dependency containers and repeatable fields are the shakiest area — the Vue component's value binding on hydration is not guaranteed across Nova majors. If a field silently empties on edit, that's the first place to look, and it's a good reason to pin Nova and this package together.
  • Debugging checklist: browser console for TinyMCE errors → network tab for failed /nova-vendor/... upload requests → php artisan nova:publish → check the disk config and storage:link → confirm auth middleware on the upload route rejects unauthenticated POSTs (test it with curl).
  • Uploads fail "silently" from the author's view. The image appears in the editor (local blob) but 404s after save if the disk is wrong. Add a staging smoke test that uploads and re-fetches the returned URL after every deploy touching storage.
  • No size limit, no resizing. The package forwards the file; validation is your job. Enforce MIME + size server-side and consider an image pipeline, because editorial uploads grow storage and egress linearly and forever.
  • Version pinning is non-optional. With one maintainer and near-zero dependents, use an exact constraint ("emilianotisato/nova-tinymce": "1.x.y") and upgrade Nova and this package in the same branch. Pre-approve the fork option in writing; the codebase is a field class plus a Vue component, so a fork is genuinely cheap.
  • Licensing stays yours to check. The package is MIT; TinyMCE's own terms (self-hosted vs. Cloud vs. premium plugins) are a separate decision and not covered by the package's license.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console