How to build a live email editor using MJML, Pug, and Gulp

How to build a live email editor using MJML, Pug, and Gulp

Hi, it's Takuya. I've been preparing onboarding newsletters for my app, Inkdrop. In order to achieve a nice-looking email design, I researched several email templates and ended up creating my own. Here is the result:

Designing emails is hard because there are so many limitations that browsers don't have despite using HTML. For example, you can't still use Flexbox. So, you have to use traditional hacks, such as using tables for layouts.

Then, I came across MJML (Mailjet Markup Language), which is an alternative markup language for writing responsive HTML emails:

MJML - The Responsive Email Framework
The only framework that makes responsive email easy. MJML is a markup language designed to reduce the pain of coding a responsive email.

For example, here is an email template with three columns in MJML format:

<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text align="left"> Some text for your preheader </mj-text>
      </mj-column>
      <mj-column>
        <mj-text align="center"><a href="email-permalink">Read in the browser</a></mj-text>
      </mj-column>
      <mj-column>
        <mj-text align="right"> Lorem ipsum </mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

It renders like so:

You can check how it looks on their live editor here:

Email Editor
Create beautiful responsive email templates with the MJML Email Editor. No installation required, just give it a try online! With this online email editor, you can preview your responsive email on different screen sizes and share the result with anyone.

There are several templates available on their website, so you can quickly get started:

Responsive Email Templates
Ready-to-use and free responsive email templates coded in MJML.

Write MJML in Pug

I am too lazy to write HTML closing tags, so I wanted to try writing MJML in another template engine like Pug. This makes the process simpler and easier to edit. For example, the above MJML example can be written in Pug as follows:

mjml
  mj-body
    mj-section
      mj-column
        mj-text(align='left')  Some text for your preheader 
      mj-column
        mj-text(align='center')
          a(href='email-permalink') Read in the browser
      mj-column
        mj-text(align='right')  Lorem ipsum 

Now, I feel like making a live editor that lets you write HTML emails quickly using MJML and Pug.

Build a simple live editor

First, you need to install a task runner. This time, I'm gonna use gulp.

mkdir email-builder
cd email-builder
npm init -y
npm i gulp gulp-pug gulp-rename

Let's create gulpfile.js in your root project folder:

const gulp = require("gulp");
const rename = require("gulp-rename");
const pug = require("gulp-pug");

gulp.task("pug", function () {
  return gulp
    .src(["pug/**/*.pug", "!pug/**/_*.pug"])
    .pipe(
      pug({
        pretty: true,
      }),
    )
    .pipe(
      rename(function (path) {
        path.extname = ".mjml";
      }),
    )
    .pipe(gulp.dest("./mjml"));
});

Now you can convert mjml files into pug. Create two folders to save them:

mkdir pug mjml

Create a file in pug/example.pug and paste the above example, then run gulp pug. It will generate a file in mjml/example.mjml.

The next step is to convert MJML files into HTML. Install additional packages:

npm i mjml through2

And add another gulp task like so:

const through = require("through2");
const mjml = require("mjml");

gulp.task("mjml", function () {
  return gulp
    .src(["mjml/*.mjml"])
    .pipe(
      through.obj(function (file, enc, callback) {
        const output = file.clone();
        const render = mjml(file.contents.toString(), {});
        output.contents = Buffer.from(render.html);
        this.push(output);
        return callback();
      }),
    )
    .pipe(
      rename(function (path) {
        path.extname = ".html";
      }),
    )
    .pipe(gulp.dest("./html"));
});

Let's run gulp mjml, then, example.html should be generated in the html/ folder. Cool!

Next, let's make it watch file changes and automatically re-generate.

gulp.task("watch", function () {
  gulp.watch("pug/**/*.pug", gulp.task("pug"));
  gulp.watch("mjml/**/*.mjml", gulp.task("mjml"));
});

Preview emails with live reload

Now, you can open the output HTML files on a browser but it'd be better to have them reload automatically when editing, just like the MJML live editor. It can be easily accomplished by using live-server. Install it:

npm i live-server

Then, add another gulp task for live server:

const liveServer = require("live-server");

gulp.task("live-server", function (done) {
  var params = {
    port: 8080,
    host: "127.0.0.1",
    root: "./html", // Set this to your HTML output directory
    open: true,
    file: "index.html",
    wait: 400,
    mount: [["/components", "./node_modules"]],
    logLevel: 2,
  };
  liveServer.start(params);
  done();
});

gulp.task("default", gulp.parallel("watch", "live-server"));

Okay, now you can just run gulp to watch files and launch live-server! Here is the result:

Looks nice 😎

Gulpfile

To sum up the snippets, the gulpfile.js looks as follows:

const gulp = require("gulp");
const rename = require("gulp-rename");
const pug = require("gulp-pug");
const through = require("through2");
const mjml = require("mjml");
const liveServer = require("live-server");

gulp.task("live-server", function (done) {
  var params = {
    port: 8080,
    host: "127.0.0.1",
    root: "./html", // Set this to your HTML output directory
    open: true,
    file: "index.html",
    wait: 400,
    mount: [["/components", "./node_modules"]],
    logLevel: 2,
  };
  liveServer.start(params);
  done();
});

gulp.task("pug", function () {
  return gulp
    .src(["pug/**/*.pug", "!pug/**/_*.pug"])
    .pipe(
      pug({
        pretty: true,
      }),
    )
    .pipe(
      rename(function (path) {
        path.extname = ".mjml";
      }),
    )
    .pipe(gulp.dest("./mjml"));
});

gulp.task("mjml", function () {
  return gulp
    .src(["mjml/emails/*.mjml", "!mjml/**/_*.mjml"])
    .pipe(
      through.obj(function (file, enc, callback) {
        const output = file.clone();
        const render = mjml(file.contents.toString(), {});
        output.contents = Buffer.from(render.html);
        this.push(output);
        return callback();
      }),
    )
    .pipe(
      rename(function (path) {
        path.extname = ".html";
      }),
    )
    .pipe(gulp.dest("./html"));
});

gulp.task("watch", function () {
  gulp.watch("pug/**/*.pug", gulp.task("pug"));
  gulp.watch("mjml/**/*.mjml", gulp.task("mjml"));
});

gulp.task("default", gulp.parallel("watch", "live-server"));

gulp.task("build", gulp.series("pug", "mjml"));

Hope it helps!


Check out my app called Inkdrop - A tech note-taking app for software developers:

Inkdrop - Note-taking App with Robust Markdown Editor
The Note-Taking App with Robust Markdown Editor