Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Ink: Automatic CSS Email Framework

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our email template is HTML... very traditional HTML. What I mean is, this is the type of HTML and CSS you would see on a normal website. And, at least inside Mailtrap... it looks good! But a big lesson of sending emails is that the HTML is often not rendered like a normal browser would render it. Some email clients don't support float or flexbox... so if you're using those to establish an email layout then... oof, it's going to look bad for some people... like people using gmail.

If you want to write an email that's going to look consistently good in every email client, the best practice is actually to use tables for your layout. If you have no idea what a table layout is... oh, you are so, so lucky. Back in the dark ages of the Internet, back before CSS float and flexbox existed, every webpage's layout consisted of tables, rows and cells. It was tables, inside of tables, inside of tables, inside of tables. It was... a nightmare.

So... um... am I saying that the nightmare of needing to write table-based layouts is still a reality when you create emails? Yes... and no. Mailer has another trick up its sleeve.

Hello Ink / Foundation for Emails

Google for "Inky Framework" to find something called "Ink" by "Zurb". Let me define... a few things. Zurb is the name of a company, a cool name - it sounds like an alien race: "the Zurb". Anyways, Zurb is the company that created "Foundation": a CSS framework that's probably the second most famous in the world behind Bootstrap. "Ink" is the name of a CSS framework that's designed specifically for emails. And actually, they've renamed "Ink" to just "Foundation for Emails".

So, Ink, or Foundation for Emails is a CSS framework for responsive HTML emails that works on any device. Even Outlook! Click on the docs.

Foundation for emails is basically two parts. First, it's a CSS file that defines useful CSS classes and a grid structure for designing emails. Again... it's just like Bootstrap CSS for emails.

The Inky Templating Language

That CSS file is super handy. But the second part of Foundation for emails is even more interesting. Click the "Inky" link on the left. The second part of this library is centered around a custom templating language called "Inky". It's a simple, but fascinating tool. Click the "Switch to Inky" link.

Here's the idea: we write HTML using some custom Inky HTML tags, like <container>, <row> and <columns>... as well as a few others like <button> and <menu>. Then, Inky will transform this pretty HTML into the crazy, ugly table-based layout required for it to render in an email! Yea, it lets us have table-based emails... without needing to use tables! Yeehaw!

Using the inky_to_html Filter

Now if you downloaded the course code, you should have a tutorial/ directory, which holds the original welcome.html.twig and an inky/ directory with an updated welcome.html.twig. New stuff!

This is basically the same template but written in that special "Inky" markup: containers, rows, columns, etc. Copy the contents... and let's close a few things. Then open up templates/email/welcome.html.twig and completely replace this file with the updated version.

It's really the same email as before: it has the same dynamic URLs and is printing the recipient's name. It's just different markup. Oh, and notice that the inline_css() stuff we added a few minutes ago is gone! Gasp! Don't worry: we'll put that back in a minute. But until then, forget about CSS.

If we sent this email right now, it would literally send with this markup. To transform this into the table-based markup we want, we'll use another special filter on the entire template. On top, add {% apply inky_to_html %}... and all the way at the bottom, put {% endapply %}. I'll indent this to make it look nice.

{% apply inky_to_html %}
<container>
<row class="header">
<columns>
<a href="{{ url('app_homepage') }}">
<img src="{{ email.image('@images/email/logo.png') }}" class="logo" alt="SpaceBar Logo">
</a>
</columns>
</row>
<row class="welcome">
<columns>
<spacer size="35"></spacer>
<h1>
<center>
Nice to meet you {{ email.toName }}!
</center>
</h1>
<spacer size="10"></spacer>
</columns>
</row>
<spacer size="30"></spacer>
<row>
<columns>
<p>
Welcome to <strong>the Space Bar</strong>, we can't wait to read what you have to write.
Get started on your first article and connect with the space bar community.
</p>
</columns>
</row>
<row>
<columns>
<center>
<button href="{{ url('admin_article_new') }}">Get writing!</button>
</center>
</columns>
</row>
<row>
<columns>
<p>
Check out our existing articles and share your thoughts in the comments!
</p>
</columns>
</row>
<row>
<columns>
<center>
<button href="{{ url('app_homepage') }}">Get reading!</button>
</center>
</columns>
</row>
<row>
<columns>
<p>
We're so excited that you've decided to join us in our corner of the universe,
it's a friendly one with other creative and insightful writers just like you!
Need help from a friend? We're always just a message away.
</p>
</columns>
</row>
<row class="footer">
<columns>
<p>Cheers,</p>
<p>Your friendly <em>Space Bar Team</em></p>
</columns>
</row>
<row class="bottom">
<columns>
<center>
<spacer size="20"></spacer>
<div>
Sent with ❤️ from the friendly folks at The Space Bar
</div>
</center>
</columns>
</row>
</container>
{% endapply %}

Let's try it! Find your browser and make sure you're on the registration page. Let's register as thetruthisoutthere11@example.com, any password, check the terms, register and... error!

Ah, but we know this error! Well, not this exact error, but almost! This is Twig telling us that we're trying to use a filter that requires an extra library. Cool! Copy the composer require line, move back over to your terminal, and paste:

composer require twig/inky-extra

Tip

Make sure you have XSL extension installed for your PHP to be able to use Inky. To check it - you can run php -m | grep xsl in your console and check the output has "xsl".

When that finishes... move back to your browser, go back to the registration form, tweak that email and... deep breath... register! I think it worked! Let's go check it out.

There's the new email! Oof, it looks terrible... but that's only because it doesn't any CSS yet. Check out the HTML source. So cool: it transformed our clean markup into table elements! We just took a huge step towards making our emails look good in every email client... without needing to write bad markup.

Inlining the foundation-emails CSS

To get this to look good, we need to include some CSS from Foundation for Emails. Go back to the documentation, click on the "CSS Version" link and click download. When you unzip this, you'll find a foundation-emails.css file inside. Copy that... and paste it into, how about, the assets/css directory.

How do we include this in our email template? We already know how: the inline_css filter. But instead of adding another apply tag around the entire template, we can piggyback off of inky! Add |inline_css and pass this source() and the path to the CSS file: @styles/foundation-emails.css.

Remember: if you look in config/packages/twig.yaml, we set up a path that allows us to say @styles to refer to the assets/css directory. That's how this path works.

And... I still do want to include my custom email.css code. Copy the source() stuff, add a second argument to inline_css - you can pass this as many arguments of CSS as you want - and point this at email.css.

{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css'), source('@styles/email.css')) %}
... lines 2 - 76
{% endapply %}

That should do it! Oh, but before we try this, back in tutorial/, that inky/ directory also holds an email.css file. Now that we're using a CSS framework, some of the code in our original email.css... just isn't needed anymore! This new email.css is basically the same as the original one... but with some extra stuff removed. Copy the code from the file, and paste it over the one in assets/css.

body {
margin: 0;
padding: 0;
background-color: #f3f3f3;
font-family: Helvetica, Arial, sans-serif;
}
h1 {
background-color: #264459;
color: #ffffff;
/*padding: 30px 0 50px 0;*/
font-weight: normal;
}
hr {
border: none;
border-top: 3px solid #264459;
margin: 20px;
}
.welcome {
background-color: #264459;
}
.bottom {
background-color: #efefee;
}
.logo {
width: 100%;
}
.text-center {
text-align: center;
}
table.button a {
background-color: #264459;
}
table.button table td {
background-color: #264459;
border: 2px solid #264459;
}

Ok, time to see the final product! Go back to the registration page, update the email, add a password, enter and... go check out Mailtrap. There it is and... it looks awesome. Well, it looks exactly like it did before, but in the HTML source, now that we have a table-based layout, we know this will display more consistently across all email clients. I won't say perfect... because you'll need to do some testing - but it's now much more likely to look good.

So that's "Foundation for Emails". It's, one, a CSS framework for emails... a lot like Bootstrap for emails... and two, a tool to transform the pretty markup known as Inky into the ugly table-based HTML that the CSS framework styles and that email clients require.

Watch your Email Sizes

Before we keep going, one thing to watch out for regardless of how you're styling your emails, is email size. It's far from a science, but gmail tends to truncate emails once their size is greater than about 100kb: it hides the rest of the email with a link to see more. Keep that in mind, but more than anything, test your emails to make sure they look good in the real world!

Next, let's bootstrap a console command that will send some emails! It turns out that sending emails in a console command requires an extra trick.

Leave a comment!

23
Login or Register to join the conversation
Mohamed Avatar
Mohamed Avatar Mohamed | posted 2 years ago

Hi

Excellent course! I have learned lots of new things!

Isn't it better to use '*.inky.twig' instead of '*.html.twig'? Personally I think it's pretty straight forward to understand it's not the regular standard html. Anyway not an issue though!

Thank you!

2 Reply

Hey zpine!

Ah, cool idea! Honestly, it never occurred to me. But yes, I think that's a perfectly appropriate idea - I like it!

Cheers!

2 Reply

It took some time to get this to work with my php7.4 running on linux alpine. Turns out the xsl extension needed to be installed.

I made a docker image if anyone has the same problem codebuds/php:prod-1.0.1

1 Reply

Hey Dennis de Best

Thanks for sharing it with others. Cheers!

Reply
Cesar Avatar

Hi guys! I really like this tutorial about Mailer because I was having a lot of mess coding emails. However, I have the same issue of other student in the comments, my emails are not responsive using Ink and Mailtrap. I had to fix manually the style labels. Do you have another recommendation of tool or framework for this? Thanks in advance.

Reply

Hey Cesar,

Hm, the Inky description here is saying:

> Inky is an HTML-based templating language that converts simple HTML into complex, responsive email-ready HTML

So as you see it should be "responsive email-ready HTML". I'd recommend you to double-check with their docs first to make sure you
re not missing anything. Then, if it does not help, try to upgrade to the latest version of it. If you still have this problem, probably try to open an issue in the original repository, maybe someone from maintainers would have an idea what's wrong in your case.

I can suggest you to look at this blog post from Mailtrap if you're looking for alternatives: https://mailtrap.io/blog/be... - but alternatives do not have such a smooth integration with Twig unfortunately.

I hope this helps!

Cheers!

Reply

Hey kiuega!

Hmm. To be honest, I'm not sure what's going on there :/. This IS one of the things that Foundation/Inky is supposed to take care of, but to be honest, I don't know much about how it works behind the scenes (and I'm not the person here at SymfonyCasts that works on the email portion). What happens if you try to send and check on a real mobile email client? I'm a bit dubious about how Mailtrap is showing it... I would normally think that a mail client would reduce the size show the entire email in the frame. \

Cheers!

Reply
Kiuega Avatar

Hello and sorry for the delay in response, I had completely forgotten! So I just checked, and indeed, if I send to a real email address, it works as it should! On the other hand, the email is sent in the "spam" folder, it's a shame

Reply

Hey @kieuga!

Ah, thanks for following-up - I was wondering about this!

> On the other hand, the email is sent in the "spam" folder, it's a shame

That is a shame :). "Deliverability" (the science of not getting your emails sent to SPAM) is a totally different topic, but one that we cover in good detail a bit later in the tutorial - https://symfonycasts.com/sc...

Cheers!

1 Reply
Roman P. Avatar
Roman P. Avatar Roman P. | posted 2 years ago

wassup, guys!
pls add a note of requirement to install (activate) XSL extension in local env to make inky work
I've spent a half-hour to understand what's wrong when got an err

Reply

Hey Bagar,

Thanks for reporting it! Hm, wasn't error message clear enough? Did you get that error on executing?

$ composer require twig/inky-extra

Because in package dependencies it clearly shows that you need to have XSL extension, otherwise I suppose this package won't be even installed. Just want to clarify things here.

Cheers!

Reply
Roman P. Avatar

Hy Victor!

I not sure that the XSL-ext is required

https://github.com/twigphp/...

I installed the package without any err but then I tried to get a result on the page I got an unobvious error message. Then I've been looking for it in SO and understood I need to install XSL

Reply

Hey Bagar,

Hm, interesting. OK, thanks for additional clarification on it, I think note would help here.

Cheers!

Reply
Roman P. Avatar

👍🏻

Reply

Hey Bagar Davidyan

Thanks for letting us know about it. We'll add a note to the tutorial

Cheers!

Reply
Nick F. Avatar
Nick F. Avatar Nick F. | posted 2 years ago

You should also note that you may need to enable the .xsl extension in PHP .ini when you are running the composer require inky command

Reply

Hey yuuzaaneemu!

Ah, that's true! A package downstream requires it - so you would get a Composer error if you're missing it. Is that what you saw? Or did it (somehow) allow you to download the package and you got an error later?

Cheers!

Reply
mr_d Avatar

Hi team,
Isn't going to be resource consuming to transform inky to html and inline css on every email send?
What are the benefits except for keeping the inky files within the same version control?
I mean, wouldn't it be better to use production exports of the email templates instead?

Greets!

Reply

Hey Donchev,

Hm, good question! But what exactly do you mean about "to use production exports of the email templates instead"? Yes, you can always make it faster by doing some kind of optimization (sometimes probably crazy complex optimization?), but does it worth it? And probably it depends! It depends on *your* project and on how many and how often you send your emails. If you really send a lot, probably it would be better for you. But if you send rare, or send not so many emails at once - probably that level of complexity would be worse for you, because you would need to maintain that complexity in the future. Anyway, you can always profile and measure things, and see if you need some optimization in this spot or you just don't care.

As always, using simple "if-else" router is always faster than using Symfony Router, dispute the fact that Symfony Router is probably the fastest router written in PHP. But does it mean that you need to rewrite all your routes with simple "if-else" router?.. ;)

I hope this helps!

Cheers!

Reply
mr_d Avatar

Thank You for the reply!

But what exactly do you mean about "to use production exports of the email templates instead"

Currently i am developing my email templates as a separate "foundation for emails" project.
With npm run build I get the production ready files (with css inlined), then I move these file to my Symfony project.

On my current project I send roughly 50-60k emails per month (transactional + newsletters). If I move the inky files to symfony, this will mean that the inky template will be converted and css inlined by twig 50-60k times per month, right?

Reply

Hey donchev!

Ah, yes I understand! We also have used the separate "foundation for emails" project - so I understand what you mean by doing all the transforming and inlining at "compile" time. The short answer is that: if you use the Twig functions, then yes - you will be going through the inlining & inky transformation for *every* email. The benefit of course is simplicity in your code. But there is that performance cost - and I haven't profiled it to know exactly how high it is. Some ability to cache the inlining or inky transformation could be added to the Twig extensions - but it doesn't exist currently. So, my best advice is to: (A) profile to see how big of an issue this is and (if it is) (B) either stick with your current setup or (most performant) send your emails async anyways with Messenger. If you're really worried about performance... the last option is the "best" because even if you aren't doing this processing for every email, sending emails still involves a network request. If you can make it async, obviously, that will have the best page speed result.

Anyways, great question! Let me know if it makes sense.

Cheers!

Reply
mr_d Avatar

Thanks for the reply!
I will implement messenger into my project regardless of the email template - because it's nice ferature. But probably will keep the email templates as a separate project and use only the HTML files in Symfony.
:)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4
    }
}