twig/inky-extra
Twig extension that adds the inky_to_html filter, converting Zurb Inky email templates into HTML. Useful for building responsive email markup within Twig templates in Symfony and other Twig-based apps.
composer require twig/twig twig/inky-extra
Twig\Environment in a service provider (e.g., App\Providers\EmailServiceProvider):
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
public function register()
{
$loader = new FilesystemLoader(__DIR__.'/../resources/views/emails');
$this->app->singleton('twig.email', function () use ($loader) {
return new Environment($loader, [
'cache' => storage_path('twig/email'),
]);
});
}
resources/views/emails/welcome.inky.twig:
{# resources/views/emails/welcome.inky.twig #}
<inky:message>
<inky:header>
<inky:title>Welcome!</inky:title>
</inky:header>
<inky:body>
<inky:section>
<h1>Hello, {{ name }}!</h1>
<p>Thanks for joining.</p>
</inky:section>
</inky:body>
</inky:message>
Mailable and use the inky_to_html filter:
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
{
use Queueable, SerializesModels;
public function build()
{
return $this->markdown('emails.welcome')
->with([
'name' => $this->user->name,
])
->withTwig(['name' => $this->user->name])
->withInkyFilter();
}
protected function withInkyFilter()
{
$twig = app('twig.email');
$html = $twig->getLoader()->getSource('welcome.inky.twig');
$content = $twig->createTemplate($html)->render([
'name' => $this->user->name,
]);
return $this->subject('Welcome!')
->html($content);
}
}
Replace a static HTML email template with a dynamic Inky + Twig template for password resets:
resources/views/emails/password_reset.blade.php to resources/views/emails/password_reset.inky.twig.<inky:message>
<inky:header>
<inky:title>Reset Your Password</inky:title>
</inky:header>
<inky:body>
<inky:section>
<h1>Hi {{ user.name }},</h1>
<p>Click the link below to reset your password:</p>
<inky:button href="{{ reset_url }}">Reset Password</inky:button>
</inky:section>
</inky:body>
</inky:message>
ResetPassword Mailable to use the new template:
public function build()
{
return $this->markdown('emails.password_reset')
->with([
'user' => $this->user,
'reset_url' => $this->resetUrl,
])
->withInkyFilter();
}
Template Structure:
Organize templates by type (e.g., resources/views/emails/transactional/, resources/views/emails/marketing/).
Example:
resources/views/emails/
├── transactional/
│ ├── password_reset.inky.twig
│ ├── order_confirmation.inky.twig
├── marketing/
│ ├── welcome_series/
│ │ ├── welcome.inky.twig
│ │ ├── welcome_part2.inky.twig
Reusable Components:
Create partials for shared elements (e.g., headers, footers) in resources/views/emails/_partials/.
{# resources/views/emails/_partials/footer.inky.twig #}
<inky:footer>
<inky:section>
<p>© {{ year }} {{ company_name }}. All rights reserved.</p>
</inky:section>
</inky:footer>
Include in templates:
{% include '_partials/footer.inky.twig' %}
Conditional Logic:
Use Twig’s {% if %} to handle dynamic content (e.g., premium user features):
<inky:section>
<h2>Your Account</h2>
{% if user.premium %}
<p>Enjoy premium features like advanced analytics.</p>
{% else %}
<inky:button href="{{ upgrade_url }}">Upgrade Now</inky:button>
{% endif %}
</inky:section>
Loops for Lists: Render dynamic lists (e.g., order items) with Twig loops:
<inky:section>
<h2>Order #{{ order.id }}</h2>
<table>
<thead>
<tr><th>Item</th><th>Price</th></tr>
</thead>
<tbody>
{% for item in order.items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.price|currency }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</inky:section>
Base Mailable Class:
Create a reusable InkyMailable class to avoid boilerplate:
abstract class InkyMailable extends Mailable
{
protected $twigData = [];
public function withTwig(array $data)
{
$this->twigData = array_merge($this->twigData, $data);
return $this;
}
protected function applyInkyFilter(string $template)
{
$twig = app('twig.email');
$html = $twig->getLoader()->getSource($template);
$content = $twig->createTemplate($html)->render($this->twigData);
return $this->html($content);
}
}
Extend in child classes:
class WelcomeEmail extends InkyMailable
{
public function build()
{
return $this->withTwig(['name' => $this->user->name])
->applyInkyFilter('emails.welcome');
}
}
Asset Management: Inline critical CSS/JS for email clients that block external resources:
use Illuminate\Support\Facades\Asset;
protected function applyInkyFilter(string $template)
{
$twig = app('twig.email');
$html = $twig->getLoader()->getSource($template);
$content = $twig->createTemplate($html)->render($this->twigData);
// Inline CSS if needed (e.g., for Gmail)
if ($this->shouldInlineCss()) {
$content = $this->inlineCss($content);
}
return $this->html($content);
}
protected function inlineCss(string $html): string
{
// Use a package like `php-css-inliner` or custom logic
return preg_replace('/<style[^>]*>(.*?)<\/style>/is', '', $html);
}
Testing:
Use Laravel’s Mailable testing helpers to verify Inky output:
public function test_welcome_email()
{
$user = User::factory()->create();
$email = new WelcomeEmail($user);
$this->assertEquals('Welcome!', $email->subject);
$this->assertStringContainsString('Hello, '.$user->name, $email->render());
}
For Inky-specific validation, add custom assertions:
public function assertInkyValid(string $html)
{
$this->assertStringContainsString('<inky:message>', $html);
$this->assertStringContainsString('<inky:header>', $html);
// Add more Inky-specific checks
}
{# resources/views/emails/marketing/campaign.inky.twig #}
{% if campaign.variant == 'A' %}
{% include 'campaign_variant_a.inky.twig' %}
{% elseif campaign.v
How can I help you explore Laravel packages today?