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

marshmallow/nova-tinymce

A TinyMCE rich-text editor field for Laravel Nova, adding a WYSIWYG input to Nova forms so you can create and format content with familiar TinyMCE editing tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish. The package ships as a standard Nova field package, so the flow is Composer → publish assets/config → use the field class.

# Pin to >= 2.3.2: earlier 2.x releases register their JS assets under
# capitalised names, which breaks on hosts that lowercase URLs (see Gotchas).
composer require marshmallow-packages/nova-tinymce:^2.3.2

# Publish the package config/assets (confirm the provider/tag names from the README,
# since small packages often change them between releases)
php artisan vendor:publish --provider="Marshmallow\NovaTinyMce\NovaTinyMceServiceProvider"
php artisan storage:link   # required if inline image uploads write to the public disk

Nova 4+ discovers fields via Composer; if you see a blank textarea or a "component not found" error, run php artisan nova:publish and php artisan optimize:clear.

Where to look first

  1. README.md — the only reliable source for the exact namespace, config keys, and upload options for this version.
  2. config/nova-tinymce.php (after publishing) — disk, upload path, TinyMCE init options, API key.
  3. src/ — the field class (usually one file extending Laravel\Nova\Fields\Field) and the upload controller. Read both; with a 3-star package you should know exactly what code is running on your server. Also check the service provider's Nova::script(...) calls — the names it registers are the contract Nova's /nova-api/scripts/<name> route matches case-sensitively.
  4. resources/js/ — the Vue component wrapping TinyMCE; this is where default toolbar/plugin options live. Note that component names are a separate contract from the registered asset names and must stay in sync with dist/.

First use case: a CMS-style body field. Add a longText (or mediumText) column, then use the field in a resource:

// database migration
$table->longText('content')->nullable();

// app/Nova/Post.php
use Marshmallow\NovaTinyMce\TinyMCE; // verify namespace in README/src

public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Title')->rules('required', 'max:255'),
        TinyMCE::make('Content')
            ->rules('required')
            ->hideFromIndex(),
    ];
}

That gives you a WYSIWYG body field where editors can paste/upload images directly into the content, with the HTML stored in posts.content. Render it on the front end with {!! $post->content !!} only after sanitizing (see Gotchas).

Implementation Patterns

Configure the editor through field meta. The common pattern for TinyMCE wrappers is to pass an options array into the field and merge it with defaults in the Vue component. Expect something along these lines (check the README for exact method names):

TinyMCE::make('Content')
    ->options([
        'height' => 500,
        'menubar' => false,
        'plugins' => 'link lists image code table',
        'toolbar' => 'undo redo | bold italic | bullist numlist | link image | code',
        'valid_elements' => '*[*]', // be deliberate here
    ]);

If the package doesn't expose a fluent options() method, use ->withMeta(['options' => [...]]) and confirm the Vue component reads that key.

Inline image uploads. The selling point of this package is the upload endpoint wired into TinyMCE's images_upload_handler. Practical pattern:

  • Publish the config and point uploads at a dedicated disk/path, e.g. public disk → uploads/editor/.
  • Prefer a dedicated disk (editor) over dumping everything into public/ so you can move to S3 later without touching content.
  • Keep image URLs relative where possible; storing absolute URLs bakes your domain into content and breaks on environment changes.
  • If the package's upload endpoint is a fixed route, wrap it with your own authorization/middleware if needed; don't assume it's locked down to admins by default.

Sanitize on write, not on read. Treat the editor output as untrusted HTML, because TinyMCE's client-side filtering is not a security boundary:

// app/Models/Post.php
protected static function booted(): void
{
    static::saving(function (Post $post) {
        $post->content = clean($post->content); // mews/purifier or HTMLPurifier
    });
}

Alternatively use a fillUsing() callback on the Nova field, but a model mutator covers seeders, imports, and API writes too — which is usually what you want for a CMS field.

Display in Nova's index/detail views. Raw HTML in an index cell is unreadable and Nova will escape it. Two patterns:

// Index: short plain-text preview
TinyMCE::make('Content')
    ->hideFromIndex(),

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

// Detail: render the HTML
TinyMCE::make('Content')
    ->asHtml()          // if the package forwards to Nova's Text behavior
    ->exceptOnForms(),

If asHtml() isn't supported, use ->displayUsing(fn ($value) => $value) with a custom detail component, or just show it as read-only text in detail and link out to the front end.

Pair with validation. Nova rules still apply because the field submits a normal string:

TinyMCE::make('Content')
    ->rules('required', 'string', 'max:65000')
    ->help('Images are uploaded to the editor disk.');

max matters: MySQL TEXT caps at 65,535 bytes, and HTML with base64 or long image markup blows past that quickly. Use LONGTEXT if editors paste a lot.

Multi-field and per-resource configuration. If you have body and summary editors with different toolbars, define small factory methods or a config array keyed by field name, then pass the relevant options per field. This keeps resource files readable and makes it obvious which editor has image upload enabled.

Integration with media libraries. For anything beyond a single editor, consider replacing the package's uploader with a Media Library package (Spatie Media Library, nova-media-hub). Pattern: keep this field for the editing UI, but override the upload handler/route to store through your media model and return the media URL. That gives you conversions, CDN URLs, and cleanup on delete — things this package will not do for you.

Upgrading to 2.3.2. The fix only renames three registered assets to lowercase; Vue component names and TinyMCE.php's $component are unchanged, so it's a drop-in upgrade. Bump the constraint, composer update, then hard-refresh the browser and check the network tab: the nova-api/scripts/... requests should return 200 and the names should be all-lowercase. If you have a forked service provider or your own Nova::script() registrations for this field, apply the same lowercase convention there.

Gotchas and Tips

  • TypeError: t.fill is not a function on save (fixed in 2.3.2). In 2.x below 2.3.2 the package registers its assets as Nova-TinyMCE / Nova-TinyMCE-tinymce. Nova serves /nova-api/scripts/<name> and matches the name case-sensitively, so any host that normalises URLs to lowercase (SEO middleware, proxies, some CDNs) 301s the request, the script 404s, the Vue component never registers, and form submission dies client-side with t.fill is not a function — no request is sent, so nothing appears in laravel.log. Symptom is "every save on this resource does nothing, including saves with no changes". Fix: upgrade to ^2.3.2; diagnose by looking at the console error and the network tab, not the server logs.
  • TinyMCE loading/API key. Many wrappers load TinyMCE from the TinyMCE Cloud CDN, which needs a valid API key in production. If you don't want an external dependency, self-host tinymce.min.js in public/vendor and check the config option that switches the source. Verify in the network tab which URL is actually loaded; "editor doesn't appear" is almost always this or a CSP block.
  • XSS. Stored HTML from the editor is executable. Sanitize server-side (HTMLPurifier) and, if you allow raw HTML pastes, consider a strict allow-list of tags/attributes. Never rely on the toolbar's "code" view being disabled.
  • Upload route authorization. Check the package's upload controller: does it use nova middleware / auth guard? If not, add middleware or replace the route. Unauthenticated image upload endpoints are a common oversight in small Nova packages.
  • Storage visibility and URLs. php artisan storage:link is mandatory for the public disk. For S3, set the disk visibility to public or generate temporary URLs — otherwise images upload fine but appear broken in the editor after refresh.
  • 419/422 on upload. A 419 is CSRF/session (Nova's middleware ordering or a stale session); a 422 usually comes from the upload endpoint's validation (file size, MIME). Check the network response body, not just the editor's generic "upload failed" toast. Note the contrast with the asset-name bug above: upload failures do reach the server; a missing component does not.
  • Column type and casts. Don't cast the column to array/json; the field expects and submits a string. If you need structured content, store it in a separate JSON column and keep the HTML body as a plain long-text column.
  • Index rendering. Don't put a WYSIWYG field on the index view and expect readable output; hide it or add a separate preview field. Also watch for Nova escaping ->asHtml() output unexpectedly in detail views — verify in the browser, not just in the resource definition.
  • Content CSS parity. The editor won't look like your front end by default. Set TinyMCE's content_css (or the package's equivalent config) to your compiled front-end CSS so editors see the same typography and spacing as visitors.
  • Package maturity. With very few stars and dependents, pin the version in composer.json (at minimum ^2.3.2), commit the published config, and audit the Vue component and controller before relying on it. Check the release date/commit history against your Nova major version — Nova field API changes between v4 and v5 are a frequent source of "field renders as a plain textarea" bugs. Also watch for host-level URL rewriting in your infrastructure: a package that works locally can fail entirely behind a middleware or proxy that changes URL casing.
  • Debugging. JS errors appear in the browser console; asset/upload failures in the network tab; backend errors in storage/logs/laravel.log. An empty log plus a broken form almost always means the field's JS never loaded — check the /nova-api/scripts/... responses first. After any config change, run php artisan config:clear — Laravel caches published package configs aggressively in production.
  • Extension points. The cleanest customization is usually (a) publish and edit the config, (b) pass per-field options meta, and (c) subclass the field to override the component or upload handler. Avoid editing files in vendor/ — the next composer update will erase them.
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.
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
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect