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

Email Templates Laravel Package

visualbuilder/email-templates

Laravel package for database-backed email templates with a visual editor. Build reusable, variable-driven transactional emails, preview and manage them from your app, and send through Laravel Mail—no code changes or redeploys needed when content changes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require visualbuilder/email-templates
php artisan vendor:publish --tag="email-templates-migrations"
php artisan migrate

The package is a Filament plugin, so registration happens in your panel provider rather than a service provider:

use Visualbuilder\EmailTemplates\EmailTemplatesPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(EmailTemplatesPlugin::make());
}

If the plugin exposes config or views, publish those too (--tag="email-templates-config", --tag="email-templates-views") — check vendor/visualbuilder/email-templates/ for the exact tag names, since this is a small package and tags are not always documented.

Where to look first

  1. vendor/visualbuilder/email-templates/src/ — find the Filament Page/Resource class; that's the editor UI entry point.
  2. config/email-templates.php (after publishing) — layout, available templates, and any variable/placeholder definitions live here.
  3. The migration — confirms the table name (commonly email_templates) and the columns you can query against (key, subject, html, language, etc.).

First use case: transactional email with an editable body

Create a template in the Filament admin (/admin → Email Templates), give it a stable key like order-shipped, drop in your blocks, and add placeholders for dynamic data (e.g. {{ $order->number }} or {{ customer_name }} depending on the syntax the package uses). Then render it at send time from a Mailable or Notification rather than hard-coding the view.

The realistic day-one win: marketing/ops can tweak copy and layout without a deploy, while your code only owns the key and the data contract.

Implementation Patterns

Keep the key contract in code, the markup in the DB

Treat template keys as part of your application's API surface. Seed them in a seeder so fresh environments and tests have deterministic templates:

// database/seeders/EmailTemplateSeeder.php
EmailTemplate::updateOrCreate(
    ['key' => 'order-shipped'],
    [
        'subject' => 'Your order {{ order_number }} has shipped',
        'html'    => view('emails.defaults.order-shipped')->render(),
    ]
);

Seeding the body (rather than leaving it blank) means local/staging environments can actually send and preview mail, and new templates ship with sensible defaults that editors can then customize.

Send via a Mailable that resolves the template at runtime

The pattern that holds up best is a thin "template-backed" Mailable base class, so business code never touches markup:

class TemplateMailable extends Mailable
{
    public function __construct(
        public string $templateKey,
        public array $data = [],
    ) {}

    public function build()
    {
        $template = EmailTemplate::where('key', $this->templateKey)->firstOrFail();

        return $this
            ->subject($this->interpolate($template->subject))
            ->html($this->interpolate($template->html));
    }
}

Then call sites stay stable: Mail::to($user)->send(new TemplateMailable('order-shipped', $orderData));

Decide interpolation once and centralize it

If the package uses Blade under the hood, Blade::render($template->html, $data) is tempting but risky — it evaluates arbitrary PHP from editor-authored content. Prefer a strict token replacer ({{ key }} → escaped value, missing keys logged) unless you fully trust every editor. Whatever you pick, put it in one TemplateRenderer service and use it for the subject line, HTML body, and plain-text fallback so they can't drift.

Preview as part of the editor workflow

Wire a "Send test to me" action into the Filament page (or add it as a custom action if the package doesn't ship one). Editors will not trust a visual builder they can't email to themselves; a preview modal rendering with dummy data is the single highest-value integration you can add. Render the same HTML the send path produces — don't build a second renderer for preview.

Store both source and compiled output

Keep the builder's JSON/structured source and the compiled HTML. Compiled HTML is what you send (fast, cacheable, diffable in logs); source is what the editor needs to reopen the template. If the package's schema only has html, add a nullable source JSON column and treat the package's field as the compiled artifact.

Versioning and rollback

Editors will break a live template. Two cheap options: (a) a versions table written on every save via a model observer, or (b) a published_at flag with drafts — the send path queries whereNotNull('published_at'). Option (b) is usually enough and avoids migration churn on the package's own table.

Multi-locale

If you send in more than one language, key templates by (key, locale) rather than duplicating layout blocks per language. Resolve with ->where('key', $k)->where('locale', app()->getLocale())->first() ?? <fallback locale>. Fallback matters more than you think — a missing translation should never mean a failed send.

Pair with an inliner at the boundary

Whatever the builder outputs, run it through tijsverkoyen/css-to-inline-styles (or similar) immediately before handing it to html(). Do this in the renderer, not the editor — editors don't need to see inlined markup, and inlining at send time means CSS changes in the template take effect without a recompile step.

Gotchas and Tips

GPL-3.0 is a real constraint, not a footnote. The repository is licensed GPL-3.0, not MIT. If you're shipping a closed-source SaaS, get a lawyer's read before bundling this into your product. For internal admin tooling it's usually fine; for redistribution it is not. This alone disqualifies the package for some teams — decide early, not after you've built on it.

Near-zero ecosystem footprint. With no dependents and modest stars, expect: undocumented edge cases, no Stack Overflow answers, and breakage on Filament major upgrades. Budget for reading the source. Pin the version in composer.json and don't composer update blindly — a Filament 4 upgrade could leave you with a broken admin page and no upstream patch.

Filament version compatibility. The plugin must match your Filament major (and often minor) version. If the panel loads but the page 500s, check composer show visualbuilder/email-templates against your filament/filament constraint first. A mismatched filament/support is the most common silent failure.

Asset publishing is easy to miss. Visual builders typically ship JS/CSS. If the editor renders blank or unstyled, you almost certainly haven't published assets — re-run vendor:publish and check public/vendor/visualbuilder/. Also check whether the package expects a build step or ships prebuilt bundles; if it requires npm run build inside the vendor directory, that's a maintenance liability worth noting.

User-authored HTML is a sanitization problem. Anyone with panel access can inject scripts or tracking pixels, and the output goes into real inboxes. Sanitize on save (strip <script>, on* attributes, javascript: URLs) or at minimum restrict the editor role. Also strip the same content before logging rendered HTML anywhere.

Blade double-render. If the package compiles templates with Blade at save time and you interpolate again at send time, {{ }} inside user text will be evaluated twice — or silently swallowed. Pick one stage. Symptom to watch for: a variable that renders correctly in preview but appears blank in the delivered email (or vice versa).

Test with a real client, not just the preview. Filament previews render in a browser iframe with modern CSS. Gmail and Outlook do not. Ship one test through Mailpit/Mailtrap and eyeball it in Outlook — column layouts and background images are where builder output usually falls apart.

Cache the lookup, invalidate on save. Templates are read on every send. Cache::rememberForever("email-template:{$key}:{$locale}") with a saved/deleted model observer busting the key avoids a query per email during bulk sends. Don't cache the compiled-with-data result — only the template record.

Missing variables should fail loudly in dev, quietly in prod. A template edited to reference {{ tracking_url }} that your code doesn't pass will render as blank or literal braces. Log a warning with the template key and missing field in non-production; in production, fall back to an empty string so the email still sends. Never let a template typo throw inside a queued job — you'll lose the notification entirely.

Extension point: the send path is yours, keep it yours. The package owns the editor; don't let it own delivery. Wrapping it behind your own TemplateRenderer/TemplateMailable means you can swap the plugin out for a hand-rolled CRUD page later without touching a single call site — which, given the licensing and maintenance profile here, is a sensible hedge.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core