gusmanson/localtinymce
A Laravel package that bundles TinyMCE locally so you can add a rich text editor without relying on a CDN. It publishes the editor assets and offers simple configuration for integrating TinyMCE into Blade views.
Install the package in a Laravel app that already has Nova installed:
composer require gusmanson/localtinymce
Publish the package assets so TinyMCE's JS/CSS and the upload endpoint config land in your app:
php artisan vendor:publish --provider="Gusmanson\\Localtinymce\\FieldServiceProvider"
Check config/localtinymce.php (if present) and public/vendor/localtinymce/ after publishing — that's where the self-hosted TinyMCE build lives. If your build pipeline uses php artisan storage:link, run it now so uploaded images resolve via the public disk.
First use case — a rich-text field on a Nova resource:
use Gusmanson\Localtinymce\LocalTinyMCE;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
LocalTinyMCE::make('Body'), // stored as HTML in a text/longText column
];
}
Where to look first:
resources/js/field.js or the compiled public/js/field.js — this is the Vue component wrapping the TinyMCE editor instance.config/localtinymce.php — the main knobs for editor options and upload behaviour.First smoke test: open the resource form, type into the field, drag a PNG into the editor, save. Verify the file landed on your configured disk and the saved HTML contains a working <img src="..."> URL. If the image renders but the URL is a relative path that breaks on a different host, that's the first thing to fix (see Gotchas).
Multiple fields, different editors. TinyMCE is initialised per component instance by the Vue wrapper, so a resource can have several LocalTinyMCE fields. If the wrapper reuses an element ID, scope the editor init to the component's own DOM node — keep that in mind if you fork the field component.
Composing with Nova's field API. Because it extends Nova's Field, all the usual modifiers work:
LocalTinyMCE::make('Content')
->rules('required', 'max:65535')
->hideFromIndex() // HTML in a table cell is unreadable
->help('Basic formatting only');
LocalTinyMCE::make('Summary')
->onlyOnForms()
->withMeta(['toolbar' => 'bold italic link']);
Prefer hideFromIndex() on every HTML field — Nova's index view renders raw HTML otherwise.
Sanitising on the way in. TinyMCE output is HTML, so treat it as untrusted. A common pattern is a Nova action or model mutator that runs the payload through an HTML purifier before persisting, or a FormRequest rule. The package does not sanitise for you — it just hands you the editor's HTML.
Upload integration tips.
public/uploads/tinymce) rather than the default media library path, so you can purge orphans independently.Storage::url() semantics) before it reaches the editor.Rendering on the front end. Store HTML as-is and render with {!! $model->body !!} only after sanitising. Alternatively, keep the raw HTML and render through a Markdown/HTML pipeline that strips scripts.
Local-first workflow. The main selling point: no CDN calls. This matters for offline dev, air-gapped environments, and GDPR-sensitive setups where third-party scripts are blocked. Verify your public/vendor assets are deployed in CI — some build pipelines skip them.
Asset publishing is easy to forget in deploys. public/vendor/localtinymce/ is not committed by default and not rebuilt by npm run build. Add php artisan vendor:publish --force --tag=localtinymce-assets (or the equivalent) to your deploy script, or commit the assets.
Editor initialised with the wrong height / hidden in tabs. TinyMCE inside Nova tabs or modals often needs a manual resize because the container has zero height when tinyMCE.init runs. If you fork the Vue field, call tinymce.get(id).mode.set('design') or a resize on the next tick.
Upload route 419/401 errors. The packaged upload endpoint is likely behind Nova's auth middleware. If uploads fail while the rest of Nova works, check:
config/localtinymce.php for the route prefix and middleware.X-CSRF-TOKEN header the Vue component sends (it should read from document.querySelector('meta[name="csrf-token"]')).Relative image URLs. Nova runs on one host/path; if your front-end app lives elsewhere, stored <img src="/storage/..."> breaks. Configure a full URL base (env-driven) in the upload response, not the raw storage path.
Toolbar/config overrides don't stick. TinyMCE options passed via withMeta are usually merged into the JS init() call, but if the package hardcodes an init object, overrides may be ignored. Inspect the Vue component to confirm the merge order — later object keys win, so pass overrides last.
No sanitisation, no version guarantee. With zero stars and no dependents, expect an unmaintained dependency. Pin the version in composer.json ("gusmanson/localtinymce": "1.x"), and audit the bundled TinyMCE version — an old TinyMCE build is a real XSS/CVE risk. Consider swapping just the vendored tinymce/ directory for a current self-hosted build if you keep the field component.
Extension points to lean on:
vendor/ — you'll re-apply patches on every composer update otherwise.Debugging checklist when the editor "doesn't appear":
tinymce.min.js load 200 from /vendor/...?mounted hook.mounted never fired — usually a Nova version mismatch.nova-components build — Nova compiles field assets on install; php artisan nova:publish may be needed after upgrading Nova.How can I help you explore Laravel packages today?