hampe/zurb-ink-bundle
Symfony bundle that integrates the ZURB Ink responsive email framework, helping you build mobile-friendly HTML emails with Twig templates and Symfony’s templating tools. Ideal for transactional emails and newsletters.
composer require hampe/zurb-ink-bundle
Register the bundle. There is no Flex recipe, so on Symfony 4+ you add it by hand:
// config/bundles.php
Hampe\ZurbInkBundle\ZurbInkBundle::class => ['all' => true],
On the Symfony 2.8/3.x this bundle was built for, it goes into app/AppKernel.php instead. Check the bundle's README and DependencyInjection/Configuration.php for the exact namespace and config tree for your version — the last tagged release is from early 2018, so expect the docs to describe an older Symfony.
Where to look first, in order:
DependencyInjection/Configuration.php — the authoritative list of options (CSS paths, template locations, inlining toggles). Ignore blog posts; this file is the truth.Resources/views/ (or templates/) — the base email layout the bundle ships. Extend it rather than writing <html><body> boilerplate by hand.Twig/…Extension.php) — this exposes the filter/function that runs the Inky compiler, which converts Ink tags (<container>, <row>, <columns>, <button>, <callout>) into the nested-table HTML email clients actually understand.First use case. Build one transactional email end-to-end: a welcome mail.
{# templates/email/welcome.html.twig #}
{% extends '@ZurbInk/layout.html.twig' %}
{% block content %}
<container>
<row>
<columns small="12">
<h1>Welcome, {{ user.firstName }}</h1>
<p>Confirm your address to get started.</p>
<button href="{{ url('confirm', {token: token}) }}">Confirm email</button>
</columns>
</row>
</container>
{% endblock %}
Then render it, run the Inky transform, inline the CSS, and hand the resulting string to your mailer. Get that one email landing correctly in Gmail, Outlook and Apple Mail before you build a second one — the pipeline is the hard part, not the markup.
Wrap the render pipeline in one service. Do not repeat "render → Inky → inline CSS" in every controller. A single EmailRenderer with render(string $template, array $context): string is the pattern that keeps you sane:
$html = $this->twig->render($template, $context);
$html = $this->inky->process($html); // Ink tags -> tables
$html = $this->inliner->convert($html, $css); // CSS -> style="" attributes
Ordering is not negotiable: Twig first, Inky second, CSS inlining last. Inlining before Inky means your selectors never match the elements Inky generates.
Layout inheritance over composition. Have one base layout per brand (transactional vs. marketing), extend it everywhere, and override {% block content %}. Partials (_button.html.twig, _footer.html.twig) are fine via include, but run Inky on the fully assembled document — a partial containing <button> rendered on its own will not be transformed.
Keep CSS in files, not in the template. Put assets/email/ink.css and assets/email/brand.css on disk, reference them from bundle config, and let the inliner read them. This gives you one place to change a link colour across every email, and it diffs cleanly in code review.
Preview route, dev-only. A controller guarded by %kernel.debug% that renders a given email template straight to the browser is the single highest-value thing you can add. Designers can iterate without sending mail, and you can eyeball the generated table soup with view-source.
Test the output, not the template. Assert on the rendered string: that no <columns/<row> tags survive, that the confirmation URL is absolute, that the unsubscribe link is present. These catch the regressions that actually reach users' inboxes.
Pass absolute URLs everywhere. url() and absolute_url(asset(...)). Email clients have no base href; relative paths silently 404.
This package is effectively unmaintained. Last release February 2018, and Zurb Ink itself was superseded by Foundation for Emails and then by MJML. It works fine if you are pinned to an older Symfony, but on Symfony 5+ expect friction: bundle registration, deprecated Twig APIs, and the removal of SwiftMailer all sit downstream of this bundle. Budget a day for a spike before committing to it on a new project; if it fights you, MJML via a small renderer service is the lower-risk path for new work.
CSS inlining destroys media queries. Standard inliners move <style> rules into style="" attributes — which email clients ignore for @media. Make sure your inliner is configured to preserve the <style> block for the media queries while inlining the rest. If your responsive breakpoints suddenly stop working, this is why.
Twig escaping. The Inky output is HTML, so whatever filter or function exposes it must be marked safe, and you must not |raw untrusted user content into the template to compensate. Keep autoescaping on for variables.
Cache warmup hides CSS edits. The bundle resolves CSS file paths at container-build time in some configurations; changing a stylesheet may require bin/console cache:clear before the change appears. If an edit "does nothing", clear the cache before you start debugging your selectors.
Override, don't fork. Bundle templates can be shadowed at templates/bundles/ZurbInkBundle/…. The Inky/Inliner services can be decorated (#[AsDecorator]) or re-aliased in services.yaml if you need custom post-processing — e.g. rewriting URLs to absolute, or appending a tracking pixel.
Check the generated HTML by hand at least once. Inky emits <table role="presentation"> nesting that is verbose and occasionally surprising with <columns small="12" large="6"> combinations. Save the final string to var/email-preview.html, open it, and compare against what you expected. It is much faster than debugging via screenshots from a colleague's Outlook.
Watch line length. Long base64 images or unwrapped HTML can trip quoted-printable encoding and produce =-broken links in some clients. Keep image URLs as asset() references rather than data URIs, and let your mailer's encoder handle wrapping.
Extension point worth knowing: if you need behaviour the bundle doesn't expose — say, injecting a preheader span into every email — write a thin Twig extension or a service decorator around the renderer rather than editing vendor templates. Vendor edits vanish on the next composer update, and this package will not be getting upstream fixes.
How can I help you explore Laravel packages today?