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

marshmallow/nova-tiptap

A Tiptap-powered rich text editor for Laravel Nova. Replace Nova's default field with a modern, extensible WYSIWYG experience, featuring configurable toolbar options and clean JSON/HTML output for flexible content editing. Easy to install and customize for your admin panels.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Justify the install before writing any code

Nova 4+ already ships a Tiptap field. The only reason to add this package is a nameable capability gap — a toolbar layout the native field won't give you, mention nodes, media/video nodes, table support, markdown input rules, or parity with a Tiptap editor already running in your app front end. Write the gap down in the PR description. If you can't, use the native field.

2. Install, publish, and confirm the asset landed

composer require marshmallow/nova-tiptap
php artisan vendor:publish --provider="Marshmallow\NovaTiptap\FieldServiceProvider"

Check the README for the exact publish tag. The compiled Vue component must land in public/vendor/ (or be wired into your Nova asset build). If nothing publishes, the field mounts as an empty box — not an error. Verify the file exists before you debug anything else.

3. Pin the version, record the matrix

Single maintainer, zero dependents, no crowd-sourced compatibility evidence. Commit the exact version (no ^) and note the tested combination — Nova major, Tiptap major, PHP version — in your repo README. This is the cheapest insurance you'll buy all sprint.

4. Read two things in the source before configuring

  1. The field class — what does it persist, HTML or ProseMirror JSON?
  2. The Vue component — is the toolbar fixed, or does it expose extension/toolbar registration?

Those two answers determine everything downstream: your sanitizer profile, your index column strategy, your API contract, and whether custom nodes are even possible without a fork.

5. First use case: a low-blast-radius surface

Don't start on your most important Resource. Pick an internal docs model, a Draft resource, or a staging copy field. Add exactly one field and verify the full round-trip: create → edit → view → index → validation failure → repopulate.

use Marshmallow\NovaTiptap\Tiptap;

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

Implementation Patterns

Wrap the field — always

The single highest-value day-to-day pattern. Every Resource references your class, so abandonment, forking, or swapping to the native field is a one-file change.

namespace App\Nova\Fields;

use Marshmallow\NovaTiptap\Tiptap;

class RichText extends Tiptap
{
    public function __construct($name, $attribute = null, callable $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);

        $this->withMeta([
            'toolbar'    => config('nova-tiptap.toolbar'),
            'extensions' => config('nova-tiptap.extensions'),
        ]);
    }

    public static function forArticle(): static
    {
        return (new static('Body'))->rules(['required', 'string', 'max:65535']);
    }

    public static function forExcerpt(): static
    {
        return (new static('Excerpt'))
            ->rules(['nullable', 'string', 'max:512'])
            ->withMeta(['toolbar' => ['bold', 'italic', 'link']]);
    }
}

Then RichText::forArticle() in the Resource. Config drift across dozens of Resource files is how a Nova editor field becomes unmaintainable — the wrapper is where you prevent it.

Put the storage-format decision in the wrapper, not the Resource

HTML is universally consumable (Blade, APIs, search indexes, PDF exporters, mobile) but lossy and needs sanitizing. ProseMirror JSON preserves the document model but forces every downstream consumer to carry a renderer — you've coupled the database to a schema nothing else understands.

Pick one, write it down, and encode it as an accessor/mutator or cast so it can't drift per-Resource:

protected function body(): Attribute
{
    return Attribute::make(
        set: fn (?string $value) => $value === null
            ? null
            : app(HtmlSanitizer::class)->clean($value),
    );
}

Sanitize at the write boundary, not in the field

Client-side schema constraints reduce malformed markup; they don't eliminate it (paste is the vector). Sanitize in the model or a saving observer using an allowlist that matches exactly the node/mark set your toolbar exposes. If the toolbar can't produce a <table>, the sanitizer should strip tables. This is also your XSS boundary for any Blade view using {!! !!}.

Give each content type its own toolbar profile

This is where the package actually earns its keep over a generic WYSIWYG. Drive toolbars from config, keyed by content shape:

// config/nova-tiptap.php
'toolbars' => [
    'article' => ['heading', 'bold', 'italic', 'bulletList', 'orderedList', 'link', 'blockquote'],
    'excerpt' => ['bold', 'italic'],
    'legal'   => ['bold', 'italic', 'link'],
],

A legal-copy field should not offer headings. Narrowing the toolbar narrows your sanitizer profile, your schema surface, and your review burden simultaneously.

Handle media as a separate concern first

Keep the editor text-only and pair it with a Nova File/Image field. If you genuinely need inline media, check whether the package ships upload support — otherwise you're wiring a Tiptap media node to your own endpoint: upload route, authorization, node view, delete cleanup. Budget that as a ticket, not a configuration toggle.

Decide the index/detail rendering up front

HTML dumped into a Nova index column renders as a wall of markup or truncates mid-tag. Either override the display, or exclude it from the index and expose a derived attribute:

Text::make('Excerpt', fn () => str($this->body)->stripTags()->limit(80))
    ->onlyOnIndex(),

For detail views, render the sanitized HTML — never the raw column — or you've reintroduced the XSS surface you just closed.

Test the contract, not the editor

You cannot meaningfully unit-test a ProseMirror instance. Test the boundaries:

it('strips script tags on save', function () {
    $article = Article::factory()->create(['body' => '<p>ok</p><script>alert(1)</script>']);
    expect($article->fresh()->body)->not->toContain('<script>');
});

it('preserves allowed marks', function () {
    $article = Article::factory()->create(['body' => '<p><strong>bold</strong></p>']);
    expect($article->fresh()->body)->toContain('<strong>');
});

Plus a browser smoke test asserting the editor mounts and accepts input on one Resource. That test catches the blank-editor failure mode, which is the one that actually hurts.

Roll out one Resource per PR

Each diff stays small, reviewable, and revertible with one commit. Never bulk-swap every editor field in a release — if the component fails to mount, you want the blast radius to be one admin screen.


Gotchas and Tips

A broken build fails silently. Vue/Tiptap/Nova version mismatches surface at component mount, not at boot. You get a blank box in production with nothing in laravel.log. Mitigation: a CI job that runs the Nova asset build, plus a post-deploy smoke test on one Resource. Treat "blank editor" as the default failure mode and design around it.

The compatibility triangle is unverified. Nova major (the Vue 2 → Vue 3 boundary) × Tiptap major (v2 → v3 changed extension APIs) × build toolchain (Mix vs Vite). With ~20 stars and zero dependents there is no crowd-sourced evidence any combination works. Prove your combination on a throwaway branch before committing to it — the failure is a blank editor, not a composer conflict, so you get no early warning.

vendor:publish vs. your own Nova build. If you rebuild Nova assets in CI, a published third-party component can be overwritten or left stale. Confirm which mechanism the package expects and make it explicit in your deploy script. This is the most common "worked on my machine" failure with Nova field packages.

Pin hard, and assume you may fork. MIT license, small surface area — the fork is realistically one to two days if your surface is thin. One wrapper class, zero direct package references scattered through Resources. Keep it that way and your exit cost stays measured in days.

Extension points before forks. If you need a custom node, register it through the package's extension configuration (if exposed) or via a small local Tiptap extension — not by editing vendor code. If the toolbar is fixed and there's no extension registration hook, that's a hard ceiling; verify it before building a feature on top of it.

Paste is where the bugs live. Before rollout, deliberately paste from Word, Google Docs, a web page containing a table, and a fenced code block. Schema-constrained editors reject some of these silently, and the lost content is what users report as "the editor ate my article."

Keep the sanitizer as strict as the toolbar. The moment your allowlist is looser than what the toolbar can produce (say, style attributes "for convenience"), you have an XSS surface — especially if any view renders with {!! !!}.

Verify repopulation on validation failure. Confirm the editor correctly restores old input when Nova redirects back with $errors. This only breaks on the error path, looks like data loss to users, and is trivially missed during happy-path testing.

Migrating content is a separate pass. Native Nova Tiptap → this package is usually direct (both HTML). Markdown → this package is a conversion job: nested lists, code fences, and tables are where fidelity dies. Run it on a copy, diff rendered output for ~50 records per content shape, then ship.

Budget for ProseMirror literacy. Anything past a default toolbar requires understanding schema, nodes, marks, and extensions. That curve — not the package — is the hidden cost. If nobody on the team has it, scope the first ticket as a time-boxed spike and treat "we learned the schema" as a valid deliverable.

No editorial workflow ships with it. Approvals, scheduling, versioning, localization, audit trails — none of it. Bolting those onto a field is the wrong shape of investment; if they're on the roadmap, that's a content-platform decision, not a field configuration.

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