charlielangridge/filament-mail-previewer
A Filament plugin for previewing Laravel mailables right in your admin panel. Browse available mail classes, render each one with sample data, and inspect the HTML and plain-text output before sending—so no more firing off test emails just to see how they look.
Baseline: v0.2.1 (adds Laravel 13 support, fixes config loading).
composer require charlielangridge/filament-mail-previewer
php artisan vendor:publish --tag="mail-previewer-config"
This is the file at config/mail-previewer.php. In v0.2.1 the package now loads it under the correct key — if you had previously worked around a broken lookup, re-check the published filename and the top-level config key before assuming your old edits still apply.PanelProvider:
public function panel(Panel $panel): Panel
{
return $panel
->plugin(FilamentMailPreviewerPlugin::make());
}
php artisan optimize:clear, then log in and look for the Mail Previewer entry in the panel navigation.Where to look first: the published config file. It is the single source of truth for which mailables show up and how they are grouped. Read it before reading the source — it tells you the expected shape of a registration entry.
First use case: you have a transactional email (order confirmation, password reset, invoice) that is painful to iterate on because it requires seeding data and triggering a real send. Register that one mailable, give it representative constructor arguments, and use the page as your rendering loop instead of Mail::fake() + assertSeeInHtml() gymnastics.
Register mailables declaratively in config. The day-to-day pattern is: add an entry per mailable, with the constructor arguments needed to instantiate it. Anything requiring a model (an Order, a User) should be resolved from a factory or a known seed record so the preview is reproducible across machines.
// config/mail-previewer.php
'mailables' => [
App\Mail\OrderConfirmation::class => [
'label' => 'Order confirmation',
'arguments' => [/* resolved model or closure */],
],
],
If the config supports closures for arguments, prefer them over hard-coded IDs — closures let you resolve "the most recent order" at render time and avoid stale fixtures after a migrate:fresh.
Gate it to non-production. This is the most important integration decision. A page that renders arbitrary mailables with real model data is a data-exposure surface. Put the plugin registration behind an environment check in the panel provider, or restrict the page via canAccess() / a policy. Do not rely on "nobody knows the URL."
->plugin(FilamentMailPreviewerPlugin::make())
// guarded by: app()->isLocal() || app()->environment('staging')
Use it as a QA surface, not just a dev toy. Because the page renders real Blade output, it doubles as a visual regression target: check your mailables at both desktop and mobile preview widths, and check plain-text variants if the package exposes them. Wire it into your pre-release checklist for any change touching resources/views/vendor/mail or a published mail theme.
Multi-panel setups. If you run an admin panel and a separate staff/app panel, register the plugin only on the panel that should own it. Registering on multiple panels duplicates nav entries and can produce route-name collisions depending on how the page slug is registered — pick one.
Testing your own integration. v0.2.1 explicitly adds tests for package boot and plugin/page registration, which is a good signal that registration is meant to be assertable. Mirror that: in your app's test suite, assert the panel boots with the plugin and the page is reachable for an authorised user, rather than asserting on rendered HTML (which is brittle).
Version pinning. Since v0.2.1 is only the second patch on a 0.2.x line, pin with a caret to the minor (^0.2) and read the changelog on each bump. Pre-1.0 packages are entitled to change config shape between minors.
Config key rename — read this first if upgrading from v0.2.0. v0.2.1 "fixes package config registration so config/mail-previewer.php is loaded correctly, while keeping a fallback for the previous config key lookup." Practically: if you published config under v0.2.0 and it seemed to be ignored, the bug was real and your config may now suddenly take effect. After upgrading, diff your published config against the package's current stub and confirm which top-level key is authoritative — the fallback means a stale duplicate key won't error, it will silently shadow or be shadowed depending on precedence. Delete the dead key rather than leaving both.
Cached config will mask the fix. If you run config:cache in local/staging, a config-loading change can appear not to work until you php artisan config:clear. Always clear before concluding the upgrade failed.
Mailables with required constructor arguments are the main friction point. A mailable that takes a non-nullable model, or that computes data in its constructor from a request-bound service, will fail to instantiate in the preview context. Two fixes: register explicit arguments in config, or refactor the mailable to be constructible from a model identifier. Prefer the latter — it makes the mailable testable independently of this package.
Request-dependent mailables. Anything reaching for request(), the authenticated user, or session state during build() will render incorrectly or throw in the previewer. Pass everything in explicitly. This is a good forcing function for mailable hygiene generally.
Laravel 13 support is new in this release. The compatibility range and CI matrix were expanded in v0.2.1, but "CI passes" is not the same as "your app's panel renders it." If you're on Laravel 13, smoke-test the page yourself after install — particularly the navigation registration and route generation, which are the parts most sensitive to framework major bumps.
Queued/ShouldQueue mailables may not behave as expected in a synchronous preview path. If previews come back blank, check whether the mailable defers work to afterCommit or a queued job.
Extension points. Before writing custom rendering, check whether the config exposes label/group/ordering options and whether the page class can be subclassed for custom actions (e.g. "send a test copy to me"). Overriding the page is usually cleaner than forking the package, and it survives patch upgrades.
Debugging a blank preview. Work outward: (1) can the mailable be instantiated with new and your config arguments in tinker? (2) does ->render() produce HTML there? (3) only then look at the package. Steps 1–2 isolate whether the problem is your mailable or the plugin.
Watch the repo for the missing changelog bullet. The v0.2.1 notes contain an empty list item after the config fix, suggesting an omitted entry. Check the full changelog link before upgrading in a production-adjacent environment.
How can I help you explore Laravel packages today?