idf/nova-unlayer-field
A Laravel Nova field that integrates the Unlayer drag-and-drop email builder, letting you design, edit, and save responsive email templates directly inside Nova resources. Ideal for managing newsletters and transactional emails from your admin panel with minimal setup.
Install the field and give it somewhere to write:
composer require idf/nova-unlayer-field
Then add the storage columns. A working default is two columns per builder — one for the editable design, one for rendered output:
Schema::table('emails', function (Blueprint $table) {
$table->json('design')->nullable(); // Unlayer design JSON, for re-editing
$table->longText('html')->nullable(); // exported HTML, for sending
});
Register the field on your Nova resource:
use IdF\NovaUnlayerField\NovaUnlayerField;
public function fields(Request $request)
{
return [
NovaUnlayerField::make('Design', 'design')
->displayMode('email') // 'email' | 'web' | 'popup' | 'document'
->hideFromIndex(),
];
}
Where to look first, in order:
vendor/idf/nova-unlayer-field/src/ — the field class. Confirm the exact fluent method names (displayMode, mergeTags, withUnlayerOptions, etc.) and which attribute the design JSON is actually posted to; the README is thin and the source is the contract.resources/js/components/ — the Vue component that loads https://editor.unlayer.com/embed.js. This tells you which Unlayer init() options the field exposes versus which you must pass through as raw JSON.composer.json / nova constraint — check it supports your Nova major (4 vs 5) before anything else, since the Vue asset pipeline differs.First use case to prove out: replace a <textarea> of raw HTML on an Email resource with the editor, save the design JSON, and render the exported HTML into a Mailable via $this->html. Get that round-trip (open → edit → save → send → re-open and see your edits) working before adding merge tags or multiple builder modes.
Persist both representations, render from one. Treat the design JSON as the source of truth and the HTML as a derived artifact. Regenerate HTML on save rather than trusting a stale column:
public function fields(Request $request)
{
return [
NovaUnlayerField::make('Design', 'design')
->displayMode('email')
->mergeTags(['first_name', 'company'])
->hideFromIndex(),
// Keep the rendered output visible but read-only in Nova
Textarea::make('HTML', 'html')->readonly()->onlyOnDetail()->rows(15),
];
}
If the field emits an exported-HTML payload alongside the design, use ->fillUsing() or an Email::saving() hook to copy it across instead of duplicating the editor.
Send from the rendered HTML. In the Mailable, bypass Blade templating and hand the string straight to the mailer:
public function build()
{
return $this->html($this->email->html)
->subject(Str::of($this->email->subject)->replaceMatches('/{{\s*(\w+)\s*}}/', ...));
}
Two builders on one resource. Landing pages and transactional emails have different Unlayer displayMode values and different available blocks. Model them as separate fields and separate columns rather than a mode column, because the JSON shape diverges:
NovaUnlayerField::make('Page Design', 'page_design')->displayMode('web'),
NovaUnlayerField::make('Email Design', 'email_design')->displayMode('email'),
Merge tags are a two-sided contract. Defining ->mergeTags([...]) in Nova only controls the insert menu inside Unlayer; your renderer must substitute the same tokens at send time. Keep the token list in one place — a config array imported by both the Nova resource and the mailable — or they will drift silently.
Draft/publish workflow. Because the design is just JSON, cheap versioning falls out naturally: snapshot design + html into an email_versions table on publish, and let editors keep working against the live row. No editor-side support needed.
Pair with a preview route. Nova can't render the editor on index/detail, so add a signed admin route that returns the stored html for a quick "view as sent" check. This becomes the fastest debugging tool you have.
Conditional exposure. Use ->onlyOnForms() on the editor and ->onlyOnDetail() on the HTML/preview field. Rendering the Unlayer iframe on the index page for 50 rows will hammer the Nova admin.
editor.unlayer.com. In locked-down networks, behind a strict CSP, or in an air-gapped staging environment, the field renders blank with no obvious error. Check the browser console first; if it's CSP, you need script-src/frame-src allowances for *.unlayer.com.max_allowed_packet and can exceed TEXT (64 KB). Use JSON / longText, and prefer image URLs over uploads — Unlayer's uploader may inline data URIs into the design.php artisan vendor:publish --provider="IdF\NovaUnlayerField\...") and npm run dev on the Nova side.hideFromIndex() is not optional. The iframe is heavy and will noticeably degrade long index tables. Same for preview() values that try to strip_tags a JSON blob.->rules('json') the design column unless you've confirmed the field posts a string rather than an already-decoded array — Nova's casting and the field's serialization can disagree, producing validation failures that only show up on certain Unlayer block types.init() config, not in the field's fluent API. Look for a pass-through method accepting an array, and send it via ->withUnlayerOptions([...]) or the equivalent — this is where you'll spend most customization time.html column and consider a nightly export so you can always send mail even if the editor integration is unavailable.How can I help you explore Laravel packages today?