Layout Contract
Every Opterius Mail template must provide a file at layouts/app.blade.php. This file is the application shell — it wraps every authenticated webmail page. To function correctly, the layout must implement a specific set of Blade directives and have access to specific PHP variables. Failing to implement any of the required directives will cause broken pages, missing content, or JavaScript errors.
Required Blade Directives
All three of the following must appear in layouts/app.blade.php:
| Directive | Where to place it | What it does |
|---|---|---|
@yield('title') |
Inside the <title> tag |
Outputs the page-specific title set by each controller |
@yield('content') |
In the main content area | Outputs the rendered content view for the current page |
@stack('scripts') |
Before </body> |
Outputs page-specific JavaScript pushed by individual views |
Correct Usage Example
<head>
<title>@yield('title', 'Webmail') — My Company</title>
</head>
<body>
{{-- navigation, sidebar, etc. --}}
<main>
@yield('content')
</main>
@stack('scripts')
</body>
What Happens If a Directive Is Missing
| Missing directive | Consequence |
|---|---|
@yield('title') |
All pages show a blank or hardcoded page title — minor but unprofessional |
@yield('content') |
All content views are silently discarded — every page appears blank |
@stack('scripts') |
Page-specific JavaScript (compose editor, message actions, 2FA timer, etc.) is not loaded — core features stop working |
PHP Variables Available in Every Authenticated Layout
These variables are injected by the InjectMailLayoutData middleware and are available in layouts/app.blade.php and every content view rendered inside it.
| Variable | Type | Description |
|---|---|---|
$folders |
array |
List of IMAP folders for the authenticated user's mailbox |
$currentFolder |
string |
The IMAP folder name currently being viewed |
$folders Structure
Each element in $folders is an associative array:
[
'name' => 'INBOX', // IMAP folder name (use in URLs)
'display' => 'Inbox', // Human-friendly label
'unseen' => 3, // Count of unread messages
'total' => 47, // Total message count
'attributes' => ['\\HasNoChildren'], // IMAP attribute flags
'special' => 'inbox', // null, or: inbox/sent/drafts/trash/spam
]
Use $folder['special'] to identify system folders and render appropriate icons:
@foreach ($folders as $folder)
<a href="{{ route('inbox.index', ['folder' => $folder['name']]) }}">
@if ($folder['special'] === 'inbox')
{{-- inbox icon --}}
@elseif ($folder['special'] === 'sent')
{{-- sent icon --}}
@else
{{-- generic folder icon --}}
@endif
{{ $folder['display'] }}
@if ($folder['unseen'] > 0)
<span>({{ $folder['unseen'] }})</span>
@endif
</a>
@endforeach
$currentFolder
A string containing the IMAP folder name of the currently open folder. Use it to highlight the active folder in your sidebar:
class="{{ $currentFolder === $folder['name'] ? 'active' : '' }}"
Required JavaScript Dependencies
The layout must load the following JavaScript. Alpine.js and Tailwind CSS are non-negotiable — the default content views use both extensively.
| Library | Version | Purpose |
|---|---|---|
| Alpine.js | 3.x | Reactivity, dropdowns, modal dialogs, inline state management |
| Tailwind CSS | 4.x | Utility-class styling for default content views |
Additionally, the compose view requires:
| Library | Version | Purpose |
|---|---|---|
| TipTap | 2.x | Rich text editor in compose/reply/forward |
If you are only overriding the layout and keeping default content views, you must include all three in your layout. If you override the compose view yourself, you can choose a different editor, but you must still handle the form submission format expected by the compose controller.
Recommended Script Loading Order
<head>
{{-- Tailwind (compiled) --}}
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
{{-- TipTap (bundled with the app) --}}
<script src="{{ asset('js/tiptap.js') }}" defer></script>
{{-- Alpine.js (must be deferred) --}}
<script src="{{ asset('js/alpine.js') }}" defer></script>
</head>
<body>
...
@stack('scripts')
</body>
CSRF Token
The CSRF token must be available as a <meta> tag for AJAX requests made by content views:
<meta name="csrf-token" content="{{ csrf_token() }}">
Alpine.js-powered forms and fetch calls read this value automatically when using the default content views. If you omit this tag, all AJAX actions (mark-as-read, delete, move, send mail) will fail with a 419 CSRF token mismatch error.
Summary Checklist
Before publishing or deploying a custom template, verify your layouts/app.blade.php includes:
@yield('title')inside<title>tags@yield('content')in the main content area@stack('scripts')before</body><meta name="csrf-token" content="{{ csrf_token() }}">- Alpine.js 3.x loaded with
defer - Tailwind CSS 4.x (compiled file or CDN)
- TipTap 2.x (if not overriding the compose view)
- Folder list rendered using
$folders - Active folder highlighted using
$currentFolder