├── dist └── .gitkeep ├── src ├── img │ └── logo.png ├── partials │ ├── follow_lee.hbs │ └── components │ │ ├── divider.hbs │ │ ├── chart.hbs │ │ ├── button.hbs │ │ └── notice.hbs ├── data │ ├── default.yml │ ├── pardot.yml │ └── hubspot.yml ├── css │ └── scss │ │ └── default │ │ ├── main.scss │ │ ├── _dividers.scss │ │ ├── _other.scss │ │ ├── _responsive.scss │ │ ├── _notices.scss │ │ ├── _type.scss │ │ ├── _buttons.scss │ │ ├── _global.scss │ │ ├── _config.scss │ │ └── _layout.scss ├── layouts │ └── default.hbs └── emails │ ├── branded.hbs │ ├── transaction.hbs │ └── components.hbs ├── preview ├── img │ ├── phone.png │ └── phone-icon.png ├── scss │ ├── _reset-ish.scss │ ├── preview.scss │ ├── _welcome.scss │ └── _ui.scss ├── _blank.html └── index.ejs ├── grunt ├── open.js ├── clean.js ├── autoprefixer.js ├── express.js ├── assemble.js ├── cdn.js ├── juice.js ├── watch.js ├── aliases.yaml ├── replace.js ├── mailgun.js ├── sass.js └── aws_s3.js ├── .gitignore ├── Gruntfile.js ├── LICENSE ├── package.json ├── server.js └── README.md /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leemunroe/grunt-email-workflow/HEAD/src/img/logo.png -------------------------------------------------------------------------------- /preview/img/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leemunroe/grunt-email-workflow/HEAD/preview/img/phone.png -------------------------------------------------------------------------------- /preview/img/phone-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leemunroe/grunt-email-workflow/HEAD/preview/img/phone-icon.png -------------------------------------------------------------------------------- /src/partials/follow_lee.hbs: -------------------------------------------------------------------------------- 1 | Follow {{{ default.twitter_handle }}} on Twitter -------------------------------------------------------------------------------- /grunt/open.js: -------------------------------------------------------------------------------- 1 | // Browser-based preview task 2 | module.exports = { 3 | preview: { 4 | path: 'http://localhost:4000' 5 | } 6 | } -------------------------------------------------------------------------------- /grunt/clean.js: -------------------------------------------------------------------------------- 1 | // Clean your /dist folder 2 | module.exports = { 3 | clean: ['!<%= paths.dist %>/.gitkeep', '<%= paths.dist %>/**/*'] 4 | }; 5 | -------------------------------------------------------------------------------- /src/data/default.yml: -------------------------------------------------------------------------------- 1 | twitter_url: "http://twitter.com/leemunroe" 2 | twitter_handle: "@leemunroe" 3 | unsubscribe_url: "http://giphy.com/gifs/stop-christina-aguilera-ncuegg83kFeBW" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .sass-cache 4 | .secret 5 | preview/css 6 | src/css/*.css 7 | *.css.map 8 | *.code-workspace 9 | secrets.json 10 | dist/* 11 | !dist/.gitkeep -------------------------------------------------------------------------------- /preview/scss/_reset-ish.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | background: #fff; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | -------------------------------------------------------------------------------- /grunt/autoprefixer.js: -------------------------------------------------------------------------------- 1 | // Browser-based preview task 2 | module.exports = { 3 | preview: { 4 | options: { 5 | browsers: ['last 6 versions', 'ie 9'] 6 | }, 7 | src: 'preview/css/preview.css' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/css/scss/default/main.scss: -------------------------------------------------------------------------------- 1 | // Main scss styles 2 | @import 'config'; 3 | @import 'global'; 4 | @import 'layout'; 5 | @import 'type'; 6 | @import 'buttons'; 7 | @import 'notices'; 8 | @import 'dividers'; 9 | @import 'other'; 10 | @import 'responsive'; -------------------------------------------------------------------------------- /preview/scss/preview.scss: -------------------------------------------------------------------------------- 1 | // Some vars here are fine. 2 | $drawer-animation-duration: 0.3s; 3 | 4 | // Janky, basic needs reset 5 | @import "reset-ish"; 6 | 7 | // Preivew User Interface 8 | @import "ui"; 9 | 10 | // Preview "Welcome (default) Screen" 11 | @import "welcome"; 12 | -------------------------------------------------------------------------------- /src/partials/components/divider.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 |
4 | 5 | 6 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /src/partials/components/chart.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 |
4 | {{{ alt }}} 5 |
8 | -------------------------------------------------------------------------------- /src/partials/components/button.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 |
4 | 5 | 6 | 9 | 10 |
7 | {{{ title }}} 8 |
11 |
-------------------------------------------------------------------------------- /grunt/express.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | options: { 4 | cmd: process.argv[0], 5 | background: true, 6 | delay: 1000, 7 | output: '.+', 8 | port: 4000, 9 | script: './server.js', 10 | debug: true, 11 | bases: ['<%= paths.dist %>', '<%= paths.preview %>', '<%= paths.src %>'], 12 | livereload: true, 13 | spawn: false, 14 | } 15 | } 16 | }; -------------------------------------------------------------------------------- /src/css/scss/default/_dividers.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | DIVIDERS 3 | ------------------------------------- */ 4 | 5 | .divider { 6 | border-collapse: separate; 7 | 8 | &-spacer { 9 | padding: $divider-margin; 10 | } 11 | 12 | td { 13 | border-top: 1px solid $divider-color; 14 | line-height: 0; 15 | font-size: 0; 16 | height: 1px; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | } -------------------------------------------------------------------------------- /grunt/assemble.js: -------------------------------------------------------------------------------- 1 | // Assembles your email content with HTML layout 2 | module.exports = { 3 | options: { 4 | layoutdir: '<%= paths.src %>/layouts', 5 | partials: ['<%= paths.src %>/partials/**/*.hbs'], 6 | helpers: ['<%= paths.src %>/helpers/**/*.js'], 7 | data: ['<%= paths.src %>/data/*.{json,yml}'], 8 | flatten: true 9 | }, 10 | pages: { 11 | src: ['<%= paths.src %>/emails/*.hbs'], 12 | dest: '<%= paths.dist %>/' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /grunt/cdn.js: -------------------------------------------------------------------------------- 1 | // CDN will replace local paths with your CDN path 2 | module.exports = { 3 | aws_s3: { 4 | options: { 5 | cdn: '<%= secrets.s3.bucketuri %>/<%= secrets.s3.bucketname %>/<%= secrets.s3.bucketdir %>', // See README for secrets.json or replace this with your Amazon S3 bucket uri 6 | flatten: true, 7 | supportedTypes: 'html' 8 | }, 9 | cwd: './<%= paths.dist %>', 10 | dest: './<%= paths.dist %>', 11 | src: ['*.html'] 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /grunt/juice.js: -------------------------------------------------------------------------------- 1 | // Inlines your CSS 2 | module.exports = { 3 | dist: { 4 | options: { 5 | preserveMediaQueries: true, 6 | applyAttributesTableElements: true, 7 | applyWidthAttributes: true, 8 | preserveImportant: true, 9 | preserveFontFaces: true, 10 | webResources: { 11 | images: false 12 | } 13 | }, 14 | files: [{ 15 | expand: true, 16 | src: ['<%= paths.dist %>/*.html'], 17 | dest: '' 18 | }] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/layouts/default.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 |
16 |
{{ body }}
17 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/partials/components/notice.hbs: -------------------------------------------------------------------------------- 1 | {{! 2 | Notification Block 3 | Params: 4 | type: (string) accepts 'info', 'success', 'warning', 'danger' 5 | class: (string) opional classes you may want to apply such as 'align-center' 6 | text: (string) unescaped, the text you wish to display in the notice 7 | }} 8 | 9 | 10 | 17 | 18 |
11 | 12 | 13 | 14 | 15 |
{{{ text }}}
16 |
-------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const path = require('path'), 3 | folders = { 4 | app: 'app', 5 | dist: 'dist', 6 | tmp: '.tmp' 7 | }; 8 | 9 | module.exports = function(grunt) { 10 | var path = require('path'); 11 | 12 | require('load-grunt-config')(grunt, { 13 | configPath: [ 14 | path.join(process.cwd(), 'grunt'), 15 | ], 16 | init: true, 17 | data: { 18 | folders: folders, 19 | paths: { 20 | src: 'src', 21 | src_img: 'src/img', 22 | dist: 'dist', 23 | dist_img: 'dist/img', 24 | preview: 'preview' 25 | }, 26 | } 27 | }); 28 | }; 29 | })(); -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | // Watches for changes to CSS or email templates then runs grunt tasks 2 | module.exports = { 3 | emails: { 4 | files: ['<%= paths.src %>/css/scss/*/**','<%= paths.src %>/emails/*','<%= paths.src %>/layouts/*','<%= paths.src %>/partials/**/*','<%= paths.src %>/data/*'], 5 | tasks: ['build'], 6 | options: { 7 | livereload: true 8 | } 9 | }, 10 | preview_dist: { 11 | files: ['./dist/*'], 12 | tasks: [], 13 | options: { 14 | livereload: true 15 | } 16 | }, 17 | preview: { 18 | files: ['<%= paths.preview %>/scss/*'], 19 | tasks: ['sass:preview','autoprefixer:preview'], 20 | options: { 21 | livereload: true 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /preview/_blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BLANK 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |

Alright. Let's roll...

18 |

Select a template from the upper right.

19 |
20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /grunt/aliases.yaml: -------------------------------------------------------------------------------- 1 | # Where we tell Grunt what to do when we type "grunt" into the terminal 2 | # $ grunt 3 | default: 4 | - serve 5 | 6 | # Use grunt send if you want to actually send the email to your inbox 7 | # $ grunt send --template=transaction.html 8 | send: 9 | - mailgun 10 | 11 | # Upload image files to Amazon S3 12 | # $ grunt s3upload 13 | s3upload: 14 | - build 15 | - aws_s3:prod 16 | - cdn:aws_s3 17 | 18 | # Compile all templates once and exit the process 19 | # $ grunt build 20 | build: 21 | - clean 22 | - sass:dist 23 | - assemble 24 | - juice 25 | 26 | # Launch the express server and start watching 27 | # $ grunt serve 28 | serve: 29 | - build 30 | - sass:preview 31 | - autoprefixer:preview 32 | - express:server 33 | - open 34 | - watch -------------------------------------------------------------------------------- /grunt/replace.js: -------------------------------------------------------------------------------- 1 | // Replace compiled template images sources from ../src/html to ../dist/html 2 | module.exports = { 3 | src_images: { 4 | options: { 5 | usePrefix: false, 6 | patterns: [ 7 | { 8 | match: /(]+[\"'])(\.\.\/src\/img\/)/gi, // Matches /' 14 | } 15 | ] 16 | }, 17 | 18 | files: [{ 19 | expand: true, 20 | flatten: true, 21 | src: ['<%= paths.dist %>/*.html'], 22 | dest: '<%= paths.dist %>' 23 | }] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /grunt/mailgun.js: -------------------------------------------------------------------------------- 1 | // Use Mailgun option if you want to email the design to your inbox or to something like Litmus 2 | module.exports = function(grunt) { 3 | return { 4 | mailer: { 5 | options: { 6 | key: '<%= secrets.mailgun.api_key %>', // See README for secrets.json or replace this with your own key 7 | domain: '<%= secrets.mailgun.domain %>', // See README for secrets.json or replace this with your own email domain 8 | sender: '<%= secrets.mailgun.sender %>', // See README for secrets.json or replace this with your preferred sender 9 | recipient: '<%= secrets.mailgun.recipient %>', // See README for secrets.json or replace this with your preferred recipient 10 | subject: 'This is a test email' 11 | }, 12 | src: ['<%= paths.dist %>/'+grunt.option('template')] 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /grunt/sass.js: -------------------------------------------------------------------------------- 1 | // Takes your SCSS files and compiles them to CSS 2 | const sass = require('node-sass'); 3 | 4 | module.exports = { 5 | dist: { 6 | options: { 7 | style: 'expanded', 8 | implementation: sass 9 | }, 10 | files: [ 11 | { 12 | expand: true, 13 | flatten: true, // do not create subfolders 14 | cwd: '<%= paths.src %>/css/scss/', 15 | src: ['*/**/*.scss', '!*/**/_*.scss'], 16 | dest: '<%= paths.src %>/css/', 17 | ext: '.css', 18 | } 19 | ] 20 | }, 21 | 22 | // This task compiles Sass for the browser-baed preview UI. 23 | // You should not need to edit it. 24 | preview: { 25 | options: { 26 | style: 'compressed', 27 | implementation: sass 28 | }, 29 | files: { 30 | '<%= paths.preview %>/css/preview.css': '<%= paths.preview %>/scss/preview.scss' 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/css/scss/default/_other.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | OTHER STYLES THAT MIGHT BE USEFUL 3 | ------------------------------------- */ 4 | 5 | .last { 6 | margin-bottom: 0; 7 | } 8 | 9 | .first { 10 | margin-top: 0; 11 | } 12 | 13 | .align-center { 14 | text-align: center; 15 | } 16 | 17 | .align-right { 18 | text-align: right; 19 | } 20 | 21 | .align-left { 22 | text-align: left; 23 | } 24 | 25 | .clear { 26 | clear: both; 27 | } 28 | 29 | .mt0 { 30 | margin-top: 0; 31 | } 32 | 33 | .mb0 { 34 | margin-bottom: 0; 35 | } 36 | 37 | // Preheader text is displayed in certain clients as a preview but not shown when the user opens the email 38 | .preheader { 39 | color: transparent; 40 | display: none; 41 | height: 0; 42 | max-height: 0; 43 | max-width: 0; 44 | opacity: 0; 45 | overflow: hidden; 46 | mso-hide: all; 47 | visibility: hidden; 48 | width: 0; 49 | } -------------------------------------------------------------------------------- /src/css/scss/default/_responsive.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | RESPONSIVE AND MOBILE FRIENDLY STYLES 3 | ------------------------------------- */ 4 | 5 | table[class=body]{ 6 | @media only screen and (max-width: $width+40) { 7 | 8 | // Type 9 | h1, 10 | h2, 11 | h3, 12 | h4 { 13 | font-weight: 600 !important; 14 | } 15 | 16 | h1 { 17 | font-size: 22px !important; 18 | } 19 | 20 | h2 { 21 | font-size: 18px !important; 22 | } 23 | 24 | h3 { 25 | font-size: 16px !important; 26 | } 27 | 28 | // Spacing 29 | .content, 30 | .wrapper { 31 | padding: 10px !important; 32 | } 33 | 34 | .container { 35 | padding: 0 !important; 36 | width: 100% !important; 37 | } 38 | 39 | // Buttons 40 | .btn table, 41 | .btn a { 42 | width: 100% !important; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /grunt/aws_s3.js: -------------------------------------------------------------------------------- 1 | // Use Amazon S3 for images 2 | // grunt s3upload 3 | module.exports = { 4 | options: { 5 | accessKeyId: '<%= secrets.s3.key %>', // See README for secrets.json 6 | secretAccessKey: '<%= secrets.s3.secret %>', // See README for secrets.json 7 | region: '<%= secrets.s3.region %>', // Enter region or leave blank for US Standard region 8 | uploadConcurrency: 5, // 5 simultaneous uploads 9 | downloadConcurrency: 5 // 5 simultaneous downloads 10 | }, 11 | 12 | prod: { 13 | options: { 14 | bucket: '<%= secrets.s3.bucketname %>', // Define your S3 bucket name in secrets.json 15 | differential: true, // Only uploads the files that have changed 16 | params: { 17 | CacheControl: '2000' 18 | } 19 | }, 20 | files: [ 21 | {expand: true, cwd: '<%= paths.dist_img %>', src: ['**'], dest: '<%= secrets.s3.bucketdir %>/<%= paths.dist_img %>'} 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2014] [Lee Munroe] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/emails/branded.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default.hbs 3 | subject: Branded transaction email example 4 | --- 5 | This is preheader text. Some clients will show this text as a preview. 6 |
7 | 8 | 9 | 12 | 13 |
10 | HTML Email 11 |
14 |
15 | 16 | 17 | 28 | 29 |
18 | 19 | 20 | 25 | 26 |
21 |

Example of an email with image asset.

22 |

You should upload your email images to a CDN, or for smaller file sizes you can also inline them.

23 |

Should come in handy for branded transactional emails or promotional type emails.

24 |
27 |
30 | 39 | -------------------------------------------------------------------------------- /src/css/scss/default/_notices.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | NOTICES 3 | ------------------------------------- */ 4 | 5 | @mixin create-notice($color) { 6 | td { 7 | background: lighten($color, 44%); 8 | border: 1px solid $color; 9 | border-radius: $notice-border-radius; 10 | color: darken($color, 20%); 11 | padding: $notice-padding; 12 | } 13 | } 14 | 15 | .notice { 16 | border-collapse: separate; 17 | 18 | &-spacer { 19 | padding: $notice-margin; 20 | } 21 | 22 | td { 23 | line-height: $notice-line-height; 24 | font-size: $notice-font-size; 25 | font-weight: $notice-font-weight; 26 | } 27 | 28 | // Color Variations 29 | &-info { 30 | @include create-notice($notice-color-info); 31 | } 32 | 33 | &-success { 34 | @include create-notice($notice-color-success); 35 | } 36 | 37 | &-warning { 38 | @include create-notice($notice-color-warning); 39 | } 40 | 41 | &-danger { 42 | @include create-notice($notice-color-danger); 43 | } 44 | 45 | // Size Variations 46 | &-lg td { 47 | font-size: ($notice-font-size * 1.2); 48 | line-height: $notice-line-height; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email-template", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "main": "Gruntfile.js", 6 | "scripts": { 7 | "send": "grunt send", 8 | "s3upload": "grunt s3upload", 9 | "build": "grunt build", 10 | "serve": "grunt serve" 11 | }, 12 | "engines": { 13 | "node": ">=12" 14 | }, 15 | "dependencies": { 16 | "assemble": "^0.24.3", 17 | "cheerio": "^1.0.0-rc.12", 18 | "connect-livereload": "^0.6.1", 19 | "ejs": "^3.1.9", 20 | "express": "^4.18.2", 21 | "grunt": "^1.6.1", 22 | "grunt-assemble": "^0.6.3", 23 | "grunt-autoprefixer": "^3.0.4", 24 | "grunt-aws-s3": "^2.0.2", 25 | "grunt-cdn": "^0.6.5", 26 | "grunt-contrib-clean": "^2.0.1", 27 | "grunt-contrib-watch": "^1.1.0", 28 | "grunt-express-server": "^0.5.4", 29 | "grunt-juice": "^0.0.2", 30 | "grunt-mailgun": "^2.0.1", 31 | "grunt-open": "^0.2.4", 32 | "grunt-parallel": "^0.5.1", 33 | "grunt-replace": "^2.0.2", 34 | "grunt-sass": "^3.1.0", 35 | "juice": "^10.0.0", 36 | "load-grunt-config": "^4.0.1", 37 | "load-grunt-tasks": "^5.1.0", 38 | "node-sass": "^9.0.0" 39 | }, 40 | "devDependencies": { 41 | "js-yaml": "^4.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/css/scss/default/_type.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | TYPOGRAPHY 3 | ------------------------------------- */ 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4 { 9 | color: $font-color !important; 10 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 11 | font-weight: 400; 12 | line-height: 1.4em; 13 | margin: 0; 14 | margin-bottom: 24px; 15 | } 16 | 17 | h1 { 18 | font-size: 38px; 19 | text-transform: capitalize; 20 | font-weight: 300; 21 | } 22 | h2 { 23 | font-size: 24px; 24 | } 25 | h3 { 26 | font-size: 18px; 27 | } 28 | h4 { 29 | font-size: 16px; 30 | font-weight:500; 31 | } 32 | 33 | p, 34 | ul, 35 | ol { 36 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 37 | font-size: 16px; 38 | font-weight: normal; 39 | margin: 0; 40 | margin-bottom: 16px; 41 | 42 | li { 43 | list-style-position: inside; 44 | margin-left: 4px; 45 | } 46 | } 47 | 48 | a { 49 | color: $primary-color; 50 | text-decoration: underline; 51 | } 52 | 53 | // Break long strings to prevent x-scrolling 54 | code, 55 | pre, 56 | .word-wrap { 57 | word-break: break-word; 58 | word-wrap: break-word; 59 | -webkit-hyphens: auto; 60 | -moz-hyphens: auto; 61 | hyphens: auto; 62 | } 63 | -------------------------------------------------------------------------------- /src/css/scss/default/_buttons.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | BUTTONS 3 | ------------------------------------- */ 4 | 5 | .btn { 6 | width: 100%; 7 | 8 | > tr > td { 9 | padding-bottom: 16px; 10 | } 11 | 12 | table { 13 | width: auto; 14 | } 15 | 16 | table td { 17 | background-color: #ffffff; 18 | border-radius: $btn-br; 19 | text-align: center; 20 | } 21 | 22 | a { 23 | background-color: #ffffff; 24 | border: solid 1px $primary-color; 25 | border-radius: $btn-br; 26 | color: $primary-color; 27 | cursor: pointer; 28 | display: inline-block; 29 | font-size: 16px; 30 | font-weight: bold; 31 | margin: 0; 32 | padding: 12px 24px; 33 | text-decoration: none; 34 | text-transform: capitalize; 35 | } 36 | } 37 | 38 | // Primary call to action button 39 | .btn-primary { 40 | 41 | table td { 42 | background-color: $primary-color; 43 | } 44 | 45 | a { 46 | background-color: $primary-color; 47 | border-color: $primary-color; 48 | color: #ffffff; 49 | } 50 | } 51 | 52 | // Secondary button 53 | .btn-secondary { 54 | 55 | table td { 56 | background-color: transparent; 57 | } 58 | 59 | a { 60 | background-color: transparent; 61 | border-color: $primary-color; 62 | color: $primary-color; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/css/scss/default/_global.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | GLOBAL RESETS 3 | ------------------------------------- */ 4 | 5 | table, 6 | td, 7 | div, 8 | a { 9 | box-sizing: border-box; 10 | } 11 | 12 | img { 13 | -ms-interpolation-mode: bicubic; 14 | max-width: 100%; 15 | } 16 | 17 | html { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | body { 23 | font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; 24 | -webkit-font-smoothing: antialiased; 25 | font-size: 16px; 26 | height: 100% !important; 27 | line-height: 1.6em; 28 | margin: 0; 29 | padding: 0; 30 | -ms-text-size-adjust: 100%; 31 | -webkit-text-size-adjust: 100%; 32 | width: 100% !important; 33 | } 34 | 35 | // Let's make sure all tables have defaults 36 | table { 37 | border-collapse: separate !important; 38 | mso-table-lspace: 0pt; 39 | mso-table-rspace: 0pt; 40 | width: 100%; 41 | 42 | td { 43 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 44 | font-size: 16px; 45 | vertical-align: top; 46 | } 47 | } 48 | 49 | // External class fix for Outlook.com 50 | .ExternalClass { 51 | width: 100%; 52 | } 53 | 54 | .ExternalClass, 55 | .ExternalClass p, 56 | .ExternalClass span, 57 | .ExternalClass font, 58 | .ExternalClass td, 59 | .ExternalClass div { 60 | line-height: 100%; 61 | } 62 | -------------------------------------------------------------------------------- /src/css/scss/default/_config.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | CONFIGURE YOUR VARIABLES 3 | ------------------------------------- */ 4 | 5 | $padding: 24px; // For consistent padding 6 | $grey: #f4f5f6; // Background color 7 | $width: 580px; // Width of the content area 8 | $br: 8px; // Border radius 9 | $btn-br: 4px; // Border radius 10 | $primary-color: #0867ec; // Primary color for buttons and links 11 | $font-color: #161f33; // Default font color 12 | 13 | // Typography 14 | // ------- 15 | 16 | $body-font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif !default; 17 | $body-font-weight: 400 !default; 18 | $body-font-size: 16px !default; 19 | $body-font-color: $font-color !default; 20 | $body-line-height: 1.6 !default; 21 | 22 | // Notices 23 | // ------- 24 | 25 | $notice-color-info: #0867ec !default; 26 | $notice-color-success: #00aa55 !default; 27 | $notice-color-warning: #f39c12 !default; 28 | $notice-color-danger: #d51507 !default; 29 | 30 | $notice-border-radius: 4px !default; 31 | $notice-font-size: $body-font-size !default; 32 | $notice-font-weight: $body-font-weight !default; 33 | $notice-line-height: $body-line-height !default; 34 | $notice-padding: 8px 12px !default; 35 | $notice-margin: 8px 0 !default; 36 | 37 | // Dividers 38 | // -------- 39 | 40 | $divider-margin: 24px 0 !default; 41 | $divider-color: #eaebed !default; -------------------------------------------------------------------------------- /src/css/scss/default/_layout.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | BODY & CONTAINER 3 | ------------------------------------- */ 4 | 5 | body{ 6 | background-color: $grey; 7 | } 8 | 9 | .body { 10 | background-color: $grey; 11 | width: 100%; 12 | } 13 | 14 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ 15 | .container { 16 | display: block; 17 | Margin: 0 auto !important; /* makes it centered */ 18 | max-width: $width; 19 | padding: 8px; 20 | width: auto !important; 21 | width: $width; 22 | } 23 | 24 | /* This should also be a block element, so that it will fill 100% of the .container */ 25 | .content { 26 | display: block; 27 | margin: 0 auto; 28 | max-width: $width; 29 | padding: 8px; 30 | } 31 | 32 | 33 | /* ------------------------------------- 34 | HEADER, FOOTER, MAIN 35 | ------------------------------------- */ 36 | 37 | .main { 38 | background: #ffffff; 39 | border: 1px solid darken($grey, 5); 40 | border-radius: $br; 41 | width: 100%; 42 | } 43 | 44 | .wrapper { 45 | padding: $padding; 46 | } 47 | 48 | // Adds padding to content - use this instead of paragraphs for consistent padding/margin rendering 49 | .content-block { 50 | padding: 0 0 $padding; 51 | } 52 | 53 | .header { 54 | margin-bottom: $padding/2; 55 | margin-top: $padding/2; 56 | width: 100%; 57 | } 58 | 59 | .footer { 60 | clear: both; 61 | width: 100%; 62 | 63 | * { 64 | color: #999999; 65 | font-size: 12px; 66 | } 67 | 68 | td { 69 | padding: 24px 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /preview/scss/_welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome-view { 2 | 3 | &, body { 4 | height: 100%; 5 | } 6 | body { 7 | color: #666; 8 | font: normal 16px/1.4 Helvetica,Arial,Sans-serif; 9 | } 10 | h1 { 11 | color: #333; 12 | font-size: 24px; 13 | margin: 20px 0 10px 0; 14 | padding: 0; 15 | } 16 | p { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | .wrapper { 21 | display: table; 22 | height: 100%; 23 | min-height: 100%; 24 | width: 100%; 25 | } 26 | .wrapper--inner { 27 | display: table-cell; 28 | text-align: center; 29 | vertical-align: middle; 30 | } 31 | .animation { 32 | height: 70px; 33 | position: relative; 34 | } 35 | .animation--stem { 36 | position: absolute; 37 | left: 50%; 38 | top: 35px; 39 | width: 2px; 40 | height: 50px; 41 | margin-left: -1px; 42 | background: #348eda; 43 | 44 | &:before { 45 | content: ""; 46 | position: absolute; 47 | left: 50%; 48 | top: 0; 49 | border-radius: 100%; 50 | background: #348eda; 51 | width: 6px; 52 | height: 6px; 53 | margin-left: -3px; 54 | margin-top: -3px; 55 | } 56 | } 57 | .animation--icon { 58 | position: absolute; 59 | left: 50%; 60 | top: 50%; 61 | width: 80px; 62 | height: 80px; 63 | margin-left: -40px; 64 | margin-top: -40px; 65 | 66 | span { 67 | background: #348eda; 68 | background: rgba(52, 142, 218, 0); 69 | border-radius: 100%; 70 | display: block; 71 | width: 80px; 72 | height: 80px; 73 | transform: scale(.2); 74 | animation-name: wilkomen; 75 | animation-duration: 1.8s; 76 | animation-iteration-count: infinite; 77 | animation-timing-function: linear; 78 | } 79 | + .animation--icon span { 80 | animation-delay: .9s; 81 | } 82 | } 83 | } 84 | 85 | @keyframes wilkomen { 86 | 0% { } 87 | 25% { 88 | background: rgba(52, 142, 218, 1); 89 | } 90 | 50% { 91 | background: transparent; 92 | box-shadow: inset 0 0 2px rgba(52, 142, 218, 1); 93 | } 94 | 100% { 95 | transform: scale(1,1); 96 | box-shadow: inset 0 0 2px rgba(52, 142, 218, 0); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/emails/transaction.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default.hbs 3 | subject: Simple Transactional Email 4 | --- 5 | This is preheader text. Some clients will show this text as a preview. 6 | 7 | 8 | 50 | 51 |
9 | 10 | 11 | 47 | 48 |
12 |

Simple responsive HTML email template

13 |

Hi there,

14 |

Sometimes you just want to send a simple HTML email with a basic design.

15 |

This is a really simple email template. It's sole purpose is to get you to click the button below.

16 |

All the information you need is on GitHub.

17 | 18 | 40 | 41 | {{> button type='primary' align='center' url='https://github.com/leemunroe/grunt-email-workflow' title='View the source on GitHub' }} 42 | 43 |

Feel free to use, copy, modify this email template as you wish.

44 |

Thanks, have a lovely day.

45 |

{{> follow_lee }}

46 |
49 |
52 | 61 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | cheerio = require('cheerio'), 3 | path = require('path'), 4 | fs = require('fs'), 5 | app = express(); 6 | 7 | // Use embedded javascript for the view engine (templates) 8 | app.set('view engine', 'ejs'); 9 | 10 | // Routes to assets 11 | app.use('/', express.static(path.join(__dirname, '/preview'))); 12 | app.use('/css', express.static(path.join(__dirname, '/preview/css'))); 13 | app.use('/dist', express.static(path.join(__dirname, '/dist'))); 14 | 15 | // Allow relative image links from either ./dist/img or ./src/img 16 | app.use('/src/img', express.static(path.join(__dirname, '/src/img'))); 17 | app.use('/dist/img', express.static(path.join(__dirname, '/dist/img'))); 18 | 19 | // DEBUG - load CSS directly for inspection 20 | // app.use('/src/css', express.static(path.join(__dirname, '/src/css'))); 21 | 22 | app.use(require('connect-livereload')({ 23 | port: 35729 24 | })); 25 | 26 | app.listen(process.env.PORT, function() { 27 | console.log('Express server listening.'); 28 | }); 29 | 30 | // Set the route handler for the preview page. 31 | app.get('/', (req, res) => { 32 | 33 | res.status(200); 34 | 35 | var data = { 36 | templates: getTemplates() 37 | }; 38 | 39 | res.render(path.join(__dirname,'/preview/index'), data); 40 | }); 41 | 42 | module.exports = app; 43 | 44 | // DEBUG - custom callback for simple server logging 45 | /* 46 | module.exports = app.listen(4000, function() { 47 | console.log('Express server listening on port ' + app.get('port')); 48 | }); 49 | */ 50 | 51 | // Helper function to get templates and their 'subject' from tag 52 | function getTemplates() { 53 | var templates = [], 54 | templateDir = path.join(__dirname,'/dist/'), 55 | templateFiles = fs.readdirSync(templateDir); 56 | 57 | templateFiles.forEach( function (file) { 58 | // if (file.substr(-5) === '.html') { 59 | if (file.substring(file.length -5) === '.html') { 60 | 61 | var contents = fs.readFileSync(templateDir + file, 'utf8'); 62 | 63 | if (contents) { 64 | $ = cheerio.load(contents); 65 | 66 | templates.push({ 67 | 'filename': '/dist/' + file, 68 | 'subject': $('html title').text() || 'Subject not available' 69 | }); 70 | } 71 | } 72 | }); 73 | 74 | return templates; 75 | } 76 | -------------------------------------------------------------------------------- /src/data/pardot.yml: -------------------------------------------------------------------------------- 1 | account_address: "%%account_address%%" 2 | address_one: "%%address_one%%" 3 | address_two: "%%address_two%%" 4 | addthis_url_email: "%%addthis_url_email%%" 5 | addthis_url_facebook: "%%addthis_url_facebook%%" 6 | addthis_url_linkedin: "%%addthis_url_linkedin%%" 7 | addthis_url_more: "%%addthis_url_more%%" 8 | addthis_url_twitter: "%%addthis_url_twitter%%" 9 | city: "%%city%%" 10 | comments: "%%comments%%" 11 | company: "%%company%%" 12 | country: "%%country%%" 13 | crm_id: "%%crm_id%%" 14 | department: "%%department%%" 15 | email: "%%email%%" 16 | email_preference_center: "%%email_preference_center%%" 17 | fax: "%%fax%%" 18 | first_name: "%%first_name%%" 19 | industry: "%%industry%%" 20 | job_title: "%%job_title%%" 21 | last_name: "%%last_name%%" 22 | phone: "%%phone%%" 23 | prospect_account-annual_revenue: "%%prospect_account.annual_revenue%%" 24 | prospect_account-billing_address_one: "%%prospect_account.billing_address_one%%" 25 | prospect_account-billing_address_two: "%%prospect_account.billing_address_two%%" 26 | prospect_account-billing_city: "%%prospect_account.billing_city%%" 27 | prospect_account-billing_country: "%%prospect_account.billing_country%%" 28 | prospect_account-billing_state: "%%prospect_account.billing_state%%" 29 | prospect_account-billing_zip: "%%prospect_account.billing_zip%%" 30 | prospect_account-description: "%%prospect_account.description%%" 31 | prospect_account-employees: "%%prospect_account.employees%%" 32 | prospect_account-fax: "%%prospect_account.fax%%" 33 | prospect_account-industry: "%%prospect_account.industry%%" 34 | prospect_account-name: "%%prospect_account.name%%" 35 | prospect_account-number: "%%prospect_account.number%%" 36 | prospect_account-ownership: "%%prospect_account.ownership%%" 37 | prospect_account-phone: "%%prospect_account.phone%%" 38 | prospect_account-rating: "%%prospect_account.rating%%" 39 | prospect_account-shipping_address_one: "%%prospect_account.shipping_address_one%%" 40 | prospect_account-shipping_address_two: "%%prospect_account.shipping_address_two%%" 41 | prospect_account-shipping_city: "%%prospect_account.shipping_city%%" 42 | prospect_account-shipping_country: "%%prospect_account.shipping_country%%" 43 | prospect_account-shipping_state: "%%prospect_account.shipping_state%%" 44 | prospect_account-shipping_zip: "%%prospect_account.shipping_zip%%" 45 | prospect_account-sic: "%%prospect_account.sic%%" 46 | prospect_account-site: "%%prospect_account.site%%" 47 | prospect_account-ticker_symbol: "%%prospect_account.ticker_symbol%%" 48 | prospect_account-type: "%%prospect_account.type%%" 49 | prospect_account-website: "%%prospect_account.website%%" 50 | salutation: "%%salutation%%" 51 | state: "%%state%%" 52 | subject: "%%subject%%" 53 | territory: "%%territory%%" 54 | unsubscribe: "%%unsubscribe%%" 55 | user_crm_id: "%%user_crm_id%%" 56 | user_email: "%%user_email%%" 57 | user_first_name: "%%user_first_name%%" 58 | user_html_signature: "%%user_html_signature%%" 59 | user_job_title: "%%user_job_title%%" 60 | user_last_name: "%%user_last_name%%" 61 | user_name: "%%user_name%%" 62 | user_phone: "%%user_phone%%" 63 | user_text_signature: "%%user_text_signature%%" 64 | user_url: "%%user_url%%" 65 | view_online: "%%view_online%%" 66 | website: "%%website%%" 67 | years_in_business: "%%years_in_business%%" 68 | zip: "%%zip%%" -------------------------------------------------------------------------------- /src/emails/components.hbs: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default.hbs 3 | subject: Components Guide 4 | --- 5 | <span class="preheader">This is preheader text. Some clients will show this text as a preview.</span> 6 | <table class="main"> 7 | <tr> 8 | <td class="wrapper"> 9 | <table> 10 | <tr> 11 | <td> 12 | <h1 class="align-center">Component Guide</h1> 13 | <p>Hi there,</p> 14 | <p> 15 | Here's a quick guide to some of the built in components.<br /> 16 | Each component has it's own <code>partials/components/<component_name>.hbs</code> file and a corresponding <code>_<component_name>.scss</code> 17 | </p> 18 | 19 | {{> divider }} 20 | 21 | <!-- BEGIN DIVIDERS --> 22 | <h2>Dividers</h2> 23 | <p> 24 | <code>{{> divider }}</code> 25 | </p> 26 | {{> divider }} 27 | 28 | <!-- BEGIN NOTICES --> 29 | <h2>Notices</h2> 30 | <p> 31 | <code>{{> notice type='info' text='Your Text Here' }}</code> 32 | </p> 33 | {{> notice type='info' text='Hello, this is a <strong>info</strong> notice.' }} 34 | {{> notice type='success' text='Hello, this is a <strong>success</strong> notice.' }} 35 | {{> notice type='warning' text='Hello, this is a <strong>warning</strong> notice.' }} 36 | {{> notice type='danger' text='Hello, this is a <strong>danger</strong> notice.' }} 37 | 38 | {{> divider }} 39 | 40 | <!-- BEGIN BUTTONS --> 41 | <h2>Buttons</h2> 42 | <p> 43 | <code>{{> button type='primary' url='Absolute URL' title='Button Text Here' }}</code> 44 | </p> 45 | {{> button type='primary' url='https://github.com/leemunroe/grunt-email-workflow' title='Button (primary)' }} 46 | {{> button type='secondary' url='https://github.com/leemunroe/grunt-email-workflow' title='Button (secondary)' }} 47 | 48 | {{> divider }} 49 | 50 | <!-- BEGIN CHARTS --> 51 | <h2>Charts (<a href="https://image-charts.com/documentation">documentation</a>)</h2> 52 | <p> 53 | <code>{{> chart width='534' height='200' params='cht=lc&chd=s:cEAELFJHHHKUju9uuXUc&chco=76A4FB&chls=2.0,0.0,0.0&chxt=x,y&chxl=0:|0|1|2|3|4|5|1:|0|50|100&chg=20,50' alt='beautiful chart' }}</code> 54 | </p> 55 | {{> chart width='534' height='200' params='cht=lc&chd=s:cEAELFJHHHKUju9uuXUc&chco=76A4FB&chls=2.0,0.0,0.0&chxt=x,y&chxl=0:|0|1|2|3|4|5|1:|0|50|100&chg=20,50' alt='beautiful chart' }} 56 | 57 | {{> divider }} 58 | 59 | <p>Feel free to use, copy, modify this email template as you wish.</p> 60 | <p>Thanks, have a lovely day.</p> 61 | <p>{{> follow_lee }}</p> 62 | </td> 63 | </tr> 64 | </table> 65 | </td> 66 | </tr> 67 | </table> 68 | <div class="footer"> 69 | <table> 70 | <tr> 71 | <td class="align-center"> 72 | <p>Don't like these annoying emails? <a href="{{ default.unsubscribe_url }}"><unsubscribe>Unsubscribe</unsubscribe></a>.</p> 73 | </td> 74 | </tr> 75 | </table> 76 | </div> 77 | -------------------------------------------------------------------------------- /preview/scss/_ui.scss: -------------------------------------------------------------------------------- 1 | .preview-interface-view { 2 | 3 | // Basic elements 4 | body { 5 | background: #fff; 6 | color: #333; 7 | font: normal 14px/1.4 Helvetica, Arial, Sans-serif; 8 | } 9 | a { 10 | color: #000; 11 | text-decoration: none; 12 | 13 | &:hover { 14 | color: #348eda; 15 | } 16 | } 17 | iframe { 18 | border: none; 19 | width: 100%; 20 | } 21 | 22 | // Header 23 | .header { 24 | box-shadow: 0 0 5px rgba(0,0,0,0.4); 25 | display: table; 26 | width: 100%; 27 | position: relative; 28 | z-index: 2; 29 | 30 | > div { 31 | display: table-cell; 32 | padding: 8px 20px; 33 | vertical-align: middle; 34 | } 35 | } 36 | .header--brand { 37 | font-size: 22px; 38 | } 39 | .header--select { 40 | text-align: right; 41 | } 42 | 43 | // Preview Screens 44 | .preview-ui--full, 45 | .preview-ui--mobile, 46 | .preview-ui--mobile__container { 47 | transition: margin all $drawer-animation-duration linear, 48 | padding all $drawer-animation-duration linear, 49 | width all $drawer-animation-duration linear, 50 | opaciy all $drawer-animation-duration linear; 51 | } 52 | 53 | .preview-ui { 54 | overflow: hidden; 55 | position: relative; 56 | z-index: 1; 57 | } 58 | .preview-ui--full { 59 | float: left; 60 | padding-right: 500px; 61 | width: 100%; 62 | } 63 | .preview-ui--mobile { 64 | background: #333; 65 | float: right; 66 | width: 500px; 67 | margin-left: -500px; 68 | position: relative; 69 | text-align: center; 70 | 71 | iframe { 72 | border: 3px solid #efefef; 73 | width: 320px; 74 | height: 461px; 75 | margin: 129px 0 0 23px; 76 | } 77 | } 78 | .preview-ui--mobile__container { 79 | background: transparent url(../img/phone.png) no-repeat 50% 0; 80 | background-size: 363px 711px; 81 | width: 363px; 82 | height: 711px; 83 | margin: 20px auto 0 auto; 84 | opacity: 1; 85 | text-align: left; 86 | } 87 | 88 | .preview-ui--mobile__toggle { 89 | cursor: pointer; 90 | position: absolute; 91 | left: 10px; 92 | top: 10px; 93 | width: 40px; 94 | height: 40px; 95 | z-index: 10; 96 | } 97 | 98 | // Layout elemnts when mobile drawer is hidden 99 | .mobile-drawer-hidden { 100 | .preview-ui--full { 101 | padding-right: 60px; 102 | } 103 | .preview-ui--mobile { 104 | width: 60px; 105 | margin-left: -60px; 106 | } 107 | .preview-ui--mobile__container { 108 | opacity: 0; 109 | } 110 | } 111 | 112 | // Drawer open toggle (phone) 113 | .toggle-drawer-open { 114 | background: transparent url(../img/phone-icon.png) no-repeat 50% 50%; 115 | background-size: 40px 40px; 116 | opacity: 0; 117 | transition: all $drawer-animation-duration ease-in-out; 118 | z-index: -1; 119 | } 120 | .mobile-drawer-hidden { 121 | .toggle-drawer-open { 122 | opacity: 1; 123 | z-index: 2; 124 | 125 | &:hover { 126 | opacity: 0.8; 127 | } 128 | } 129 | } 130 | 131 | // Drawer close toggle (x) 132 | .toggle-drawer-close { 133 | z-index: 1; 134 | 135 | &:hover { 136 | opacity: 0.8; 137 | } 138 | 139 | &:before, 140 | &:after { 141 | content: ""; 142 | position: absolute; 143 | background: #fff; 144 | display: block; 145 | width: 4px; 146 | height: 40px; 147 | transition: all $drawer-animation-duration ease-in-out; 148 | } 149 | &:before { 150 | left: 18px; 151 | transform: rotate(45deg); 152 | } 153 | &:after { 154 | right: 18px; 155 | transform: rotate(-45deg); 156 | } 157 | } 158 | .mobile-drawer-hidden { 159 | .toggle-drawer-close { 160 | &:before, 161 | &:after { 162 | width: 1px; 163 | opacity: 0; 164 | transform: rotate(0deg); 165 | } 166 | &:before { 167 | left: 0; 168 | } 169 | &:after { 170 | right: 0; 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/data/hubspot.yml: -------------------------------------------------------------------------------- 1 | # Refer to http://designers.hubspot.com/docs/hubl/hubl-supported-variables 2 | 3 | # Required email template variables 4 | unsubscribe_link: "{{ unsubscribe_link }}" 5 | site_settings-company_city: "{{ site_settings.company_city }}" 6 | site_settings-company_name: "{{ company_name }}" 7 | site_settings-company_state: "{{ company_state }}" 8 | site_settings-company_street_address_1: "{{ company_street_address_1 }}" 9 | 10 | # Variables available in all templates 11 | account: "{{ account }}" 12 | company_domain: "{{ company_domain }}" 13 | contact: "{{ contact }}" 14 | content: "{{ content }}" 15 | content-absolute_url: "{{ content.absolute_url }}" 16 | content-archived: "{{ content.archived }}" 17 | content-author_email: "{{ content.author_email }}" 18 | content-author_name: "{{ content.author_name }}" 19 | content-author_username: "{{ content.author_username }}" 20 | content-campaign: "{{ content.campaign }}" 21 | content-campaign_name: "{{ content.campaign_name }}" 22 | content-created: "{{ content.created }}" 23 | content-meta_description: "{{ content.meta_description }}" 24 | content-name: "{{ content.name }}" 25 | content-publish_date: "{{ content.publish_date }}" 26 | content-publish_date_localized: "{{ content.publish_date_localized }}" 27 | content-template_path: "{{ content.template_path }}" 28 | content-updated: "{{ content.updated }}" 29 | content_id: "{{ content_id }}" 30 | eastern_dt: "{{ eastern_dt }}" 31 | favicon_link: "{{ favicon_link }}" 32 | hub_id: "{{ hub_id }}" 33 | hubspot_analytics_tracking_code: "{{ hubspot_analytics_tracking_code }}" 34 | local_dt: "{{ local_dt }}" 35 | local_time_zone: "{{ local_time_zone }}" 36 | page_meta-canonical_url: "{{ page_meta.canonical_url }}" 37 | page_meta-html_title: "{{ page_meta.html_title }}" 38 | page_meta-name: "{{ page_meta.name }}" 39 | portal_id: "{{ portal_id }}" 40 | request_contact: "{{ request_contact }}" 41 | site_settings: "{{ site_settings }}" 42 | year: "{{ year }}" 43 | 44 | # Color and font settings 45 | site_settings-background_color: "{{ site_settings.background_color }}" 46 | site_settings-body_border_color: "{{ site_settings.body_border_color }}" 47 | site_settings-body_border_color_choice: "{{ site_settings.body_border_color_choice }}" 48 | site_settings-body_color: "{{ site_settings.body_color }}" 49 | site_settings-color_picker_favorite_1: "{{ site_settings.color_picker_favorite_1 }}" 50 | site_settings-primary_accent_color: "{{ site_settings.primary_accent_color }}" 51 | site_settings-primary_font: "{{ site_settings.primary_font }}" 52 | site_settings-primary_font_color: "{{ site_settings.primary_font_color }}" 53 | site_settings-primary_font_size: "{{ site_settings.primary_font_size }}" 54 | site_settings-secondary_accent_color: "{{ site_settings.secondary_accent_color }}" 55 | site_settings-secondary_font: "{{ site_settings.secondary_font }}" 56 | site_settings-secondary_font_color: "{{ site_settings.secondary_font_color }}" 57 | site_settings-secondary_font_size: "{{ site_settings.secondary_font_size }}" 58 | 59 | # Email variables 60 | background_color: "{{ background_color }}" 61 | body_border_color: "{{ body_border_color }}" 62 | body_border_color_choice: "{{ body_border_color_choice }}" 63 | body_color: "{{ body_color }}" 64 | content-create_page: "{{ content.create_page }}" 65 | content-email_body: "{{ content.email_body }}" 66 | content-emailbody_plaintext: "{{ content.emailbody_plaintext }}" 67 | content-from_name: "{{ content.from_name }}" 68 | content-reply_to: "{{ content.reply_to }}" 69 | content-subject: "{{ content.subject }}" 70 | email_body_border_css: "{{ email_body_border_css }}" 71 | email_body_padding: "{{ email_body_padding }}" 72 | email_body_width: "{{ email_body_width }}" 73 | primary_accent_color: "{{ primary_accent_color }}" 74 | primary_font: "{{ primary_font }}" 75 | primary_font_color: "{{ primary_font_color }}" 76 | primary_font_size: "{{ primary_font_size }}" 77 | primary_font_size_num: "{{ primary_font_size_num }}" 78 | secondary_accent_color: "{{ secondary_accent_color }}" 79 | secondary_font: "{{ secondary_font }}" 80 | secondary_font_color: "{{ secondary_font_color }}" 81 | secondary_font_size_num: "{{ secondary_font_size_num }}" 82 | site_settings-company_street_address_2: "{{ site_settings.company_street_address_2 }}" 83 | site_settings-office_location_name: "{{ site_settings.office_location_name }}" 84 | subscription_confirmation_url: "{{ subscription_confirmation_url }}" 85 | subscription_name: "{{ subscription_name }}" 86 | unsubscribe_anchor: "{{ unsubscribe_anchor }}" 87 | unsubscribe_link_all: "{{ unsubscribe_link_all }}" 88 | unsubscribe_section: "{{ unsubscribe_section }}" 89 | view_as_page_section: "{{ view_as_page_section }}" 90 | view_as_page_url: "{{ view_as_page_url }}" -------------------------------------------------------------------------------- /preview/index.ejs: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en" class="preview-interface-view"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title>Grunt Email Workflow :: (Preview) 6 | 7 | 8 | 9 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 | 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grunt Email Design Workflow 2 | by [github.com/leemunroe](https://github.com/leemunroe/grunt-email-workflow) 3 | 4 | ## Changelog 5 | 6 | ### v0.2.0 - 24/01/2024 7 | 8 | * update packages and dependencies for current node versions: nvm not needed 9 | * remove obsolete tasks and packages 10 | 11 | ## Purpose 12 | 13 | Designing and testing emails is a pain. HTML tables, inline CSS, various devices and clients to test, and varying support for the latest web standards. 14 | 15 | This Grunt task helps simplify things. 16 | 17 | 1. Compiles your SCSS to CSS 18 | 2. Builds your HTML email templates 19 | 3. Inlines your CSS 20 | 21 | ## Requirements 22 | 23 | You may already have these installed on your system. If not, you'll have to install them. 24 | 25 | * Node.js - [Install Node.js](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager) 26 | * Grunt-cli and Grunt (`npm install grunt-cli -g`) 27 | 28 | ## Getting started 29 | 30 | If you haven't used [Grunt](http://gruntjs.com/) before check out Chris Coyier's post on [getting started with Grunt](http://24ways.org/2013/grunt-is-not-weird-and-hard/). 31 | 32 | #### 1. Setup 33 | 34 | Clone this repo, cd to the directory, run `npm install` to install the necessary packages. 35 | 36 | ```sh 37 | cd grunt-email-workflow 38 | npm install 39 | ``` 40 | 41 | The very first installation may take a while. Please wait patiently until completion. 42 | 43 | #### 2. Run Grunt 44 | 45 | Run `grunt build` and check out your `/dist` folder to see your compiled and inlined email templates. 46 | Run `grunt serve`, a new live-reload browser tab will open. Happy coding :) 47 | 48 | #### 3. Create secrets.json 49 | 50 | If you're using [Mailgun](https://www.mailgun.com/) and/or [Amazon S3](https://aws.amazon.com/s3/) create a `secrets.json` file in your project root as outlined below under "[Sensitive Information](#sensitive-information)". 51 | 52 | If you don't use or need these services **it's ok to skip this step**. 53 | 54 | ### Sensitive information 55 | 56 | We encourage you __not__ to store sensitive data in your git repository. If you must, please look into [git-encrypt](https://github.com/shadowhand/git-encrypt) or some other method of encrypting your configuration secrets. 57 | 58 | 1. Create a file `secrets.json` in your project root. 59 | 2. Paste the following sample code in `secrets.json` and enter the appropriate credentials for the services you want to connect with. 60 | 61 | ```json 62 | { 63 | "mailgun": { 64 | "api_key": "YOUR MG PRIVATE API KEY", 65 | "domain": "YOURDOMAIN.COM", 66 | "sender": "E.G. POSTMASTER@YOURDOMAIN.COM", 67 | "recipient": "WHO YOU WANT TO SEND THE EMAIL TO" 68 | }, 69 | "s3": { 70 | "key": "AMAZON S3 KEY", 71 | "secret": "AMAZON S3 SECRET", 72 | "region": "AMAZON S3 REGION", 73 | "bucketname": "AMAZON S3 BUCKET NAME", 74 | "bucketdir": "AMAZON S3 BUCKET SUBDIRECTORY (optional)", 75 | "bucketuri": "AMAZON S3 PATH (ex: https://s3.amazonaws.com/)" 76 | } 77 | } 78 | ``` 79 | 80 | After this you should be good to go. Run `grunt build` and your email templates should appear automagically in a `/dist` folder. 81 | 82 | ## How it works 83 | 84 | ### CSS 85 | 86 | This project uses [SCSS](http://sass-lang.com/). You don't need to touch the .css files, these are compiled automatically. 87 | 88 | For changes to CSS, modify the `.scss` files. 89 | 90 | Media queries and responsive styles are in a separate style sheet so that they don't get inlined. Note that only a few clients support media queries e.g. iOS Mail app. 91 | 92 | ### Email templates and content 93 | 94 | Handlebars and Assemble are used for templating. 95 | 96 | `/layouts` contains the standard header/footer HTML wrapper markup. You most likely will only need one layout template, but you can have as many as you like. 97 | 98 | `/emails` is where your email content will go. To start you off I've included example transactional emails based on my [simple HTML email template](https://github.com/leemunroe/html-email-template). 99 | 100 | `/data` contains _optional_ .yml or .json data files that can be used in your templates. It's a good way to store commonly used strings and variables. See `/data/default.yml` and `/partials/follow_lee.hbs` for an example. 101 | 102 | `/partials` contains _optional_ .hbs files that can be thought of like includes. To use a partial, for example `/partials/follow_lee.hbs` you would use the following code in your emails template: 103 | 104 | ```hbs 105 | {{> follow_lee }} 106 | ``` 107 | 108 | `/partials/components` contains _optional_ .hbs files that can help generate your markup. Each component will typically have a corresponding Sass file in `src/css/sass/.scss`. To use a component, for example `/partials/components/button.hbs` you would use the following code in your emails template. _(note: You can use single -or- double quotes for attributes)_ 109 | 110 | ```hbs 111 | {{> button type="primary" align="center" url="LINK GOES HERE" title="ANCHOR TEXT GOES HERE" }} 112 | ``` 113 | 114 | ### Generate your email templates 115 | 116 | In terminal, run `grunt build`. This will: 117 | 118 | * Compile your SCSS to CSS 119 | * Generate your email layout and content 120 | * Inline your CSS 121 | 122 | See the output HTML in the `dist` folder. Open them and preview it the browser. 123 | 124 | Alternatively run `grunt serve`. This will check for any changes you make to your .scss and .hbs templates, automatically run the tasks, and serve you a preview in the browser on [http://localhost:4000](http://localhost:4000). Saves you having to run grunt every time you make a change. 125 | 126 | ### Browser-based previews 127 | 128 | In terminal, run `grunt serve`. 129 | 130 | * This will run the default tasks `grunt` + the `watch` task will be initiated 131 | * A preview UI will automagically open on [http://localhost:4000](http://localhost:4000) and you can review your templates 132 | * Go about your business editing templates and see your template changes live-reload 133 | * __NOTE:__ The express server stops working when the `watch` task is not running 134 | 135 | image 136 | 137 | 138 | ### Sample email templates 139 | 140 | I've added a few templates here to help you get started. 141 | 142 | * [Simple transactional email template](http://leemunroe.github.io/grunt-email-workflow/dist/transaction.html) 143 | * [Branded email via CDN](http://leemunroe.github.io/grunt-email-workflow/dist/branded.html) 144 | * [Email with components](http://leemunroe.github.io/grunt-email-workflow/dist/components.html) 145 | 146 | ### More resources 147 | 148 | * For more transactional email templates check out [HTML Email templates](https://htmlemail.io) 149 | * [Things I've learned about sending email](http://www.leemunroe.com/sending-email-designers-developers/) 150 | * [Things I've learned about building email templates](http://www.leemunroe.com/building-html-email/) 151 | * [Things I've learned about responsive email](https://www.leemunroe.com/responsive-email-design/) 152 | * Prefer Gulp? Daryll Doyle has created a [Gulp email creator](https://github.com/darylldoyle/Gulp-Email-Creator) 153 | --------------------------------------------------------------------------------