wamesk/laravel-nova-tiptap-field
A Laravel Nova field that brings the Tiptap rich text editor to your admin panels, giving you a clean, extensible WYSIWYG experience for editing formatted content inside Nova resources.
This package is a Nova form field that swaps Nova's plain Textarea for a Tiptap (ProseMirror) WYSIWYG editor. You reach for it when a model has a long-form body field — blog posts, policy pages, CMS blocks — and editors need headings, lists, links and images without you hand-rolling a Vue component.
Minimal path:
composer require wamesk/laravel-nova-tiptap-field
php artisan vendor:publish --tag=laravel-nova-tiptap-field-assets
php artisan nova:publish
Then in the resource:
use Wamesk\NovaTiptapField\Tiptap;
public function fields(NovaRequest $request): array
{
return [
Tiptap::make('Body')
->rules('required', 'string')
->stacked(),
];
}
Column should be $table->longText('body')->nullable(); — rich HTML and ProseMirror JSON both outgrow text.
Where to look first. The field class in src/ is the entire public API surface — every capability you get day-to-day is a fluent method on it. Read that file before the README; at 2.x with a single-patch history, the source is more reliable than the docs. Then resources/js/field.js when the browser behaviour doesn't match what the method names imply. Confirm the exact vendor:publish tag and any storage-format method name against the README — the 2.0 line reset some 1.x names, so snippets you find in blog posts and Stack Overflow are frequently for the wrong major.
First use case, end to end. A Post resource with a body field. Render with {!! $post->body !!} behind a sanitizer, and keep a parallel plain excerpt column populated from stripped text on save — that keeps index cards, meta descriptions and search independent of whatever shape the editor emits today versus after the next upgrade.
Pin ^2.0.1. If you trialled 2.0.0 and got a blank form area, that was the upstream "Showing editor" bug, now fixed — upgrade before you start debugging your own configuration.
Decide the storage format before the first save, and never mix.
LIKE-searchable, drops into existing Blade and any Markdown/HTML pipeline you already run.LIKE queries are useless against it. Cast 'body' => 'array' and render through a Tiptap renderer or a Blade component that walks the node tree.Switching later needs a one-off backfill command. A JSON string accidentally rendered into {!! !!} prints node objects on the page — this is the most common self-inflicted wound with this field.
Validate and sanitize server-side, at write time.
Tiptap::make('Body')
->rules(['required', 'string', 'max:60000'])
->help('Rich text — headings, lists, links, images.')
->hideFromIndex();
Editor output is user input. If the field exposes its own sanitizer, wire it up; if not, sanitize on save via a mutator or saving observer using mews/purifier or symfony/html-sanitizer. Sanitizing only in the render path means dirty HTML sits in the database waiting for the next consumer — an API endpoint, an RSS feed, an email digest — to leak it.
Derive secondary fields in an observer, not in the resource. The everyday pattern:
protected static function booted(): void
{
static::saving(function (Post $post) {
$post->excerpt = Str::limit(strip_tags($post->body), 180);
});
}
In the model (or an observer) so imports, seeders and API writes inherit it. If you build it into the Nova resource, the first non-Nova write path silently produces posts with empty excerpts and nobody notices for a month.
Keep the index view cheap. Nova will dump raw HTML or JSON into a table cell and blow out row height. Either ->hideFromIndex() and show a Text::make('Excerpt'), or ->displayUsing(fn ($value) => Str::limit(strip_tags($value), 60)).
Uploads need a decision, not a default. If 2.x exposes a file/upload API (check the README — the method name varies across versions), point it at a dedicated disk and directory resolved from config, not hardcoded per resource. Editor uploads bypass your Media model relationships entirely, so decide up front whether they're acceptable orphans (fine for a blog) or need an owning record (mandatory the moment you have retention or deletion obligations).
Test the save path, not the editor. Feature-test that a payload submitted through the Nova field shape persists sanitized and derives the excerpt. The JavaScript is third-party; the risk lives in what you write to the database.
Roll out resource by resource. Declare the field only where content authoring happens — it's a heavy JS bundle and there's no reason for it to load on resources with no rich-text column.
nova:publish, hard-refresh, and check the browser console for a 404 on the field's JS. After any composer update, assume assets need republishing.Text field on the same resource to confirm the form loads, then swap in bare Tiptap::make('Body') with no chained methods. Bare field works + configured field doesn't = a chained modifier, not the package.<div>s, inline styles from an old CMS — gets silently dropped or rewritten the first time someone opens and saves a record. Backfill and validate historical content before pointing the field at it, and take a backup.{!! !!} without sanitization is an open door. The field's client-side behaviour is not a security boundary. Sanitize on save, and add a CSP as defence in depth.->dependsOn() on the same form is a known source of flicker and lost edits. Prefer separate resources or ->onlyOnForms() variants over conditional show/hide of an editor.resources/js/field.js, and that patch is a merge cost on every release of a package that ships patches.^2.0.1, read changelogs before bumping, and keep one manual or CI-adjacent check that opens a record in Nova's create, update and detail views and saves it. 2.0.1 specifically fixed a display-context bug — that's exactly the surface a mount smoke test catches.vendor/ so the fork stays optional.How can I help you explore Laravel packages today?