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 Bundle Laravel Package

notfloran/mjml-bundle

A Symfony bundle that integrates MJML into PHP apps, compiling MJML markup into responsive HTML emails. It wraps the MJML renderer with configurable options and a simple service for generating templates, making it easy to build maintainable, mobile-friendly transactional emails in Symfony projects.

View on GitHub
Deep Wiki
Context7

Getting Started

notfloran/mjml-bundle wraps the MJML compiler so you can turn .mjml markup into responsive email HTML from PHP instead of shelling out to the CLI yourself.

composer require notfloran/mjml-bundle

Two things must exist before the first render:

  1. The mjml binary — npm install -g mjml (or a local node_modules/.bin/mjml). Point the bundle at it with the MJML_BINARY_PATH env var or the mjml.binary_path config key; it defaults to looking up mjml on PATH.
  2. A renderer choice — the bundle ships a BinaryRenderer (local subprocess) and an ApiRenderer (Mailjet's hosted MJML API). Binary is the sane default; the API renderer is for hosts where you cannot install Node.

Minimal usage once wired up:

use Mjml\Renderer\RendererInterface;

$html = $renderer->render(<<<'MJML'
<mjml>
  <mj-body>
    <mj-section><mj-column>
      <mj-text>Hello</mj-text>
    </mj-column></mj-section>
  </mj-body>
</mjml>
MJML);

Where to look first: the bundle's DependencyInjection/Configuration.php (all config keys and defaults), the Renderer/ directory (which renderer is aliased to RendererInterface), and your framework's service registration for the bundle. Note this is a Symfony bundle at heart — it ships a *Bundle class and DI extension, not a Laravel service provider. In Laravel you either use it via a small adapter package, or instantiate BinaryRenderer directly and bind it in a service provider. Verify which path you are on before debugging config that "isn't being read" — that is almost always the cause.

First real use case: take an existing transactional email (password reset, order confirmation), port the HTML to MJML, render it in a console command, and diff the output against the current template before wiring it into the mailable/notification.

Implementation Patterns

Template-first, then MJML. Rarely hand-write MJML strings in PHP. Render a Twig/Blade view containing the MJML, then pass the result to the renderer. This keeps loops, conditionals and translations in the templating layer where they belong:

$mjml = $twig->render('emails/order.mjml.twig', ['order' => $order]);
$html = $renderer->render($mjml, ['minify' => true]);

Options are per-render and global. Global defaults live in config (mjml.options); the second argument to render() merges over them for a single call. Use the global layer for house style (juicePreserveTags, minify, font settings) and per-call options for one-offs like keepComments in local debugging.

Cache the compiled output, not the input. MJML compilation spawns a Node process — tens to hundreds of milliseconds each. Cache the resulting HTML keyed on a hash of the MJML source plus the options array, with a TTL tied to your deploy. Because the source includes rendered variables, this naturally invalidates when data changes; for static shells, key on template mtime instead.

Wrap it in a mailer/notification layer. Don't inject RendererInterface into twenty classes. Introduce one MjmlMailRenderer that owns the render + cache + error handling, and inject that. It also gives you a single seam to swap in the API renderer or a stub in tests.

Stub it in tests. Bind a fake RendererInterface returning a fixed HTML string for anything asserting on mail content; keep one or two integration tests that hit the real binary so a broken mjml install fails CI rather than production. A test that asserts "the output contains <!doctype html> and no <mj- tags" catches a renderer that silently returned its input.

Install Node in your runtime image. The single most common integration failure is a PHP container with no Node. Add nodejs + mjml to the Dockerfile (or a sidecar) and set MJML_BINARY_PATH explicitly rather than relying on PATH resolution, which differs between CLI and FPM.

Pin the binary version alongside the bundle. MJML output changes between major versions. Record the mjml version in the image and, if you snapshot-test emails, expect churn on every MJML upgrade.

Gotchas and Tips

MJML 5 changes the runtime floor, not just the API. v3.11.0 adapts BinaryRenderer for the MJML 5 CLI. MJML 5 requires a much newer Node than the 4.x line did, so an image that happily ran mjml@4 may fail to install or execute mjml@5 — check your Node version before blaming the bundle. If you cannot move Node yet, pin mjml@4 in the image; the bundle's binary renderer talks to whichever binary you point it at.

The adapter is about invocation, not output. Treat v3.11.0 as a compatibility fix: expect it to make the subprocess call work against the 5.x CLI's arguments/entry point. It does not promise byte-identical HTML versus 4.x. If you have golden-file tests, regenerate them after upgrading the binary and skim the diff for dropped styles or changed inlining.

Silent-empty output is the classic failure mode. When the binary exits non-zero or writes to stderr, you can end up with an empty or truncated string rather than an exception, depending on renderer and version. Always assert the result is non-empty and contains </html> at your mail-render boundary. When debugging, run the exact same MJML through the CLI by hand — mjml --stdin --config.minify true — and read stderr; the bundle's exception message often surfaces only a generic exit code.

Option flattening is literal. Nested option arrays become dotted CLI flags (['config' => ['minify' => true]] → --config.minify true). Wrong nesting produces a flag the CLI rejects, which surfaces as an exit-code error with no useful message. When an option "doesn't work", echo the assembled command and compare against mjml --help for your installed version.

Paths and permissions. Under FPM the working directory and PATH differ from your shell. Use an absolute binary_path, and confirm the user running PHP has execute permission on both the mjml shim and the node it invokes. exec/proc_open disabled in php.ini produces a confusing "renderer unavailable" rather than a clear error.

The API renderer is a different risk profile. It ships your email HTML to Mailjet over the network: latency in the send path, rate limits under batch sends, an external dependency in your password-reset flow, and a data-egress question for anything containing customer PII. Use it for previews and tooling, prefer the binary for production mail.

Escape MJML-sensitive characters in interpolated data. User content containing <, &, or {{ }} inserted into <mj-text> can break parsing or be interpreted by the templating layer before MJML sees it. Escape at the templating layer and keep a raw block for the deliberately-HTML parts.

Extension point: implement RendererInterface and alias it over the bundle's renderer service to slot in a custom backend (a queue-backed renderer, a cached decorator, a mock). That alias is the supported seam — avoid subclassing BinaryRenderer internals, since v3.11.0 just demonstrated that those internals track the MJML CLI.

mj- tags leaking into the final email means MJML never ran — the string went to the transport directly. A one-line assertion in your mail test (assertStringNotContainsString('<mj-', $html)) catches this permanently.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console