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

webard/nova-suneditor

A Laravel Nova field that integrates the SunEditor WYSIWYG editor into your admin panels. Add rich text editing with a lightweight toolbar, image and media handling, and configurable options—all delivered as a native Nova field you can drop into any resource.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (root of your Nova app):

composer require webard/nova-suneditor

This package is inert outside Laravel Nova — there is no standalone Blade component, no CDN script tag, and no facade. It exists to register a Nova field. If you are not already on Nova, stop here; a SunEditor integration for a non-Nova app is a different (and simpler) piece of work.

Confirm the field namespace before you write code. The class is typically Webard\NovaSunEditor\SunEditor, but this is a 3-star, single-maintainer package — read vendor/webard/nova-suneditor/src/ end to end. It is small enough to finish over coffee, and doing so answers the questions the README usually does not: which fluent methods actually exist, how options() merges with defaults, whether uploads are wired at all, and how the field resolves on detail/index views.

Verify assets actually load. Drop the field into one resource, open the create form, and look at the widget. If you see a plain textarea or an empty bordered box, Nova did not register the package's JS/CSS. Check the package's service provider for Nova::script() / Nova::style() calls, confirm the composer.json Nova constraint matches your installed Nova version, and run php artisan nova:publish to refresh the asset manifest.

First use case — swap a Textarea for a rich editor:

use Webard\NovaSunEditor\SunEditor;

public function fields(NovaRequest $request)
{
    return [
        SunEditor::make('Body', 'body')
            ->rules('required', 'string', 'max:65535'),
    ];
}

Create a record, type formatted content, save, reopen. Confirm the HTML round-trips — bold stays bold, links stay links, and nothing is silently truncated. Then check the detail and index views specifically. Rich-text fields frequently forget those contexts, so either accept default behavior or pin it explicitly:

SunEditor::make('Body', 'body')
    ->onlyOnForms()          // hide from index/detail entirely
// or
    ->showOnIndex(false)     // if you want it on detail but not index

Where to look first, in order:

  1. vendor/webard/nova-suneditor/README.md — the fluent API surface.
  2. vendor/webard/nova-suneditor/src/ — the field class and its service provider. This is your source of truth.
  3. SunEditor's upstream documentation — anything not exposed as a fluent method is usually just a raw option passed through.

Implementation Patterns

Centralize configuration in a single subclass. This is the single highest-leverage habit with this package. Every ->options([...]) call scattered across resources becomes a place you must edit when the toolbar policy changes or SunEditor renames an option upstream. Build one field:

namespace App\Nova\Fields;

use Webard\NovaSunEditor\SunEditor;

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

        $this->options([
            'height' => 'min-content',
            'buttonList' => [
                ['undo', 'redo'],
                ['bold', 'underline', 'italic', 'strike'],
                ['fontColor', 'hiliteColor'],
                ['outdent', 'indent', 'align', 'list'],
                ['table', 'link', 'image'],
                ['removeFormat'],
                ['codeView', 'preview', 'fullScreen'],
            ],
            'pasteTagsWhitelist' => 'h1|h2|h3|p|ul|ol|li|a|strong|em|blockquote|pre|code|table|tr|td|th|img',
        ]);
    }
}

Then every resource reads RichText::make('Body', 'body'). Toolbar changes are a one-file diff, not a grep across forty resources.

Build a sanitization pipeline on save — the package will not do it for you. SunEditor emits raw HTML, and the codeView button means a technically-minded author can paste <script> tags. Do not rely on client-side filtering. Two viable patterns:

RichText::make('Body')
    ->rules('required', 'string', 'max:65535')
    ->fillUsing(function ($request, $model, $attribute) {
        $model->{$attribute} = clean($request->input($attribute)); // mews/purifier
    });

or sanitize on resolve:

RichText::make('Body')->resolveUsing(fn ($value) => clean($value));

Pick one and be consistent. Sanitize-on-save is better for performance and gives you a clean database; sanitize-on-resolve is safer defense-in-depth if you ever have other write paths. Most teams do both, or at minimum, sanitize at output on any public-facing template.

Image uploads require you to own the endpoint. Enabling the image button in SunEditor does not ship you a working upload backend. The editor will POST to whatever URL you configure:

->options([
    'imageUploadUrl'      => '/nova-api/suneditor-upload',
    'imageUploadHeader'   => ['X-CSRF-TOKEN' => csrf_token()],
])

You then write the controller: validate image/* MIME with mimes:jpg,jpeg,png,webp, enforce a size cap, store on a private disk, and serve through an authorized route. Do not point this at public/ unless the images are genuinely public content and you have a reason to accept the risk.

Guard against oversized payloads at the field level and the schema level. HTML blobs grow fast. Add ->rules('max:65535') (or lower, based on your column) and confirm the database column is TEXT or MEDIUMTEXT. MySQL TEXT caps at 64KB; a single pasted article with a base64-embedded screenshot will blow past that and produce the classic "it saved but content disappeared" truncation bug, which is miserable to debug because nothing errors.

Detail and read-only views need explicit handling. A raw editor widget on a detail page is either broken or a security surface. Use ->onlyOnForms() for records where detail views show content through your frontend, or ->readonly() if you want the editor visible but locked. If you render the stored HTML in Blade, only do so raw if you sanitized on save; otherwise wrap with Purifier::clean().

Migrating from an existing editor. Same column type, same content shape. Audit before you flip:

SELECT id, LENGTH(body) FROM posts ORDER BY LENGTH(body) DESC LIMIT 10;

Catch the oversized rows and any badly malformed HTML first, sanitize them in a one-off command, then swap the field. This is a field-level change, not a data migration — that property is the real strategic value of storing HTML.

Add a feature-flagged fallback and stop worrying about upgrades.

env('USE_SUNEDITOR', true)
    ? RichText::make('Body', 'body')
    : Textarea::make('Body', 'body')->rows(15);

If a Nova upgrade breaks asset loading, you flip one env var and buy yourself time. This is not paranoia — it is a 3-star dependency with zero dependents and no upgrade pressure from the ecosystem.

Gotchas and Tips

Nova version drift is your primary compatibility risk. The package will not automatically track Nova majors. Pin the version explicitly in composer.json ("webard/nova-suneditor": "1.0.*" or an exact tag), and treat every Nova major upgrade as a review item that includes "mount this field and confirm save/load." The fallback env switch above is your insurance.

options() is your extension point and also a trap. Any SunEditor option not exposed as a fluent method goes through options([...]). That is powerful — charCounter, mode (classic/inline/balloon), plugins, pasteTagsWhitelist are all reachable — but untyped and unvalidated. A SunEditor upstream version bump can silently rename an option and you get no error, just a missing feature. Keeping all options([...]) in one subclass means an upstream bump is a single diff rather than archaeology.

Paste from Word and Google Docs will bloat your HTML. SunEditor imports inline styles liberally by default, which mangles your frontend CSS and inflates storage. Set pasteTagsWhitelist to a strict list and enforce the same list in your server-side sanitizer. Even with the client-side whitelist, do not trust it — the sanitizer is what actually matters.

Large pastes freeze the browser. Users pasting a full Word doc with embedded images generate base64 data URIs measured in megabytes, stalling the tab and potentially timing out the save request. Mitigations: charCounter on the editor, an input-length guard in JS, and — most effective — disabling inline image and routing users to a media library field or a separate upload component.

CSP breaks editor bundles in specific, confusing ways. SunEditor uses inline styles for its UI chrome. Behind a strict Content-Security-Policy, the editor often appears but renders wrong: missing toolbar backgrounds, collapsed buttons, invisible dropdowns. If the widget loads but looks broken, check style-src first — you likely need 'unsafe-inline' on the Nova admin surface, or a nonce policy that covers the editor's runtime style injection. Test against your production CSP, not just local dev.

Debugging "saved but empty" — check the database first, then the field. If content disappears between save and reload:

  1. dd(Post::find($id)->body) in Tinker. If DB has content, it is a hydration/display issue — usually ->onlyOnForms() applied to a context you actually wanted, a resolveUsing callback returning null, or a detail-view rendering choice.
  2. If DB is empty, it is a save-path issue — missing from $fillable, a fillUsing that shadows the default, or a validation rule silently rejecting the payload.

The database check takes ten seconds and immediately bisects the problem.

Character-limit the field and column together. ->rules('max:65535') at the field level without a corresponding MEDIUMTEXT column gives you a confusing mismatch where validation passes but the DB truncates. Match them, and consider setting them lower than you think — HTML overhead means 60k characters of editor input is often ~80k bytes of stored HTML.

codeView is a footgun for content trust. It lets authors paste arbitrary HTML, including tags your sanitizer will later strip and tags your renderer will not. Either drop it from the default toolbar for staff who do not need it, or accept that your server-side sanitizer is doing the actual defense work. There is no middle ground.

Forking is genuinely cheap. MIT-licensed, ~200 lines, no exotic dependencies. If a Nova upgrade breaks the field and the maintainer is unresponsive, fork to a repositories entry in composer.json and patch directly. Do not burn days working around a stale dependency whose entire source you can read in an afternoon.

Set an explicit review trigger. Revisit the package when any of these happen: upstream inactive for more than six months, a SunEditor security advisory lands, a Nova upgrade is blocked, or your content model shifts toward structured blocks instead of HTML. Because everything lands as plain HTML in a text column, swapping editors later is a field-level change — that exit ramp is the reason experimenting now is cheap and low-risk.

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.
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
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
cgv/localized-resource