Email Layouts
Database-managed email chrome — the branded wrapper around every system email, its slot dialect, editor, and version history.
Overview
An email layout is the chrome wrapped around rendered email content: the document shell, styles, header, body slot, and footer. Layouts live in the database (email_layouts), so the branded wrapper is editable from the admin panel instead of being a fixed Blade view.
The shipped Signals layout is seeded as a system layout and marked as the default, so a fresh install behaves exactly as before.
Where it lives
Routes:
- List:
/admin/settings/email-layouts(admin.settings.email-layouts) - Edit:
/admin/settings/email-layouts/{layout}/edit(admin.settings.email-layouts.edit)
Open Admin → Preferences → Email Layouts. Both pages require email-templates.manage, on mount and on every action.
Key workflows
| Action | Behaviour |
|---|---|
| New Layout | Creates a layout seeded with the master markup; the key is slugified from the name and suffixed until unique |
| Duplicate | Same, but pre-filled with the source layout's HTML and named Copy of … |
| Edit | CodeMirror editor with live preview, slot/field browser, and version history |
| Make Default | Promotes one layout to the single default in a transaction, clearing the flag on every other row |
| Active toggle | Deactivates a layout so it can no longer be picked; the default layout cannot be deactivated |
| Delete | Removes a layout — system and default layouts are protected |
The first layout created on an install with no default becomes the default automatically.
Resolution order
App\Services\Notifications\EmailLayoutRenderer picks a layout in this order:
- the layout chosen on the email template (
email_templates.email_layout_id), when it is still active; - the active default layout;
- a hard fallback to the static Blade view
resources/views/emails/layouts/signals.blade.php.
Transactional mail must never fail because a layout was edited, deactivated, or deleted, so every database path is wrapped: a missing table, a query failure, or a render error logs a warning and degrades to the static view. Keep the Blade view visually in step with the seeded master layout — the seeder is a faithful conversion of it.
The default layout is cached under email-layouts:default for one hour and the cache is dropped by every layout write (create, update, delete, set-default) and by the seeder.
Layout dialect
A layout body is plain HTML plus three placeholder forms. Styles are inlined onto each element after rendering, so <style> rules survive email clients.
Raw slots — {{{ slot }}}
Inserted unescaped, because they carry rendered HTML:
| Slot | Contents |
|---|---|
{{{ body }}} |
Required — the rendered email template content |
{{{ eyebrow }}} |
Short label above the heading |
{{{ preheader }}} |
Hidden inbox preview text |
{{{ footer_context }}} |
Contextual footer line |
{{{ unsubscribe_url }}} |
Unsubscribe link for customer communications |
An unwrapped slot with no value renders as an empty string.
Conditional blocks — {{{ if slot }}} … {{{ endif }}}
Removed entirely when the named slot is empty, so optional chrome does not leave empty padded elements behind:
{{{ if eyebrow }}}
<p class="eyebrow">{{{ eyebrow }}}</p>
{{{ endif }}}
Merge fields — {{ field.path }}
Escaped values resolved from the branding data set, sharing the dot-notation grammar and | upper / | lower / | default:"x" filters used by email templates:
| Field | Value |
|---|---|
{{ company.name }} |
company.name setting |
{{ current_year }} |
Current year, for the footer |
{{ eyebrow }} / {{ preheader }} / {{ footer_context }} |
Escaped forms of the same values |
{{ unsubscribe_url }} |
Unsubscribe URL |
Editor
The edit page splits into a browser column and an editor/preview pair:
- Slots and Branding Fields panels insert a snippet at the cursor; when CodeMirror is unavailable the page falls back to a plain textarea and inserts server-side.
- Layout HTML — CodeMirror 6 source editor.
- Live Preview — the current buffer rendered with sample body copy, eyebrow, preheader, footer context, and unsubscribe URL in a sandboxed iframe. A dialect error is reported in the preview pane rather than thrown.
- Versions — every save first snapshots the previous body into
email_layout_versions(append-only, numbered, attributed to the saving user). Restore loads an earlier body back into the editor and saves it, which itself snapshots the current one. - Make Default and the Active checkbox apply the same guards as the list page.
Choosing a layout per template
The email template form carries an Email Layout picker listing every active layout, with the first option — Default — {layout name} — leaving email_layout_id null so the template follows whichever layout is currently the default. The template preview renders through the selected layout. See Email Templates.
Seeding
EmailLayoutSeeder creates the master Signals layout (is_system, default when no default exists yet) and is idempotent — re-running refreshes the name, description, and system flag without overwriting an edited body. It runs from CompleteSetup and is listed on the Database Seeders panel.