Everyone

Layout Contract

The required Blade directives, available variables, and JavaScript dependencies every layouts/app.blade.php must implement.

Last updated 2026-04-12
  • PHP Variables Available in Every Authenticated Layout
  • Required JavaScript Dependencies
  • CSRF Token
  • Summary Checklist
  • 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