Jigsaw-based PHP build system for rapid HTML email development with Tailwind CSS.
css-mqpacker
as a PostCSS plugin instead of interacting with the filesystem (246d7826cd9d1d27ad3313d156bf0a8a12044a64)email-remove-unused-css
dependency to 4.6.0 (6c9b8dca8238086bd9d36ec9f5d14673613cb894)extraBorderUtilities
Tailwind plugin (d6d187db1f561a18d90773fab6a14f8b01a106e8)package.json
to tested releases.This release updates Tailwind CSS, fixes a few bugs and adds support for using hover-
utilities without a @media
query.
googleFonts
key format
lang=""
for webmail
Until now, you couldn't have used a utility like hover-text-green
, because Juice would have removed the CSS selector as part of the removeStyleTags
feature. The common solution was to 'hide' the selector inside a @media
query, so Juice would leave it alone - i.e. all-hover-text-green
.
This release updates Juice to version 5.0.0
, which fixes the inconvenience.
googleFonts
key formatDue to a recently introduced type check for the googleFonts
Front Matter variable, using a simple string was not working anymore. Updated the example templates and docs to use an array.
<td>
and <th>
were being reset to use the font-sans
utility class. This meant that, if you wanted to use a different font family on an element, you needed to override this reset either globally, or by adding a font-...
utility on every table cell.
This release removes the global font family reset, and adds it on the wrapper table instead.
lang=""
for webmailWebmail clients, such as Gmail, completely remove your <html>
and <body>
tags. Since the lang=""
attribute is being set on the <html>
tag, this resulted in a situation where a screenreader could not use the language we specified, for correct pronunciation.
This release adds the lang=""
attribute on elements in the included templates, too. When creating a new template, it is highly advisable that you add this, just like in the examples.
Updated to Tailwind CSS 0.6.6
, which fixes an issue where units were stripped from zero value properties, and promotes shadowLookup
from experiment to official feature.
There is also a borderCollapse: [],
module in the Tailwind config, which you can use if you ever need variants for border-collapse
on tables.
#outlook a
removedTesting in Outlook 2013/2016 shows that #outlook a {padding: 0;}
isn't needed for the 'view in browser' bar to show up, so it has been removed.
outlook-conditional.blade.php
now checks if there's a Blade section named mso-css
in your template. If it finds it (and it's not empty), it will use its contents in the <style>
tag from the <!--[if mso]>
conditional in a layout's <head>
.
For example, say we wanted a different font stack for this email instead of the default one (that is using Segoe UI), as well as some extra MSO-specific table styling:
// custom-mso-css.blade.md
---
title: Custom MSO styling example
---
@section('mso-css')
table {border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt;}
td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: Helvetica, Arial, sans-serif; mso-line-height-rule: exactly;}
@endsection
<table ...
Note: whatever you have in this section will replace the default CSS for Outlook that Maizzle comes with.
Removed the custom Parsedown parser container binding, so that Parsedown Extra can actually work.
Jigsaw uses Parsedown Extra since v1.2.0, so this was unnecessary.
Maizzle now exposes email-remove-unused-css
's backend
option in your config, which you can use to define heads/tails pairs of characters that wrap your CSS class names, so the cleanup library doesn't remove those classes.
A {{
}}
pair is included - useful if you're using Blade to further process an email after Maizzle compiles it:
'cleanup' => [
'removeUnusedCss' => [
// ...
'backend' => [
[
'heads' => "{{",
'tails' => "}}",
],
],
// ...
],
],
Imagine foo = 'odd'
somewhere in your application. Previously, if you wanted to output Blade from Maizzle:
<tr class="@{{ foo }}">
... you would have ended up with this in the compiled HTML:
<!-- foo is removed ? -->
<tr class="{{ }}">
Having {{
and }}
defined as heads/tails in the backend
option, the compiled HTML will now look like this:
<!-- we can now use `foo` ? -->
<tr class="{{ foo }}">
extraBorderUtilities
plugin by defaultThe require
for this Tailwind CSS plugin is now commented out by default, as it's not really needed in most cases. Simply uncomment this line in tailwind.js
, if you need it:
// require('./tailwind/plugins/extraBorderUtilities'),
This release adds some cool new features, and fixes some minor issues.
Specifically, it:
removeStyleTags
from the configs
@env
Blade directive
Maizzle now uses PostCSS instead of Sass.
It's faster, avoids preprocessor gotchas (like a Python dependency for node-sass
in Windows, or not being able to use Tailwind's @apply
with !important
), and makes more sense considering the tooling being used.
Maizzle can now automatically generate plaintext versions of your emails, with the help of string-strip-html.
A new 'plaintext' => false,
option has been added to the configs. When you set it to true
, Maizzle will generate a plaintext version for every template. The .txt
file will be placed in the same directory as the HTML it's based on, and it will also have the same name.
Plaintext is enabled by default for production builds, in config.production.php
.
Maizzle now uses postcss-merge-longhand to rewrite your CSS padding
, margin
, and border
in shorthand-form, where possible. Because Tailwind classes mostly map one-to-one with CSS properties, this won't have any effect on them. Instead, it's very useful when you extract components with Tailwind's @apply
.
For example, considering this template:
---
title: Confirm your email
preheader: Please verify your email address with us
bodyClasses: bg-grey-light
---
<div class="col">test</div>
... let's extract a .col
class in an imaginary source/_styles/components.css
:
.col {
@apply py-8 px-4;
}
Previously, that would have given us this:
<div style="padding-top: 8px; padding-bottom: 8px; padding-left: 4px; padding-right: 4px;">test</div>
Now, thanks to postcss-merge-longhand
, we get this:
<div style="padding: 8px 4px;">test</div>
As mentioned, this works for padding
, margin
, and border
. Using shorthand CSS for these is well supported in email clients and will make your HTML lighter, but the shorthand border is particularly useful because it's the only way Outlook will render it properly.
Note: the library won't assume any missing values. For padding and margin, the class needs to specify properties for all four sides. For borders, see the example below.
To get the PostCSS plugin to rewrite your CSS borders in shorthand-form, you need to specify all these:
When extracting a component class in Tailwind, that means you can do something like this:
.my-border {
@apply border border-solid border-blue;
}
... which, following the example above, will result in this shorthand form:
<div style="border: 1px solid #3490dc;">test</div>
You can now set a doctype for your emails, either globally or per-template.
There is a new doctype
option in config.php
:
'doctype' => 'html',
This is used in the default layout(s) that Maizzle comes with:
<!DOCTYPE {!! $page->doctype ?? 'html' !!}>
Note: the {!! !!}
Blade statement is used instead of {{ }}
. This is to prevent the value from being escaped - helps if you use an older doctype, like in the front matter example below.
The global variable set in config.php
can be overridden at a template level, just like any other option:
---
doctype: html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
title: Confirm your email
preheader: Please verify your email address with us
---
Maizzle's default layouts now include a @stack('head')
directive (see Blade docs).
This allows you to use @push('head')
and @prepend('head')
in your Blade email files, to add anything you'd like right before the closing </head>
tag.
You could use this to add custom <style>
blocks on a per-template basis. Email client-specific CSS resets make for a good example:
source/emails/confirm-email.blade.md
---
// front matter needs to be first!
---
@push('head')
<style data-embed>
a[x-apple-data-detectors] {color: inherit; text-decoration: none;}
</style>
@endpush
<table ...
You can push multiple stacks, too:
---
...
---
@push('head')
<style data-embed>
a[x-apple-data-detectors] {color: inherit; text-decoration: none;}
</style>
@endpush
@push('head')
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<meta name="format-detection" content="address=no">
<meta name="format-detection" content="email=no">
@endpush
Of course, you can use it for anything you'd like to have right before </head>
, for this template only: additional meta tags, Outlook conditionals, custom ESP code - you name it!
Notes:
When @push
ing a <style>
, you need to add a data-embed
attribute on it, so that the inliner leaves it alone.
You cannot use Tailwind CSS @
-rules here. Tailwind is processed before this file is, so using something like @apply
will have no effect.
Maizzle now exposes juice.styleToAttribute
to your configs, so you can define which CSS properties should be also added as HTML attributes to table elements that Juice knows about.
This is opt-out by design, and will only have effect if inlining is enabled.
You can customise the styleToAttribute
key under inlineCSS
, mapping CSS properties to HTML attributes.
For example this:
// config.production.php
'transformers' => [
'inlineCSS' => [
'enabled' => true,
'styleToAttribute' => [
'background-color' => 'bgcolor',
],
],
...
],
... will transform this:
<!-- using Tailwind class here... -->
<table class="bg-white">
...
</table>
... to this:
<table style="background-color: #ffffff;" bgcolor="#ffffff">
...
</table>
Opt-out by design
By default, Maizzle configs include the styleToAttribute
key, and populate it with Juice's defaults.
This is on purpose, for better email client compatibility.
However, if you specify a single mapping, like we did in the example above, Juice will only convert that to an attribute, keeping any other property as inline CSS-only.
Maizzle now exposes juice.codeBlocks
to your config, so that you can define additional fenced code blocks that you need Juice to ignore when inlining.
codeBlocks
must be an array of items matching juice.codeBlocks
dictionary format.
For example:
// config.production.php
'inlineCSS' => [
'enabled' => true,
'codeBlocks' => [
'ASP' => ['start' => '<%@', 'end' => '%>'],
],
],
The above will add the ASP
key to Juice's juice.codeBlocks
dictionary, so that Juice doesn't treat something like <%@ page language="C#" %>
as an HTML tag that needs to have CSS inlined.
There is a new applySizeAttribute
key under the inlineCSS
options:
'inlineCSS' => [
'enabled' => true,
// ...
'applySizeAttribute' => [
'width' => ['TABLE', 'TD', 'TH', 'IMG', 'VIDEO'],
'height' => ['TABLE', 'TD', 'TH', 'IMG', 'VIDEO'],
],
],
Under the width
and height
keys, you can specify an array of elements that should receive width=""
and height=""
attributes. These elements will be passed to the Juice inliner, which will duplicate any inline width and height CSS rules it finds as HTML attributes, but only for those elements.
The new excludedProperties
option allows you to define an array of CSS properties that should be excluded from the CSS inlining process by Juice:
'inlineCSS' => [
'enabled' => true,
// ...
'excludedProperties' => [],
],
Maizzle sets this as an empty array in the production environment configs.
Property names are considered unique, so you need to specify each one you'd like to exclude. For example:
'excludedProperties' => ['padding', 'padding-left'],
Note: corresponding classes that they're derived from (i.e. pl-2
) will still be removed from your HTML if removeUnusedCss
is enabled. To prevent this, you could add 'removeStyleTags' => false,
in the inlineCSS
options array.
removeStyleTags
has been removed from config.*.php
. It was set to true
in all three config files, and the option it's for also falls back to true
in the post-processing script, so it was redundant.
However, you can still add it to your config, to prevent Juice from removing possibly-inlined CSS from your <style></style>
tags:
//config.staging.php
'transformers' => [
// [...]
'inlineCSS' => [
'enabled' => true,
'removeStyleTags' => false,
],
],
preferAttributeWidth
The preferAttributeWidth
option has been renamed and refactored to apply to both widths and heights:
'cleanup' => [
// [...]
'keepOnlyAttributeSizes' => [
'width' => ['TABLE', 'TD', 'TH', 'IMG', 'VIDEO'],
'height' => ['TABLE', 'TD', 'TH', 'IMG', 'VIDEO'],
],
],
You can now specify for which elements should the inline CSS width
and height
be removed. As you can see, Maizzle defaults to the same elements as in the applySizeAttribute
option.
Removing the entire keepOnlyAttributeSizes
key will leave any inline CSS width/height untouched.
Providing an empty array to one of its keys will not remove the corresponding CSS property for that key on any element. So if you do this:
'cleanup' => [
// [...]
'keepOnlyAttributeSizes' => [
'width' => [],
'height' => ['TABLE', 'TD', 'TH', 'IMG', 'VIDEO'],
],
],
... then inline CSS width
will be left untouched, while inline CSS height
will be removed for all those elements in the array.
@env
Blade directiveThere is a new @env($environment)
Blade directive (inspired by the one in the Laravel docs), which you can use to output content only when building for a specific environment.
For example, to output some text only for production emails, you can do this:
source/emails/example.blade.md
---
front matter always first!
---
<table class="w-full" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" class="px-8">
@env('production')
This text will be visible only when running `npm run production`
@endenv
</td>
</tr>
</table>
Of course, you can do an if/else statement:
...
<table class="w-full" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center" class="px-8">
@env('development')
Show this if we do `npm run dev` or `npm run watch`
@elseenv('production')
But when we do `npm run prod`, show this instead
@endenv
</td>
</tr>
</table>
Note: $environment
must match one of the NODE_ENV
environment variables set in package.json
.
There is a new custom-utilities.css
inside the source/_styles
directory. Use this file to register any utility classes that Tailwind doesn't provide yet. This release includes extra table-display utilities, useful for example if you need to do reverse column stacking, plus a .mso-leading-exactly
utility:
.mso-leading-exactly {
mso-line-height-rule: exactly;
}
@responsive {
.table-row-group {
display: table-row-group;
}
.table-header-group {
display: table-header-group;
}
.table-footer-group {
display: table-footer-group;
}
.table-column-group {
display: table-column-group;
}
.table-column {
display: table-column;
}
.table-caption {
display: table-caption;
}
}
There is a new tailwind/plugins
directory, where you can add your Tailwind CSS plugins.
Maizzle now comes with a custom plugin that allows for defining individual border side colors and styles, thanks to @spiltcoffee.
Also included is a plugin that generates CSS background gradients, inspired by @benface's plugin.
You can define the gradients in your tailwind.js
config, just like with any other module:
let gradients = {
'grey-dark': ['#b8c2cc', '#8795a1'],
'red-dark': ['#e3342f', '#cc1f1a'],
'orange-dark': ['#f6993f', '#de751f'],
'yellow-dark': ['#ffed4a', '#f2d024'],
'green-dark': ['#38c172', '#1f9d55'],
'teal-dark': ['#4dc0b5', '#38a89d'],
'blue-dark': ['#3490dc', '#2779bd'],
'indigo-dark': ['#6574cd', '#5661b3'],
'purple-dark': ['#9561e2', '#794acf'],
'pink-dark': ['#f66d9b', '#eb5286'],
}
Those are the default ones Maizzle comes with, and they're based on Tailwind's default color palette.
You can add your own gradients, with as many color stops as you need:
let gradients = {
// ...
'hydrogen': ['#667db6', '#0082c8', '0082c8', '667db6'],
}
If you define a single color instead of an array, the resulting gradient will start from transparent
and move towards the color you defined:
let gradients = {
// ...
'black': '#22292f',
}
... will generate classes like:
.bg-gradient-to-top-black {
background-image: linear-gradient(to top, transparent, #22292f);
}
Of course, the generated classes cover all four directions. With the example above, you'd get:
.bg-gradient-to-top-black {
background-image: linear-gradient(to top, transparent, #22292f);
}
.bg-gradient-to-right-black {
background-image: linear-gradient(to right, transparent, #22292f);
}
.bg-gradient-to-bottom-black {
background-image: linear-gradient(to bottom, transparent, #22292f);
}
.bg-gradient-to-left-black {
background-image: linear-gradient(to left, transparent, #22292f);
}
Just like with any module, you can control which variants are generated. Maizzle only enables the responsive
and hover
variants by default, but you can use any of the variants, or set it to false
to not generate any background gradients:
modules: {
// ...
gradients: ['responsive', 'hover'],
// ...
},
source/_styles
directory structure:
extra.css
is now inserted when Jigsaw builds the files, instead of when post-processing happens. This speeds up the post-processing a bit by not relying on Cheerio@apply
inside extra.css
config.php
googleFonts
to empty arraymerriweather
font stack definition from tailwind.js
typography.css
responsive.css
uglify
option has been renamed to uglifyClassNames
, for clarityThis release exposes new CSS cleanup options in config.php, thanks to v4.2.0
of email-remove-unused-css
.
You now have two new options in removeUnusedCss
:
'enabled' => true,
will remove any HTML comments in your compiled email. Set to false
to not remove any comments.
preserve
can be set to an array of strings. If any comment's opening tag contains any of these strings, that comment will not be removed.
Warning: if using removeHTMLComments
but want to preserve certain comments such as Outlook conditionals, you need to explicit about both the starting and ending comment in the preserve
strings, because they are treated as two separate comments.
Basically, don't do this:
// will remove your closing <![endif]--> comment
'preserve' => ['if', 'mso', 'ie'],
Instead, do this:
// will preserve Outlook conditional comments as expected
'preserve' => ['if', 'endif', 'mso', 'ie'],
Maizzle defaults to the email-remove-unused-css
library defaults (['if', 'endif', 'mso', 'ie']
) both in the config.*.php
, and as a fallback: if you enable removeHTMLComments
but don't specify a list of strings (or specify an empty array for preserve
), it will still properly handle Outlook conditionals.
This is a very cool option introduced in email-remove-unused-css v4.2.0
: it renames all your classes, in both HTML and CSS, to be very short (starting from a single character). It's a small but very useful feature, especially for complex layouts where you need to shave off as many KB as possible to avoid Gmail clipping.
For example, this:
<style>
.text-purple-lightest {color: #f3ebff;}
.bg-grey-darkest {color: #3d4852;}
</style>
<table>
<tr>
<td class="bg-grey-darkest">
<p class="text-purple-lightest">Lorem ipsum</p>
</td>
</tr>
</table>
gets transformed to something like this:
<style>
.a {color: #f3ebff;}
.b {color: #3d4852;}
</style>
<table>
<tr>
<td class="b">
<p class="a">Lorem ipsum</p>
</td>
</tr>
</table>
Maizzle enables uglify
by default only in config.production.php
.