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

Lilac Laravel Package

lilac-wysiwyg/lilac

Lilac is a modern WYSIWYG editor for Laravel applications. It offers a simple Blade component, clean HTML output, image upload support, and configurable toolbars, making it easy to add rich text editing to your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Lilac is a small, single-purpose package: a WYSIWYG editor written in Qwik, shipped as a prebuilt JS/CSS bundle (the UMD build at dist/lilac.umd.js, exposing a window.Lilac global) plus a thin Composer/Laravel wrapper so you can drop a rich-text field into an existing form without wiring up a JS build step yourself. Treat it as a UI component, not a CMS — you still own persistence, sanitization, and rendering.

Minimal path:

  1. Install, and pin to 0.7.1 or later.

    composer require lilac-wysiwyg/lilac:0.7.1
    

    0.7.0 is broken — it shipped without the UMD bundle, so the editor simply never appears. If your lockfile already has 0.7.0, run composer update lilac-wysiwyg/lilac before you debug anything else. Because the package is pre-1.0 with 15 stars and no dependents, pin explicitly and read the changelog before every bump.

  2. Confirm the bundle is actually on disk. This is the one check worth doing immediately after install, because it's exactly what regressed in 0.7.0:

    ls -l vendor/lilac-wysiwyg/lilac/dist/
    # expect lilac.umd.js (+ css); if empty, you have the wrong version
    
  3. Publish what it gives you.

    php artisan vendor:publish            # lists the real tags — do this first
    php artisan vendor:publish --tag=lilac-config
    php artisan vendor:publish --tag=lilac-assets
    
  4. Look here first, in this order:

    • vendor/lilac-wysiwyg/lilac/README.md — the only authoritative API reference for a package this size.
    • vendor/lilac-wysiwyg/lilac/dist/ — the UMD file name and CSS. If you're loading it manually, this is the exact path you reference.
    • vendor/lilac-wysiwyg/lilac/resources/views/ — the Blade component/directive names and props.
    • vendor/lilac-wysiwyg/lilac/config/lilac.php — toolbar, height, upload disk, asset source (CDN vs published).
    • vendor/lilac-wysiwyg/lilac/src/ — usually 2–4 classes; skim them so you know what's actually happening.
    • The Qwik live playground linked in the release notes — it's the fastest way to see the current window.Lilac API and toolbar behaviour without grepping minified JS.
  5. First use case — swap a <textarea> in an admin CRUD. The editor enhances an existing textarea rather than replacing your form handling, so the diff is tiny:

    {{-- before --}}
    <textarea name="body" class="form-control">{{ old('body', $post->body) }}</textarea>
    
    {{-- after --}}
    <x-lilac::editor name="body" :value="old('body', $post->body)" />
    

    Confirm the exact component name from the published views — the Blade side is thin and has historically been the least stable part.

  6. Include the assets once, in your layout, not per-view:

    @lilacStyles
    ...
    @lilacScripts
    

    If no directive is registered, load the UMD build yourself and init explicitly:

    <script src="{{ asset('vendor/lilac/lilac.umd.js') }}"></script>
    <script>window.Lilac?.init(document.querySelectorAll('[data-lilac]'));</script>
    
  7. Prove the round trip with one request: save, redirect, re-open the edit form with the stored HTML, confirm the toolbar reflects formatting (bold stays bold, lists stay lists). If that works, the integration is done.

Implementation Patterns

Wrap it in your own Blade component. Don't scatter <x-lilac::editor> through 30 views. Make one resources/views/components/rich-text.blade.php that owns the label, error state, help text, and Lilac invocation. When the package's API shifts (likely — it's 0.7.x and the 0.7.0 → 0.7.1 patch was a packaging bug, not a feature), you edit one file.

{{-- resources/views/components/rich-text.blade.php --}}
@props(['name', 'label' => null, 'value' => '', 'height' => 320])

<div class="mb-4">
    @if($label)<label for="{{ $name }}" class="block font-medium">{{ $label }}</label>@endif
    <x-lilac::editor :name="$name" :value="old($name, $value)" :height="$height" />
    @error($name)<p class="text-red-600 text-sm">{{ $message }}</p>@enderror
</div>

Initialise through one JS entry point. Because the public surface is the window.Lilac UMD global rather than a PHP class, keep all init in a single inline script or Alpine component and put it behind a helper:

window.mountLilac = (root = document) => {
    root.querySelectorAll('[data-lilac]').forEach(el => {
        if (el.dataset.lilacReady) return;      // guard against double init
        el.dataset.lilacReady = '1';
        window.Lilac?.init(el);
    });
};
document.addEventListener('DOMContentLoaded', () => window.mountLilac());

When you move to a Livewire/Turbo page-swap world you re-call mountLilac() from the navigation event and never touch the package internals.

Normalize on the way in, sanitize on the way out. Two separate concerns, two separate places:

  • In: strip the "empty editor" sentinel. Most WYSIWYGs emit <p><br></p> or <p></p> when the user types nothing, which defeats required validation and pollutes your DB with junk. Normalize in a form request's prepareForValidation():
    protected function prepareForValidation(): void
    {
        $this->merge([
            'body' => trim(strip_tags($this->body ?? '', '<img><iframe>')) === ''
                ? null
                : $this->body,
        ]);
    }
    
  • Out: never {!! $post->body !!} raw. Run it through HTMLPurifier (or mews/purifier) with an explicit allowlist at render time, or sanitize once on save and store the clean version. Sanitizing on save is cheaper; sanitizing on render is safer against config drift. Pick one and be consistent.

Validation rule for a rich-text field:

'body' => ['nullable', 'string', 'max:65000', new SanitizedHtml],

Keep the max generous (HTML is verbose) but bounded — without it a paste of a whole document becomes a multi-megabyte row and a slow page.

Livewire / Alpine integration. Livewire's DOM morphing will fight a JS-initialized editor. The reliable pattern:

<div wire:ignore>
    <x-lilac::editor name="body" :value="$body" wire:model.defer="body" />
</div>

wire:ignore prevents morphing from destroying the editor; you then sync the editor's HTML back to the component with an explicit $wire.set('body', html) on the editor's change/input event, or by reading the underlying textarea on submit. Don't rely on wire:model alone to capture keystrokes from a contenteditable. Then call mountLilac() inside a livewire:navigated listener, because a freshly morphed DOM has an uninitialised editor again.

Image uploads. If Lilac supports inline images, point it at a dedicated disk and a controller action rather than letting it base64-embed:

  • Config: upload disk = public (or S3), path = uploads/posts/{id}.
  • Return JSON in whatever shape the editor expects — read the upload handler or the playground network tab for the exact key.
  • Run php artisan storage:link or images 404 silently while the editor looks broken.
  • Resize/compress on upload (intervention/image); a 6 MB phone photo in a blog post is the single most common complaint after this integration.
  • Validate server-side: image|mimes:jpeg,png,webp|max:5120. The editor's client-side checks are cosmetic.

Config-driven toolbar presets. Publish the config and define two profiles — basic (bold/italic/link/lists) for comments or short bios, full (headings, images, tables, code) for long-form. Select per field via a component prop so you're not maintaining parallel editor builds.

Vendor the UMD build rather than trusting npm/CDN parity. The prepublishOnly hook that packs dist/lilac.umd.js is an npm lifecycle script; it says nothing about what a Composer archive or a CDN mirror contains. If you load from a CDN, verify the file at that exact URL for your version — or, simpler, publish assets locally and reference asset('vendor/lilac/lilac.umd.js').

Rendering outside the browser. For excerpts, RSS, og:description, or search indexes, derive a plain-text version on save and store it in a sibling column (body_excerpt). Regex-stripping tags at render time produces &nbsp; soup and kills list/heading context. One Str::limit(strip_tags($html), 160) in a model observer is enough.

Feature tests. Don't assert on editor markup — it's JS-generated, Qwik-hydrated, and version-dependent. Assert the contract: $response->assertSee('name="body"', false), post HTML directly to the endpoint, and assert the stored, sanitized value. Add one browser test (Dusk/Pest browser) that types into the editor and submits, because that's the part PHP tests can't see.

Gotchas and Tips

  • 0.7.0 shipped without dist/lilac.umd.js — the editor does not exist on that version. Symptom is a blank field or a 404 on the script, with no PHP-side error at all. First move after any bump: ls vendor/lilac-wysiwyg/lilac/dist/. 0.7.1 moves bundle generation into prepublishOnly, after the TypeScript build, so the artifact exists at pack time — but that only guarantees the npm tarball. Verify the Composer-installed copy, not the release page.
  • The public API is a JS global, not a PHP facade. window.Lilac is what you integrate against; the Laravel package is a delivery mechanism for views, config, and assets. Read dist/ and the playground before assuming a Blade prop exists.
  • Qwik components mount asynchronously. DOMContentLoaded can fire before the editor is interactive, and a chunked lazy load can beat your init call. Guard your init with an existence check (window.Lilac?.init) and, if you see a flash of plain textarea, init from the library's own ready hook rather than from DOMContentLoaded.
  • Check for a separate Qwik loader. Qwik apps normally bootstrap via a small loader script that resumes the serialized component tree. If the UMD build doesn't bundle it, you'll get a 404 for qwikloader.js-style assets and a permanently inert editor. Confirm with one look at the network tab on a bare Blade page.
  • The package is young and thin. 15 stars, zero dependents, 0.7.x. There is no ecosystem of Stack Overflow answers, and a patch release just broke the core artifact — that's your calibration for how much to trust defaults. Budget an hour to read src/ and dist/. It will be short, and it's worth more than guessing at directives.
  • XSS is entirely your problem. The editor runs in the browser; anyone can POST <script> to your endpoint with curl. A WYSIWYG package does not sanitize for you. HTMLPurifier with a narrow allowlist is non-negotiable the moment non-admin users can post.
  • Tailwind Preflight eats editor content. @tailwind base resets h1–h6, ul, ol, and blockquote to unstyled. Either scope the editor's CSS (.lilac-content h1 { ... }) or wrap the rendered output in a container with explicit typography rules. Symptom: "the editor looks right but the published page has no bullets or heading sizes."
  • CSS/JS load order. Editor JS initialised before the DOM element exists (script in <head>, or assets emitted before the field) fails silently with a console error you'll only notice in devtools. Put scripts at the end of body, or wrap init in DOMContentLoaded.
  • Turbo / Livewire Navigate / SPA-style page swaps cause double init or dead editors. On each page swap the editor must be re-initialised and the old instance destroyed. Use the mountLilac() helper above from a turbo:load / livewire:navigated listener rather than hoping the package handles it.
  • Published assets vs CDN. If config defaults to a CDN, you've introduced an external dependency and a privacy/GDPR question, and it breaks offline dev — and a CDN can serve a build that doesn't match your Composer version. Publish locally.
  • Asset publishing is easily forgotten in deploy. Add the publish step to your deploy script or commit the published files, otherwise a fresh server 404s on the UMD bundle while your local machine works fine — indistinguishable from the 0.7.0 bug.
  • <p><br></p> is not empty. Covered above, but it bites twice: required validation passes on a blank editor, and {{ $post->body ? 'has content' : 'empty' }} always says "has content."
  • Debugging order when the editor doesn't appear: (0) is dist/lilac.umd.js present on disk for your installed version? (1) is the script in the page source and returning 200? (2) is there exactly one element matching the selector? (3) console errors / loader 404s? (4) does it work on a bare Blade view outside your layout? That last step isolates CSS/JS conflicts in about two minutes.
  • Extension points are limited — plan accordingly. Expect config (toolbar, height, uploads) plus window.Lilac.init as the main knobs, with no plugin API. If you need custom buttons, custom paste handling, or a specific HTML schema, fork or subclass rather than patching vendor/. Wrap the editor in your own Blade component now so a future swap to TipTap/Trix/Editor.js is a one-file change instead of a grep-and-replace.
  • Pin the version and watch releases. With no dependents, you are the QA. Pin the exact 0.7.1 for now, commit the lockfile, and read the diff before bumping — a packaging regression in a patch release is exactly the failure mode a ^0.7 constraint will not protect you from.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package