MJML rendering for Laravel Blade emails with a dedicated view engine, configurable MJML options, and predictable output for Mailables and Notifications.
LaraMJML gives you a focused way to render MJML in Laravel without changing your email workflow:
- keeps Laravel Blade as the template layer
- compiles
.mjml.blade.phplayouts through an MJML view engine - supports MJML runtime options from Laravel config
- works with Mailables and Notifications
- keeps rendering behavior deterministic across environments
- PHP
>=8.2 - Laravel
12.xor13.x - Node.js runtime with the
mjmlpackage installed
Install the package:
composer require evanschleret/lara-mjmlInstall MJML in your Laravel app:
npm install mjmlPublish the package config (optional):
php artisan vendor:publish --provider="EvanSchleret\LaraMjml\Providers\LaraMjmlServiceProvider"Create an MJML layout:
<mjml>
<mj-body>
<mj-section>
<mj-column>
@yield('content')
</mj-column>
</mj-section>
</mj-body>
</mjml>Save it as:
resources/views/layouts/base.mjml.blade.php
Create your email view with regular Blade inheritance:
@extends('layouts.base')
@section('content')
<mj-text>Hello {{ $userName }}</mj-text>
@endsectionSave it as:
resources/views/emails/welcome.blade.php
Use this hierarchy to avoid malformed MJML:
- the layout contains
<mjml>and<mj-body> - the layout filename includes
.mjml.blade.php - child views extend the MJML layout
- child views do not include
.mjmlin the filename
Correct:
resources/views/layouts/base.mjml.blade.php
resources/views/emails/welcome.blade.php
Incorrect:
resources/views/layouts/base.mjml.blade.php
resources/views/emails/welcome.mjml.blade.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeMail extends Mailable
{
use Queueable;
use SerializesModels;
public function __construct(
public string $userName,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome',
);
}
public function content(): Content
{
return new Content(
view: 'emails.welcome',
with: [
'userName' => $this->userName,
],
);
}
}Send it:
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
Mail::to($user->email)->send(new WelcomeMail($user->name));<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class WelcomeNotification extends Notification
{
use Queueable;
public function __construct(
private readonly User $user,
) {}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage())
->subject('Welcome')
->view('emails.welcome', [
'userName' => $this->user->name,
]);
}
}Dispatch it:
$user->notify(new WelcomeNotification($user));The config/laramjml.php file controls:
binary_path: path to the Node binary for MJML executionbeautify: format generated HTMLminify: minify generated HTMLkeep_comments: preserve MJML comments in outputoptions: extra options passed to MJML
Environment variables:
MJML_NODE_PATH=null
LARA_MJML_BEAUTIFY=false
LARA_MJML_MINIFY=true
LARA_MJML_KEEP_COMMENTS=false- Empty or broken HTML: ensure only the layout contains
<mjml>and<mj-body> - MJML binary error: install
mjmlin the project and verify Node.js is available - Malformed MJML exceptions: remove
.mjmlsuffix from child views and keep it only on the layout
Run tests:
composer testValidate all MJML Blade layouts:
php artisan laramjml:validateValidate specific paths:
php artisan laramjml:validate --path=resources/views/emails
php artisan laramjml:validate --path=resources/views/layouts/base.mjml.blade.phpUse a different validation level:
php artisan laramjml:validate --validation=softThe command returns a non-zero exit code when at least one template fails validation, so it is ready for CI workflows.
- Add an optional Artisan install command (
laramjml:install) to publish config in one step - Add optional plain HTML fallback rendering when MJML conversion fails
- Add built-in MJML lint/validate command for CI workflows
- Add a stub generator for MJML layout + starter email view
- Add snapshot tests for common MJML output patterns
- Add first-party examples for dark mode email patterns
- Add docs for queue-first email pipelines with MJML pre-rendering
- Add benchmarking docs and performance tips for high-volume mailing
If you want to explore more of my packages:
- evanschleret/formforge
- evanschleret/formformclient (FormForge Client)
- evanschleret/laravel-user-presence
- evanschleret/laravel-typebridge
- Contributing guide: CONTRIBUTING.md
- Security policy: SECURITY.md
- Code of conduct: CODE_OF_CONDUCT.md
- License: LICENSE
