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

Laravel Nova Tiptap Field Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

Decide the storage format before the first save, and never mix.

  • HTML string — trivial to render, LIKE-searchable, drops into existing Blade and any Markdown/HTML pipeline you already run.
  • Tiptap/ProseMirror JSON — lossless round-trips (re-opening a record doesn't renormalise your markup), but you own rendering, and 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.

Gotchas and Tips

  • Blank editor is almost always assets. Label renders, editor doesn't → unpublished or stale field assets. Re-run 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.
  • Bisect before blaming config. Put a stock 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.
  • The editor renormalises on open. Tiptap parses and re-serialises the document when it mounts. Schema-invalid legacy markup — nested <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.
  • Reactive forms don't mix well with rich text. Rich-text fields hold a lot of component state; chaining ->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.
  • Custom extensions are the fork decision. If your requirement is "Tiptap plus a custom node" — callouts, embeds, footnotes — verify 2.x exposes an extension registration hook or toolbar config before committing. If it doesn't, you're patching resources/js/field.js, and that patch is a merge cost on every release of a package that ships patches.
  • Pin and smoke-test. Single-maintainer package on a young major. Pin ^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.
  • Nova major upgrades force this package's upgrade in lockstep. There's no migration tooling. Budget one package-upgrade cycle per Nova major, and keep any customisation in your own component overrides rather than in vendor/ so the fork stays optional.
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