#Implementing WizzyWig in Laravel (Livewire + Tailwind)
This guide shows how to add the [WizzyWig](https://www.npmjs.com/package/wizzywig) editor to a Laravel application that uses Livewire, Alpine.js, Vite, and Tailwind CSS. It covers installing the package, applying your license key, wiring the editor to Livewire form state, and rendering saved HTML safely on the front end.
**Audience:** developers with a WizzyWig license integrating the editor into their own app.
---
##What you need
| Requirement | Notes |
|-------------|--------|
| Laravel app with Vite | npm build pipeline |
| Livewire | Form components that store HTML in a public property |
| Alpine.js | Included with Livewire; used to mount and destroy the editor |
| Tailwind CSS | Optional but recommended for UI; Typography plugin for styled output |
| WizzyWig license key | Bound to your production domain |
Development hosts such as `localhost` and common `*.test` domains are exempt from license enforcement. Production hostnames require a valid key registered for that domain in your [WizzyWig / High Octane Brands license account](https://wizzy-wig.com).
---
##1. Install packages
```bash
npminstallwizzywig
```
For readable public pages (headings, lists, links), also install Tailwind Typography:
```bash
npminstall@tailwindcss/typography
```
###Styles
Import the editor stylesheet from your JS bridge (shown below), or from your CSS entry:
```js
import'wizzywig/style.css';
```
Enable Typography in Tailwind CSS v4 (`resources/css/app.css`):
```css
@import'tailwindcss';
@plugin '@tailwindcss/typography';
```
Without the Typography plugin, `prose` classes have no effect. Tailwind’s Preflight resets heading sizes and list markers, so published HTML can look like unstyled plain text.
Rebuild assets:
```bash
npmrunbuild
# development:
npmrundev
```
---
##2. Configure your license
Pass your purchased key when creating the editor. Leave the license server unset unless WizzyWig support has told you to use a custom endpoint — the package defaults to the official check-in service.
```js
newEditor({
element:mountEl,
content:'<p></p>',
toolbar: true,
theme:'light', // or 'dark'
licenseKey:'HOB-WIZZY-XXXX-XXXX-XXXX',
// licenseServer: only if instructed — omit for the default
});
```
| Option | Purpose |
|--------|---------|
| `licenseKey` | Your yearly license key |
| `licenseServer` | Optional override for the check-in URL. Omit in normal installs |
###Domain binding
Register the **exact hostname** where users load pages that include the editor (for example `app.yourcompany.com` or `www.yourcompany.com`). Local development does not need a binding.
###Laravel `.env` (recommended)
Keep the key out of source control:
```env
WIZZYWIG_LICENSE_KEY=HOB-WIZZY-XXXX-XXXX-XXXX
```
`config/wizzywig.php`:
```php
<?php
return[
'license_key'=>env('WIZZYWIG_LICENSE_KEY'),
];
```
###Product slug
Check-in requests include product slug `wysiwyg-editor` by default. You do not configure this for a standard license — it is built into the npm package.
License status is cached in the browser (`localStorage` keys prefixed with `wysiwyg-license:`). After correcting a domain binding, clear those keys or wait for the cache to expire if a banner still appears.
Licensing is **fail-open**: a temporary check-in outage does not disable editing. Fix banners before go-live so editors are clearly licensed.
---
##3. Register Alpine early (important for Livewire)
If your app uses Livewire’s `wire:navigate`, Alpine stays running while pages change. Loading the editor script only when an edit form appears (for example `@vite` inside a Blade component) often races:
1. New HTML includes `x-data="wizzywigEditor(...)"`
2. Alpine starts the component
3. The editor module has not finished loading
4. The mount stays empty until a full page refresh
**Fix:** import your bridge from the main Vite entry that already loads on every page:
`resources/js/app.js`:
```js
import'./wizzywig-editor.js';
```
Do not rely on a deferred `@vite('resources/js/wizzywig-editor.js')` solely inside the editor Blade component.
---
##4. Alpine + Livewire bridge
Create `resources/js/wizzywig-editor.js`:
```js
import { Editor } from'wizzywig';
import'wizzywig/style.css';
functionregisterWizzywigAlpine() {
if (!window.Alpine||window.__wizzywigAlpineRegistered) {
return;
}
window.__wizzywigAlpineRegistered= true;
Alpine.data('wizzywigEditor', (config= {}) => ({
editor: null,
property:config.property||'body',
licenseKey:config.licenseKey|| null,
licenseServer:config.licenseServer|| null,
minHeight:config.minHeight||'220px',
init() {
constmount=this.$refs.mount;
if (!mount) {
return;
}
mount.style.minHeight=this.minHeight;
constinitial=this.$wire.get(this.property) ??'';
this.editor=newEditor({
element:mount,
content:initial||'<p></p>',
toolbar: true,
theme:config.theme||'light',
licenseKey:this.licenseKey|| undefined,
licenseServer:this.licenseServer|| undefined,
});
this.editor.on('change', () => {
if (!this.editor) {
return;
}
// false = update Livewire state without an immediate round-trip
this.$wire.set(this.property, this.editor.getContent(), false);
});
constdestroy= () => {
if (this.editor) {
this.editor.destroy();
this.editor= null;
}
};
document.addEventListener('livewire:navigating', destroy, { once: true });
this.$el.addEventListener('alpine:destroy', destroy);
},
destroy() {
if (this.editor) {
this.editor.destroy();
this.editor= null;
}
},
}));
}
document.addEventListener('alpine:init', registerWizzywigAlpine);
if (window.Alpine) {
registerWizzywigAlpine();
}
```
###`wire:ignore`
WizzyWig owns the DOM inside the mount node. Livewire’s morphing can tear the editor down mid-edit. Put `wire:ignore` on the Alpine wrapper. Content still syncs through `$wire.set`.
###Hydrating from Livewire
`this.$wire.get(this.property)` reads the current Livewire property (`body`, `content`, `description`, …). Persist that property as a string column in your database. Validate length with Livewire rules or a Form Request as you would any other text field.
---
##5. Reusable Blade component
`resources/views/components/editors/wizzywig.blade.php`:
```blade
@props([
'property' => 'body',
'minHeight' => '220px',
'label' => null,
'theme' => 'light',
])
<div {{ $attributes->class('space-y-1') }}>
@if ($label)
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $label }}</label>
@endif
<div
wire:ignore
class="overflow-hidden rounded-lg border border-zinc-300 bg-white dark:border-zinc-700 dark:bg-zinc-900"
x-data="wizzywigEditor({
property: @js($property),
licenseKey: @js(config('wizzywig.license_key')),
minHeight: @js($minHeight),
theme: @js($theme),
})"
>
<div x-ref="mount" class="wizzywig-host"></div>
</div>
</div>
```
Use it inside a Livewire form view:
```blade
<x-editors.wizzywig property="body" label="Article body" min-height="320px" />
```
The `property` value must match the Livewire public property that stores the HTML.
Example Livewire side:
```php
publicstring$body='';
publicfunctionrules():array
{
return[
'body'=>['required','string','max:65535'],
];
}
```
---
##6. Sanitize HTML before you print it
Treat stored HTML as untrusted when rendering with `{!! !!}`. Strip scripts and event handlers at minimum.
Example helper:
```php
<?php
namespaceApp\Support;
useIlluminate\Support\Str;
finalclassSafeHtml
{
privateconst ALLOWED_TAGS ='<p><br><strong><b><em><i><u><s><strike><sub><sup><code><pre><blockquote><ul><ol><li><h1><h2><h3><h4><h5><h6><a><img><table><thead><tbody><tfoot><tr><th><td><hr><span><div><figure><figcaption>';
publicstaticfunctionclean(?string$html):string
{
if($html=== null ||trim($html)===''){
return'';
}
$cleaned=strip_tags($html,self::ALLOWED_TAGS);
$cleaned=preg_replace('/\son\w+\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i','',$cleaned)??$cleaned;
$cleaned=preg_replace('/\s(href|src)\s*=\s*([\'"])\s*javascript:[^\'"]*\2/i',' $1="#"',$cleaned)??$cleaned;
return$cleaned;
}
publicstaticfunctionplain(?string$html,int$limit=160):string
{
returnStr::limit(
trim(preg_replace('/\s+/',' ',strip_tags((string)$html))??''),
$limit
);
}
}
```
For stricter requirements, use a dedicated HTML sanitizer (for example HTML Purifier). The allowlist above covers tags WizzyWig commonly emits.
---
##7. Display content with Tailwind Typography
```blade
<article class="prose max-w-none dark:prose-invert">
{!! \App\Support\SafeHtml::clean($post->body) !!}
</article>
```
| Class | Purpose |
|--------|---------|
| `prose` | Spacing and styles for headings, lists, paragraphs, and links |
| `prose-invert` / `dark:prose-invert` | Readable on dark backgrounds |
| `max-w-none` | Use your layout’s width instead of Typography’s default measure |
WizzyWig may wrap list items as `<li><p>…</p></li>`; Typography handles that structure.
For excerpts, meta descriptions, or Open Graph text, use `SafeHtml::plain($html)` so tags are removed.
---
##8. Implementation checklist
1.`npm install wizzywig` (and `@tailwindcss/typography` if you use `prose`)
2. Import the Alpine bridge from `resources/js/app.js`
3. Put your license key in `.env` / config; bind the production hostname to that key
4. Add the Blade component with `wire:ignore` and `x-ref="mount"`
5. Store HTML on a Livewire string property; validate and persist it
6. Sanitize with `SafeHtml::clean` (or stronger) before `{!! !!}`
7. Wrap published HTML in `prose`
8. With `wire:navigate`, soft-open an edit page once and confirm the toolbar appears without a hard refresh
---
##9. Troubleshooting
| Symptom | Likely cause | What to do |
|---------|--------------|------------|
| Empty editor on first soft navigation; hard refresh works | Alpine component registered after Livewire painted the page | Import the bridge from `app.js`; avoid deferred-only Vite tags on the form |
| Unlicensed banner on production | Key missing, wrong key, or domain not bound | Set `WIZZYWIG_LICENSE_KEY`; bind the live hostname; clear `wysiwyg-license:*` in `localStorage` |
| Banner mentions wizzy-wig.com | Notice UI from the package (docs link), or check-in failed | Inspect Network for the license check response; fix key/domain rather than renaming products |
| Published page looks like a wall of text | Typography plugin not installed or not enabled | Install `@tailwindcss/typography` and add `@plugin` |
| Editor UI disappears after a Livewire update | Missing `wire:ignore` | Add `wire:ignore` on the editor wrapper |
| Works on localhost, fails in production | Production domain not on the license | Add the production hostname in your license account |
| Alpine error: unknown `wizzywigEditor` | Bridge not in the built bundle | Confirm `import './wizzywig-editor.js'` and redeploy `npm run build` |
---
##10. Migrating from TinyMCE
WizzyWig supports familiar APIs such as `getContent`, `setContent`, `insertContent`, change events, and toolbar configuration. Keep the same HTML database columns; swap the TinyMCE init for the Alpine bridge above; keep sanitization and `prose` rendering on the front end.
For non-Vite pages you can load WizzyWig from the CDN with a license-enabled script URL. In Livewire apps, npm + Vite + early Alpine registration remains the most reliable approach with `wire:navigate`.
---
##Suggested file layout
```text
config/wizzywig.php
resources/js/app.js
resources/js/wizzywig-editor.js
resources/views/components/editors/wizzywig.blade.php
app/Support/SafeHtml.php
resources/css/app.css
```
---
##Further reading
- [wizzywig on npm](https://www.npmjs.com/package/wizzywig)
- [WizzyWig product site](https://wizzy-wig.com)
- [Livewire navigate](https://livewire.laravel.com/docs/navigate)
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography)