If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur 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.
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.
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!
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.
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.
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.
Hey Mohamed!
Ah, cool idea! Honestly, it never occurred to me. But yes, I think that's a perfectly appropriate idea - I like it!
Cheers!
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
What would be a best practice to render the inky templates locally? Right now I have to resend the mails every time I change something. While creating the mere template this is a bit complicated. Thanks!
Hi @Eric,
Probably the most complicated thing here is dummy data for email, because rendering is simple, just create a controller which will receive template name and return you a rendered page. You can embed this page with iframe on some admin page, and with little JS help change templates you need to watch =)
Of course you will need some dummy data manager to populate email vars, we created an YAML config file for every email, with predefined data.
Cheers and happy coding!
My first try was indeed a simple Controller rendering the template. But I make use of the email variable in the template. Is there a way to create this variable in a Controller? I tried passing a new TemplatedEmail but that didn't help much... :)
It's not very complex solution, you need some place to predefine this variables, for example yaml
// emails.yaml
emails:
firstTemplate:
var1: 'some value'
vae2: 'some value'
and so on...then you need a service which will read this vars as array and pass it to twig on template rendering
cheers!
Does it have to be external predefined variables? Or is there a way to create all necessary vars in the controller and pass them via the render-function? That way one could keep everything close together.
It feels like there is some basic concept involved I am currently missing.
I prefer an external file with variables, but the solution may be complex here, because it depends on what data you need to pass to emails, is it simple vars (strings, arrays) or it's some complex models. In both ways you create a service for example DummyEmailDataManager
in that service you read your variables data from file or any other place you like and in controller you should call something like $this->render('emails/'.$emailTemplate.'.html.twig', $dummnyEmailDataMenager->loadVariablesFor($emailTemplate));
and that's all sorry can't provide you exact implementation of variables loader, but it's doable and it's not very hard
cheers
Hey @Eric!
I think you just asked one of those "nobody does this well" questions, lol. We created a system internally to handle this in our admin - an area where you can select from an email, and see it rendered. It's SUPER convenient, but it did take a bit of time to setup. Let me bing Vladimir who worked on it - he can add some extra details about how our system looks :).
Cheers!
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.
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!
Hello, I just noticed that in reality, the email is not responsive. On mailTrap, when I click on the "mobile phone" icon to see the mobile view, the rendering is not responsive, look:
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!
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
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!
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
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!
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
Hey Bagar,
Hm, interesting. OK, thanks for additional clarification on it, I think note would help here.
Cheers!
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
Hey Nick F.!
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!
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!
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!
Thank You for the reply!
<blockquote>But what exactly do you mean about "to use production exports of the email templates instead"</blockquote>
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?
Hey mr_d!
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!
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.
:)
// 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
}
}
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!