Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mjml Binary Laravel Package

gollumsf/mjml-binary

gollumsf/mjml-binary makes it easy to compile MJML into responsive HTML emails in Laravel/PHP projects. It bundles the MJML binary, so you can render templates without managing external installations or dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually installing. gollumsf/mjml-binary is a Composer package that ships pre-compiled MJML CLI executables (Linux/macOS/Windows builds produced by Node's pkg). It exists so that GollumSF/MjmlBundle (Symfony) can render MJML without Node.js installed. There is no PHP class, no service provider, no facade, and no config to publish — it's a binary distribution plus a thin path helper. In a Laravel app you are using exactly one thing from it: the executable file.

Minimal steps

composer require gollumsf/mjml-binary

Then find the binary and confirm it runs:

ls vendor/gollumsf/mjml-binary/
find vendor/gollumsf/mjml-binary -type f -name 'mjml*' -executable
./vendor/gollumsf/mjml-binary/<platform>/mjml --version

Wrap that discovery in a tiny helper rather than hardcoding the path (the folder layout is platform-specific and has changed across releases):

// app/Support/Mail/MjmlBinary.php
class MjmlBinary
{
    public static function path(): string
    {
        $candidates = glob(base_path('vendor/gollumsf/mjml-binary/**/mjml*')) ?: [];
        foreach ($candidates as $file) {
            if (is_file($file) && is_executable($file) && ! str_ends_with($file, '.md')) {
                return $file;
            }
        }
        throw new RuntimeException('MJML binary not found; run composer install.');
    }
}

First use case. Add a mail.mjml.* set of Blade views that emit MJML instead of HTML, and compile them in the mailable:

// config/mjml.php
return ['binary' => env('MJML_BINARY', null), 'timeout' => 30];
// app/Support/Mail/MjmlRenderer.php
class MjmlRenderer
{
    public function __construct(private ?string $binary = null, private int $timeout = 30) {}

    public function render(string $mjml): string
    {
        $process = new Symfony\Component\Process\Process(
            [$this->binary ?? MjmlBinary::path(), '-s', '-r', '--config.validationLevel=soft']
        );
        $process->setInput($mjml);
        $process->setTimeout($this->timeout);
        $process->run();

        if (! $process->isSuccessful()) {
            throw new RuntimeException("MJML failed: ".$process->getErrorOutput());
        }
        return $process->getOutput();
    }
}
// app/Providers/AppServiceProvider.php
$this->app->singleton(MjmlRenderer::class, fn () => new MjmlRenderer(config('mjml.binary'), config('mjml.timeout')));

Where to look first: the package README (it lists the platform folders and the -s/-r stdin/stdout flags), then vendor/gollumsf/mjml-binary/ on disk. Ignore anything about MjmlBundle — that's Symfony wiring you won't use. Also read the MJML CLI docs upstream; the flags are MJML's, not the package's.

Implementation Patterns

Pattern 1 — compile-at-render in a Mailable. Simplest and fine for low volume:

class InvoiceMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public Invoice $invoice) {}

    public function build(): static
    {
        // Render Blade -> MJML source (escape data yourself, see Gotchas)
        $source = view('mail.mjml.invoice', [
            'invoice' => $this->invoice,
            'customerName' => e($this->invoice->customer->name),
            'ctaUrl' => e(route('invoices.show', $this->invoice)),
        ])->render();

        return $this->subject('Invoice #'.$this->invoice->number)
                    ->html(app(MjmlRenderer::class)->render($source));
    }
}

Mailable::build() is invoked with no arguments by Laravel, so type-hinted injection doesn't work here — resolve from the container with app(), or move the rendering into content()/an accessor.

Pattern 2 — compile the shell at build time, keep Blade at runtime. MJML is not a templating language, so the cheapest production setup is: compile the layout once in CI/deploy into an .blade.php file, then let Laravel interpolate per-recipient data. Put Blade tags directly in the MJML source and they survive compilation as literal text:

{{-- resources/mjml/invoice.mjml --}}
<mjml>
  <mj-body>
    <mj-section>
      <mj-text>Hi {{ $customerName }},</mj-text>
      <mj-button href="{{ $ctaUrl }}">View invoice</mj-button>
    </mj-section>
  </mj-body>
</mjml>
// app/Console/Commands/CompileMjml.php
protected $signature = 'mjml:compile';

public function handle(MjmlRenderer $mjml): int
{
    foreach (File::glob(resource_path('mjml/*.mjml')) as $file) {
        $html = $mjml->render(File::get($file));
        File::put(resource_path('views/mail/html/'.basename($file, '.mjml').'.blade.php'), $html);
    }
    return self::SUCCESS;
}

Now the mailable is a plain ->view('mail.html.invoice', $data), the binary never runs in production, and you get zero runtime latency. This is the pattern I'd default to for anything beyond a handful of transactional emails.

Pattern 3 — cache by content hash. If you must compile at request time, cache the compiled shell (variables stripped) and only recompile when the MJML source changes:

$key = 'mjml:'.md5($layoutSource);
$shell = Cache::rememberForever($key, fn () => $mjml->render($layoutSource));
// then compile the small variable-bearing fragment, or splice at markers

For per-user data, compile the layout to @include-able HTML and use Blade inheritance rather than re-running the binary per email.

Pattern 4 — queue it. Compilation is 100–800 ms (cold start higher). If you send bulk mail, compile inside a job and persist the HTML to storage/app/mail/ or a rendered_html column; the actual send job just attaches/streams it. Keeps mail worker time flat and makes retries free.

Integration with Laravel Mail tests.

Mail::fake();
$this->post('/invoices/1/send');
Mail::assertSent(InvoiceMail::class, fn ($mail) => str_contains($mail->render(), 'View invoice'));

Bind a fake renderer in TestCase::setUp() so your test suite never shells out:

$this->app->instance(MjmlRenderer::class, new class extends MjmlRenderer {
    public function render(string $mjml): string { return '<html>stub</html>'; }
});

Keep one or two real-binary tests (a "contract" test) asserting the output contains <table and the expected CTA URL, so you notice when the binary breaks on a CI image change.

Config/env conventions that pay off

Concern Where
Binary path override MJML_BINARY env → config/mjml.php
Timeout MJML_TIMEOUT, default 30s
Validation level soft in prod (don't fail sends on warnings), strict in dev/CI
Build-time compilation php artisan mjml:compile in your deploy script, before config:cache

Gotchas and Tips

The license is the first thing to check. This package is GPL-3.0. MJML itself is MIT, but this distribution's license is not. Shipping a GPL binary inside a commercial Laravel app is usually "mere aggregation" (your PHP code stays proprietary), but many companies' OSS policies block GPL in vendor/ outright. Get a legal sign-off before it lands on main, or avoid the package and install mjml via npm in a build step instead — the output is identical.

It's stale. Last release January 2021. That pins you to an MJML from that era: newer components and bug fixes are missing, and the compiled pkg runtime is old enough that it may not start on modern distros. Treat the binary as frozen and version-pin it ("gollumsf/mjml-binary": "1.0.*"), or vendor the file yourself so a composer update can't swap it.

Architecture and libc are the top failure modes.

  • The 2021 builds are almost certainly x86_64 only. On Apple Silicon, add --platform=linux/amd64 in Docker or run under Rosetta; ARM GitHub runners (ubuntu-24.04-arm) will fail.
  • pkg output is glibc-linked, so it does not run on Alpine/musl (Error loading shared library ld-linux-x86-64.so.2 or a bare "not found"). Use a Debian-slim base image, add gcompat, or compile MJML at build time on a builder stage and copy the HTML out.
  • If the executable bit is lost (zip extraction, some volume mounts, Windows hosts), Process throws "Permission denied" — chmod +x in your deploy step, and never execute from a host-mounted vendor/ on macOS.

Escaping is on you. MJML is XML-shaped. Any data injected into an mj-text, mj-button href, or attribute must be XML-escaped (e() / htmlspecialchars) before compilation, or you get broken markup or a compiler error. Conversely, when you intentionally inject HTML blocks ({!! $html !!}), MJML's own sanitization won't save you — sanitize at the source.

Use stdin, not argv. -s -r (stdin/stdout) avoids ARG_MAX limits, quoting bugs, and shell-injection risk from filenames. Build the argv array with Process, never a shell string.

Alpine and Vapor/Lambda. Both need special handling: Alpine via gcompat/Debian, Lambda via a custom layer or, far simpler, build-time compilation (Pattern 2) so no binary ships at all.

Windows dev machines. Even if a Windows build exists, path/line-ending differences bite. Guard MjmlBinary::path() to throw a clear message, and point MJML_BINARY at a local npx mjml shim if you want dev parity without the package.

Debugging checklist. Run the binary by hand with the exact same input: ./path/mjml -s -r < t.mjml. If it succeeds there and fails in PHP, the issue is Process env (missing PATH/LANG), timeout, or the exec bit. Log getErrorOutput() — MJML prints useful validation errors there but exits 0 with warnings under validationLevel=soft, so a "successful" render can still be visibly wrong.

Validation level. strict turns warnings into errors — great in CI, hostile in production. soft (or skip) is the right prod setting.

Minification and comments. MJML minifies by default (--config.minify). For email debugging you'll want --config.beautify --config.minify=false in a dev-only code path; remember comments (--config.keepComments) leak into client-facing HTML otherwise.

Extension points you actually have. There are none inside the package — swap behavior at your boundary: bind MjmlRenderer to a fake in tests, to a remote-API implementation (mjml.io / a Node sidecar) if the binary misbehaves, or to a "read precompiled Blade" implementation in production. Keep the interface small (render(string): string) and every escape hatch stays open.

Alternatives worth pricing in. If you're on Laravel, weigh: (a) npm i mjml in a CI build step + Pattern 2 — MIT, always current, no GPL, no runtime binary; (b) a Node sidecar or mjml HTTP service; (c) a hosted MJML API for low volume. This package's real value — "no Node.js needed" — matters mainly for Symfony shared hosting, which isn't a typical Laravel deployment target. With 3 stars and no dependents, you're the maintainer if it breaks.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment