├── __templates ├── partials │ ├── empty-text.hbs │ ├── preheader.hbs │ └── content.hbs └── index.tpl ├── .gitignore ├── gulpfile.js ├── gulptasks ├── core │ ├── core-variant-list.js │ ├── core-errors.js │ ├── core-clean.js │ ├── core-watch.js │ ├── core-watch-notification.js │ ├── core-paths.js │ ├── core-templates.js │ └── core-mjml.js ├── default.js ├── build.js ├── variants │ ├── variants-build.js │ └── variants-prepare.js └── style │ ├── style-inject.js │ └── style-compile.js ├── .stylelintrc ├── __styles └── source-stylesheet.scss ├── package.json └── README.md /__templates/partials/empty-text.hbs: -------------------------------------------------------------------------------- 1 |
 
2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.yml 3 | __templates/variants 4 | dist 5 | styles 6 | *.log 7 | -------------------------------------------------------------------------------- /__templates/partials/preheader.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{_d.preheader}} 3 | 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TASKS_DIR = require( 'require-dir' )( './gulptasks', { recurse: true } ) 4 | -------------------------------------------------------------------------------- /gulptasks/core/core-variant-list.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EMAIL_VARIANTS = { 4 | 'confirm-subscripton': {} 5 | , 'fragment': {} 6 | } 7 | 8 | module.exports = EMAIL_VARIANTS 9 | -------------------------------------------------------------------------------- /__templates/partials/content.hbs: -------------------------------------------------------------------------------- 1 | {{_d.title}} 2 | 3 | Some content. 4 | The produced HTML can also host its own template variable: 5 | \{{anotherPostProductionVariable}} easily tag along with {{_d.staticContent}} prerendered variables. 6 | 7 | -------------------------------------------------------------------------------- /gulptasks/default.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , SEQUENCE = require( 'gulp-sequence' ).use( GULP ) 5 | 6 | let defaultGulpTask = () => SEQUENCE( 'build', 'core-watch' )() 7 | 8 | GULP.start( 'build') 9 | 10 | GULP.task( 11 | 'default' 12 | , defaultGulpTask 13 | ) 14 | module.exports = defaultGulpTask 15 | -------------------------------------------------------------------------------- /gulptasks/core/core-errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GULP = require( 'gulp' ) 4 | , GUTIL = require( 'gulp-util' ) 5 | 6 | let onError = function( taskName, err ){ 7 | GUTIL.log( GUTIL.colors.red( 'ERROR', taskName ), err); 8 | this.emit( 'end', new GUTIL.PluginError( taskName, err, { showStack: true } ) ); 9 | } 10 | 11 | module.exports = onError 12 | -------------------------------------------------------------------------------- /gulptasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , SEQUENCE = require( 'gulp-sequence' ) 5 | 6 | let build = function(){ 7 | return SEQUENCE( 8 | 'core-clean' 9 | , 'core-watch-notification' 10 | , 'variants-prepare' 11 | , 'style-compile' 12 | , 'style-inject' 13 | , 'variants-build' 14 | )() 15 | } 16 | 17 | GULP.task( 18 | 'build' 19 | , build 20 | ) 21 | module.exports = build 22 | -------------------------------------------------------------------------------- /gulptasks/core/core-clean.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , DEL = require( 'del' ) 5 | , PATHS = require( './core-paths' ) 6 | 7 | let cleanUp = function(){ 8 | console.log( '\n\u001b[38;5;202mDeleting…\u001b[0m') 9 | return DEL([ 10 | `${PATHS.dir.variants.variants}**/*` 11 | , `${PATHS.dir.variants.dist}*` 12 | , `${PATHS.dir.styles.dist}*` 13 | ]) 14 | } 15 | 16 | GULP.task( 'core-clean', cleanUp ) 17 | 18 | // module.exports = [ 'core'] 19 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-no-empty": null, 4 | "color-no-invalid-hex": true, 5 | "declaration-colon-space-after": "always", 6 | "indentation": [ 7 | 2, 8 | { 9 | "except": ["value"], 10 | "indentInsideParens": ["once-at-root-twice-in-block"] 11 | } 12 | ], 13 | "max-empty-lines": 2, 14 | "unit-whitelist": [ 15 | "em" 16 | , "rem" 17 | , "%" 18 | , "px" 19 | , "s" 20 | , "vw" 21 | , "vh" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gulptasks/core/core-watch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , PATHS = require( './core-paths' ) 5 | , FILES_TO_WATCH_OUT = [ 6 | `${PATHS.dir.styles.source}**/*+(${PATHS.file.lessStylesheets}|${PATHS.file.sassStylesheets})` 7 | , `${PATHS.dir.variants.source}**/*${PATHS.file.template}` 8 | ] 9 | 10 | let watchAll = () => GULP.watch( FILES_TO_WATCH_OUT, [ 'build' ], [ 'change' ] ) // https://github.com/floatdrop/gulp-watch#api 11 | 12 | GULP.task( 'core-watch', watchAll ) 13 | module.export = watchAll 14 | -------------------------------------------------------------------------------- /gulptasks/core/core-watch-notification.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , PATHS = require( './core-paths' ) 5 | , FILES_TO_WATCH_OUT = [ 6 | `${PATHS.dir.styles.source}**/*${PATHS.file.sourceStylesheet}` 7 | , `${PATHS.dir.variants.source}**/*${PATHS.file.template}` 8 | ] 9 | 10 | let watchNotification = function(){ 11 | console.log( '\n\u001b[38;5;202;1m( ͡° ͜ʖ ͡°) WATCHED\n\u001b[0m') 12 | return true 13 | } 14 | 15 | GULP.task( 'core-watch-notification', watchNotification ) 16 | 17 | module.export = watchNotification 18 | -------------------------------------------------------------------------------- /__styles/source-stylesheet.scss: -------------------------------------------------------------------------------- 1 | mj-text, mj-list, mj-button, mj-table, .is-heading{ 2 | font-family: 'Roboto Condensed', 'Roboto', sans-serif; 3 | color: #333; 4 | } 5 | 6 | .theme-forest{ 7 | background-color: #2FDFAA; 8 | } 9 | 10 | .theme-tomato{ 11 | background-color: tomato; 12 | } 13 | 14 | mj-section{ 15 | full-width: "full-width"; 16 | padding: 0; 17 | } 18 | 19 | mj-column{ 20 | padding-left: 0; 21 | padding-right: 0; 22 | background-color: #f2f2f2; 23 | } 24 | 25 | mj-text{ 26 | padding: 0; 27 | } 28 | 29 | .is-empty{ 30 | padding: 0; 31 | font-size: 0; 32 | line-height: 0; 33 | } 34 | -------------------------------------------------------------------------------- /gulptasks/core/core-paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PATHS = { 4 | dir: { 5 | base: './' 6 | , variants: { 7 | source: './__templates/' 8 | , partials: './__templates/partials/' 9 | , variants: './__templates/variants/' 10 | , dist: './dist/' 11 | } 12 | , styles: { 13 | source: './__styles/' 14 | , dist: './styles/' 15 | } 16 | } 17 | , file: { 18 | template: '.+(tpl|hbs)' 19 | , variant: '.mjml' 20 | , lessStylesheets: '.less' 21 | , sassStylesheets: '.scss' 22 | , stylesheet: '.css' 23 | , production: '.html' 24 | } 25 | } 26 | 27 | module.exports = PATHS; 28 | -------------------------------------------------------------------------------- /gulptasks/core/core-templates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let templates = function(){ 4 | const TEMPLATE_DATA = [ 5 | { 6 | name: 'email-A' 7 | , _d: { 8 | title: 'My title A' 9 | , preheader: 'A catchy headline that\'ll boost up your opens rate' 10 | , staticContent: 'any' 11 | , theme: 'forest' 12 | } 13 | } 14 | , { 15 | name: 'email-B' 16 | , _d: { 17 | title: 'Some random title B' 18 | , preheader: 'Preheader headline' 19 | , staticContent: 'all' 20 | , theme: 'tomato' 21 | } 22 | } 23 | ] 24 | 25 | return { 26 | data: TEMPLATE_DATA 27 | } 28 | } 29 | 30 | module.exports = templates(); 31 | -------------------------------------------------------------------------------- /__templates/index.tpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | {{> preheader}} 18 | 19 | 20 | 21 | 22 | 23 | {{> content}} 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /gulptasks/variants/variants-build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , MJML_ENGINE = require( 'mjml' ) 5 | , MJML = require( 'gulp-mjml' ) 6 | , HTML_CLEAN = require( 'gulp-htmlclean' ) 7 | , HTML_MIN = require( 'gulp-htmlmin' ) 8 | , RENAME = require( 'gulp-rename' ) 9 | , PATHS = require( '../core/core-paths' ) 10 | , ON_ERROR = require( '../core/core-errors' ) 11 | 12 | let buildVariant = function(){ 13 | console.log( '\n\u001b[38;5;120;1m> MJML \u001b[0m\u001b[38;5;115m Building variant…\u001b[0m') 14 | 15 | return GULP.src( `${PATHS.dir.variants.variants}**/*${PATHS.file.variant}` ) 16 | .pipe( MJML( MJML_ENGINE ) ) 17 | .pipe( HTML_CLEAN() ) 18 | .pipe( HTML_MIN({ 19 | maxLineLength: 960 20 | }) ) 21 | .pipe( RENAME({ 22 | extname: PATHS.file.production 23 | }) ) 24 | .pipe( GULP.dest( PATHS.dir.variants.dist ) ) 25 | } 26 | 27 | GULP.task( 'variants-build', buildVariant ) 28 | 29 | module.exports = buildVariant 30 | -------------------------------------------------------------------------------- /gulptasks/style/style-inject.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , SMOOSHER = require( 'gulp-smoosher' ) 5 | , INLINE_CSS = require( 'gulp-inline-css' ) 6 | , MJML_DICTIONARY = require( '../core/core-mjml' ) 7 | , PATHS = require( '../core/core-paths' ) 8 | , ON_ERROR = require( '../core/core-errors' ) 9 | 10 | 11 | let injectStyleInVariants = function(){ 12 | console.log( '\n\u001b[38;5;120;1m> INJECTION \u001b[0m\u001b[38;5;115m Embedding rules…\u001b[0m') 13 | let onInjectStyleError = ( err ) => ON_ERROR( 'email:inject-css', err ) 14 | return GULP.src( `${PATHS.dir.variants.variants}*${PATHS.file.variant}` ) 15 | .pipe( SMOOSHER() ) 16 | .pipe( INLINE_CSS({ 17 | applyWidthAttributes : true 18 | , preserveMediaQueries : true 19 | , removeStyleTags : true 20 | , applyAttributesTo : MJML_DICTIONARY 21 | }).on( 'error', onInjectStyleError ) ) 22 | .pipe( GULP.dest( PATHS.dir.variants.variants ) ) 23 | } 24 | 25 | GULP.task( 'style-inject', injectStyleInVariants ) 26 | 27 | module.exports = injectStyleInVariants 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-mjml-hbs-pipeline ", 3 | "version": "0.7.3", 4 | "description": "", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "daylon", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "del": "^3.0.0", 13 | "event-stream": "^4.0.0", 14 | "gulp": "^3.9.1", 15 | "gulp-autoprefixer": "^6.0.0", 16 | "gulp-compile-handlebars": "^0.6.1", 17 | "gulp-csso": "^3.0.0", 18 | "gulp-dest": "^0.2.3", 19 | "gulp-group-css-media-queries": "^1.1.0", 20 | "gulp-htmlclean": "^2.7.6", 21 | "gulp-htmlmin": "^5.0.0", 22 | "gulp-inline-css": "daylon/gulp-inline-css", 23 | "gulp-less": "^4.0.0", 24 | "gulp-mjml": "^3.0.0", 25 | "gulp-purifycss": "^0.2.0", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-sass": "^4.0.0", 28 | "gulp-sequence": "^1.0.0", 29 | "gulp-smoosher": "0.0.9", 30 | "gulp-stylelint": "^8.0.0", 31 | "gulp-util": "^3.0.7", 32 | "mjml": "^4.0.2", 33 | "require-dir": "^1.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gulptasks/variants/variants-prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , EVENT_STREAM = require( 'event-stream' ) 5 | , RENAME = require( 'gulp-rename' ) 6 | , HANDLEBARS = require( 'gulp-compile-handlebars' ) 7 | , PATHS = require( '../core/core-paths' ) 8 | , TEMPLATES = require( '../core/core-templates' ) 9 | , ON_ERROR = require( '../core/core-errors' ) 10 | 11 | 12 | let prepareVariants = function(){ 13 | let variants 14 | , wrapInIEConditional = ( options ) => ( options.fn ? ``: options ) 15 | , tplOptions = { 16 | ignorePartials: true 17 | , batch: [ `${PATHS.dir.variants.partials}` ] 18 | , helpers: { wrapInIEConditional } 19 | } 20 | , renderEntry = function( _entry ){ 21 | console.log( `\t> rendering ${_entry.name}…` ) 22 | 23 | return GULP.src( `${PATHS.dir.variants.source}*${PATHS.file.template}` ) 24 | .pipe( HANDLEBARS( 25 | _entry 26 | , tplOptions 27 | )) 28 | .pipe( RENAME({ 29 | prefix: 'email-' 30 | , basename: _entry.name 31 | , extname: PATHS.file.variant 32 | }) ) 33 | .pipe( GULP.dest( `${PATHS.dir.variants.variants}` ) ) 34 | } 35 | 36 | console.log( '\n\u001b[38;5;120;1m> TPL \u001b[0m\u001b[38;5;115m Preparing all variants…\u001b[0m') 37 | 38 | variants = TEMPLATES.data.map( renderEntry ) 39 | return EVENT_STREAM.merge.apply( null, variants ) 40 | } 41 | 42 | GULP.task( 'variants-prepare', prepareVariants ) 43 | 44 | module.exports = prepareVariants 45 | -------------------------------------------------------------------------------- /gulptasks/style/style-compile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GULP = require( 'gulp' ) 4 | , EVENT_STREAM = require( 'event-stream' ) 5 | , SASS = require( 'gulp-sass' ) 6 | , LESS = require( 'gulp-less' ) 7 | , GROUP_MEDIA_QUERIES = require( 'gulp-group-css-media-queries' ) 8 | , STYLE_LINT = require( 'gulp-stylelint' ) 9 | , AUTOPREFIXER = require( 'gulp-autoprefixer' ) 10 | , CSSO = require( 'gulp-csso' ) 11 | , PURIFY = require( 'gulp-purifycss' ) 12 | , PATHS = require( '../core/core-paths' ) 13 | , ON_ERROR = require( '../core/core-errors' ) 14 | 15 | let compileStylesheets = function(){ 16 | 17 | let autoprefixerOptions = { 18 | browsers: [ 'last 3 versions' ] 19 | , cascade: false 20 | } 21 | , compileAllStylesheets = function(){ 22 | let lessStream = GULP.src( `${PATHS.dir.styles.source}*${PATHS.file.lessStylesheets}` ) 23 | .pipe( STYLE_LINT({ 24 | reporters: [ 25 | {formatter: 'string', console: true} 26 | ] 27 | , syntax: 'less' 28 | }) ) 29 | .pipe( LESS() ) 30 | let sassStream = GULP.src( `${PATHS.dir.styles.source}*${PATHS.file.sassStylesheets}` ) 31 | .pipe( STYLE_LINT({ 32 | reporters: [ 33 | {formatter: 'string', console: true} 34 | ] 35 | , syntax: 'scss' 36 | }) ) 37 | .pipe( SASS() ) 38 | return EVENT_STREAM.merge.apply( null, [ lessStream, sassStream ] ) 39 | } 40 | 41 | console.log( '\n\u001b[38;5;120;1m> STYLESHEET \u001b[0m\u001b[38;5;115m Compiling CSS\u001b[0m') 42 | 43 | return compileAllStylesheets() 44 | .pipe( GROUP_MEDIA_QUERIES() ) 45 | .pipe( AUTOPREFIXER( autoprefixerOptions ) ) 46 | .pipe( CSSO() ) 47 | .pipe( PURIFY( [ `${PATHS.dir.variants.variants}*${PATHS.file.variant}` ] ) ) 48 | .pipe( GULP.dest( `${PATHS.dir.styles.dist}` ) ) 49 | } 50 | 51 | GULP.task( 'style-compile', compileStylesheets ) 52 | 53 | module.exports = compileStylesheets 54 | -------------------------------------------------------------------------------- /gulptasks/core/core-mjml.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MJML_DICTIONARY = { 4 | 'table, tr, td, img': [ 5 | 'width' 6 | , 'height' 7 | , 'align' 8 | ] 9 | , 'table, td': [ 'bgcolor' ] 10 | , 'table': [ 11 | , 'cellpadding' 12 | , 'cellspacing' 13 | ] 14 | , 'td': [ 15 | 'valign' 16 | ] 17 | , 'img': [ 18 | 'border' 19 | ] 20 | , 'v\\:rect': [ 21 | 'width' 22 | , 'height' 23 | , 'color' 24 | , 'strokecolor' 25 | , 'arcsize' 26 | ] 27 | , 'mj-body, mj-container, mj-section, mj-column, mj-button': [ 'background-color' ] 28 | , 'mj-container': [ 29 | 'width' 30 | , 'font-size' 31 | , 'background-color' 32 | ] 33 | , 'mj-section': [ 34 | 'full-width' 35 | , 'background-color' 36 | , 'background-url' 37 | , 'background-repeat' 38 | , 'background-size' 39 | , 'vertical-align' 40 | , 'text-align' 41 | , 'padding' 42 | , 'padding-top' 43 | , 'padding-bottom' 44 | , 'padding-left' 45 | , 'padding-right' 46 | ] 47 | , 'mj-column': [ 48 | 'width' 49 | , 'vertical-align' 50 | , 'background-color' 51 | ] 52 | , 'mj-text': [ 53 | 'color' 54 | , 'font-family' 55 | , 'font-size' 56 | , 'font-style' 57 | , 'font-weight' 58 | , 'line-height' 59 | , 'text-decoration' 60 | , 'align' 61 | , 'container-background-color' 62 | , 'padding' 63 | , 'padding-top' 64 | , 'padding-bottom' 65 | , 'padding-left' 66 | , 'padding-right' 67 | ] 68 | , 'mj-button': [ 69 | 'container-background-color' 70 | , 'border-radius' 71 | , 'font-style' 72 | , 'font-size' 73 | , 'font-weight' 74 | , 'font-family' 75 | , 'color' 76 | , 'border' 77 | , 'text-decoration' 78 | , 'align' 79 | , 'vertical-align' 80 | , 'href' 81 | , 'padding' 82 | , 'padding-top' 83 | , 'padding-bottom' 84 | , 'padding-left' 85 | , 'padding-right' 86 | ] 87 | , 'mj-table': [ 88 | 'color' 89 | , 'font-family' 90 | , 'font-size' 91 | , 'line-height' 92 | , 'container-background-color' 93 | , 'padding' 94 | , 'width' 95 | , 'table-layout' 96 | ] 97 | } 98 | 99 | module.exports = MJML_DICTIONARY 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hbs-mjml-seed 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/Daylon/hbs-mjml-seed.svg)](https://greenkeeper.io/) 4 | Gulp pipeline example dedicated to build HTML emails. 5 | 6 | ## Disclaimer 7 | _This is a mere prototype. Use it at your own risks. 8 | This was intended to provide a faster to build a complete set of emails with custom parts and styling._ 9 | 10 | ## Usage 11 | 12 | ```sh 13 | $ git clone https://github.com/Daylon/hbs-mjml-seed.git 14 | $ cd hbs-mjml-seed 15 | $ gulp 16 | ``` 17 | 18 | This pipeline use `MJML` for custom email markup, `Handlebars` for templating and `SASS` for styling. Please refer to their own documentation and/or repo for help: 19 | - [mjml.io](https://mjml.io/); 20 | - [HandlebarsJS](http://handlebarsjs.com/); 21 | - [Sass](http://sass-lang.com/). 22 | 23 | ## Let's keep MJML and CSS separated, shall we? 24 | 25 | ### Rationale 26 | 27 | The same way that making HTML for email can be tedious, having to tweak CSS rules attributes by hand can be cumbersome as well. For these reasons, I didn't want to regress from my previous setup _but_ MJML is pretty much the only solution (at the time of this writing) to provide a markup capable of removing noise from your HMTL templates. 28 | 29 | To achieve this, this pipeline leverages a fork from the fantastic lib `inline-css` by (and, in the process, `gulp-inline-css`; same author). [Here's the pull request, if you wish to weigh in](https://github.com/jonkemp/inline-css/pull/40). While this lib neatly converts class related css rules into style attributes, I tweak it up to support any custom rule, including _exotic_ ones (_e.g._ `full-width`). 30 | 31 | Interesting side-effect: VML style attributes (for, say, call-to-actions) can now be set, so you won't have to worry about having discrepancies between templates and style rules when modifying your call-to-actions. 32 | 33 | ### Can I change these definitions? 34 | 35 | Yes, you can. Open up `/gulptasks/core/core-mjml.js` and start adding properties. By default, any added rule will become an attribute. 36 | 37 | ## Templating 38 | 39 | ### Why using Handlebars 40 | 41 | Handlebars allows us to use a neat built-in feature that make it skip partials declaration on first pass. 42 | 43 | `\{{>partialName}}` 44 | ([Source](https://stackoverflow.com/questions/22249235/render-double-curly-brackets-inside-handlebars-partial)) 45 | 46 | This way, a single handlebars template can produce multiple files, based on the first layer of variables, ready to be fed with real production data. 47 | 48 | ### Use case: theming 49 | 50 | #### First round —Gulp task 51 | 52 | Given a single `index.tpl`, we loop through an array of JSON data defined in `/gulptasks/core/core-templates.js`. 53 | Each index sets up what is static: pre-header, default unsuscribe link, invoke the correct partial, etc. 54 | 55 | #### Second pass —on server 56 | 57 | The produced new templates (one per theme) can now be used for a specific scenario or userbase sampling. 58 | 59 | ### Shortcomings 60 | 61 | **MJML** being a young project, some elements can still be buggy or lack optimizations; you may have to mix up both MJML and raw html. 62 | --------------------------------------------------------------------------------