evanschleret / lara-mjml
Just a service provider for Spatie's MJML wrapper
Fund package maintenance!
Requires
- php: ^8.2 || ^8.3 || ^8.4 || ^8.5
- illuminate/config: ^12.0|^13.0
- illuminate/mail: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/view: ^12.0|^13.0
- spatie/mjml-php: ^1.2.5
Requires (Dev)
- laravel/pint: ^1.0
- pestphp/pest: ^2.20 || ^3.0
- spatie/ray: ^1.28
README
LaraMJML
MJML rendering for Laravel Blade emails with a dedicated view engine, configurable MJML options, and predictable output for Mailables and Notifications.
Why LaraMJML
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
Requirements
- PHP
>=8.2 - Laravel
12.xor13.x - Node.js runtime with the
mjmlpackage installed
Installation
Install the package:
composer require evanschleret/lara-mjml
Install MJML in your Laravel app:
npm install mjml
Publish the package config (optional):
php artisan vendor:publish --provider="EvanSchleret\LaraMjml\Providers\LaraMjmlServiceProvider"
Quick start
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> @endsection
Save it as:
resources/views/emails/welcome.blade.php
Blade file rules
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
Use with Mailable
<?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));
Use with Notification
<?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));
Configuration
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
Troubleshooting
- 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
Testing
Run tests:
composer test
Validate MJML templates
Validate all MJML Blade layouts:
php artisan laramjml:validate
Validate specific paths:
php artisan laramjml:validate --path=resources/views/emails php artisan laramjml:validate --path=resources/views/layouts/base.mjml.blade.php
Use a different validation level:
php artisan laramjml:validate --validation=soft
The command returns a non-zero exit code when at least one template fails validation, so it is ready for CI workflows.
Roadmap
- 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
Other packages
If you want to explore more of my packages:
- evanschleret/formforge
- evanschleret/formformclient (FormForge Client)
- evanschleret/laravel-user-presence
- evanschleret/laravel-typebridge
Open source
- Contributing guide: CONTRIBUTING.md
- Security policy: SECURITY.md
- Code of conduct: CODE_OF_CONDUCT.md
- License: LICENSE
