Automate Salesforce Marketing Cloud MJML Email Templates with Gulp

… Last Updated:

A typical staging conveyor belt for MJML email template compilation.
munich conveyor belt” by tom_focus is licensed under CC BY-NC-SA 2.0.

The Manual Export Problem

If you’ve read our MJML tutorial for Salesforce Marketing Cloud, you know how powerful MJML is for creating cross-client compatible email templates. But there’s one tedious part of the workflow: manually exporting HTML every time you make a change.

Salesforce Email Developer Frustrations:

  • Constantly switching between VS Code and command palette
  • Forgetting to export before testing in Marketing Cloud
  • Managing multiple template files across projects
  • Keeping shared components synchronized across templates
  • Manually organizing exported HTML files

When you’re building multiple email templates that share common components (headers, footers, buttons), the manual export process becomes a productivity killer. Here’s how to automate the entire workflow using Node.js and Gulp.

What This Automation Accomplishes

Automatic Workflow Benefits:

  • Live compilation – HTML updates instantly when you save MJML files
  • Smart include handling – Changes to shared components recompile all templates
  • Organized file structure – Maintains folder hierarchy in output
  • Error reporting – Immediate feedback on MJML syntax issues
  • Prettier formatting – Clean, readable HTML output
  • Build management – Clean builds and batch processing

Prerequisites

Before starting, ensure you have:

  • Node.js installed on your system (download here)
  • Basic familiarity with command line/terminal
  • An existing MJML project or willingness to create one
  • Understanding of file/folder structures

Project Structure Setup

Create this folder structure for your email project:

email-project/
├── src/
│   ├── includes/
│   │   ├── header.mjml
│   │   └── footer.mjml
│   ├── newsletter.mjml
│   └── promotional.mjml
├── dist/
├── gulpfile.js
└── package.json

Folder Explanation:

  • src/ – Your MJML source files
  • src/includes/ – Shared components (headers, footers, etc.)
  • dist/ – Compiled HTML output (auto-generated)
  • gulpfile.js – Gulp automation configuration
  • package.json – Node.js project dependencies

Step 1: Create the Gulp Configuration

Create a file named gulpfile.js in your project root and add this comprehensive automation script:

javascript

//----------------------------------------------------------//
//               _        __ _ _         
//    __ _ _   _| |_ __  / _(_) | ___ 
//   / _` | | | | | '_ \| |_| | |/ _ \
//  | (_| | |_| | | |_) |  _| | |  __/
//   \__, |\__,_|_| .__/|_| |_|_|\___|
//   |___/        |_|          
//  
//
//  TABLE OF CONTENTS
//  ---
//  01. DEPENDENCIES
//  02. FILE PATHS
//      a.  Base Paths
//      b.  File Paths
//  03. HELPER FUNCTIONS
//      a.  getMJMLFiles
//  04. TASKS
//      a.  Process MJML
//      b.  File Watching
//      c.  Clean Distributable Files
//      d.  Build Distributable Files
//  05. EXPORTS
//  
//----------------------------------------------------------//

//----------------------------------------------------------//
//  01. DEPENDENCIES
//----------------------------------------------------------//

import { series, watch } from 'gulp';
import { deleteSync } from 'del';
import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
import mjml2html from 'mjml';
import prettier from 'prettier';

// Normalize paths for cross-platform use
const normalizePath = (p) => p.replace(/\\/g, '/');

//----------------------------------------------------------//
//  02. FILE PATHS
//----------------------------------------------------------//
// Define file source and destination paths for markup & images
// Can point to a directory of files, single file, or specfic 
//----------------------------------------------------------//
//  a.  Base Paths
//      • Define base paths for source code (src) and 
//        production ready code (dist)
//----------------------------------------------------------//

const basePath = {
    src: './src',
    dist: './dist'
};

//----------------------------------------------------------//
//  b.  File Paths
//      • Define paths for file types
//----------------------------------------------------------//

const filePath = {
    emails: {
        src: `${basePath.src}`,
        dist: `${basePath.dist}`,
    }
};

//----------------------------------------------------------//
//  03. HELPER FUNCTIONS
//----------------------------------------------------------//
//  getMJMLFiles
//  •   Recursively get all .mjml files, excluding "include"
//      directories
//----------------------------------------------------------//

function getMJMLFiles(dir, files = []) {
  const entries = fs.readdirSync(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);

    if (entry.isDirectory()) {
      if (entry.name.toLowerCase() === 'includes') continue;
      getMJMLFiles(fullPath, files);
    } else if (entry.isFile() && fullPath.endsWith('.mjml')) {
      files.push(fullPath);
    }
  }

  return files;
}

//----------------------------------------------------------//
//  04. TASKS
//----------------------------------------------------------//
//     a.  Process Emails
//----------------------------------------------------------//
//         Compile a MJML file
//----------------------------------------------------------//

async function compileMJMLFile(inputPath) {
  if (!inputPath.endsWith('.mjml')) return;
  if (inputPath.includes('/include/') || inputPath.includes('\\include\\')) return;

  const relative = path.relative(filePath.emails.src, inputPath);
  const outputPath = path.join(filePath.emails.dist, relative.replace(/\.mjml$/, '.html'));

  fs.mkdirSync(path.dirname(outputPath), { recursive: true });

  try {
    const mjmlContent = fs.readFileSync(inputPath, 'utf8');
    const { html, errors } = mjml2html(mjmlContent, {
      filePath: inputPath
    });

    if (errors && errors.length > 0) {
      console.warn(`⚠ MJML warning in ${inputPath}:\n`, errors);
    }

    const formattedHtml = await prettier.format(html, {
      parser: 'html',
      printWidth: 120,
      tabWidth: 2,
      useTabs: false,
      singleQuote: false,
    });

    fs.writeFileSync(outputPath, formattedHtml, 'utf8');
    console.log(`✔ Compiled ${inputPath} → ${outputPath}`);
  } catch (err) {
    console.error(`❌ MJML failed for ${inputPath}:\n${err.message}`);
  }
}

//----------------------------------------------------------//
//         Compile All MJML
//----------------------------------------------------------//

async function compileAllMJML() {
  const mjmlFiles = await glob(`${filePath.emails.src}/**/*.mjml`, {
    ignore: [`**/includes/**`],
  });

  for (const file of mjmlFiles) {
    await compileMJMLFile(file);
  }
}

//----------------------------------------------------------//
//        Process Emails
//----------------------------------------------------------//

const emails = series(compileAllMJML);

//----------------------------------------------------------//
//      b.  File Watching
//----------------------------------------------------------//

function fileWatch() {
    watch([`${filePath.emails.src}/**/*.mjml`], { interval: 1000 }).on('change', async (changedFile) => {
      const filePathNormalized = normalizePath(changedFile);

      if (filePathNormalized.includes('/includes/')) {
        console.log(`🔁 Include file changed: ${filePathNormalized} → recompiling all templates`);
        await compileAllMJML();
      } else {
        console.log(`✏ File changed: ${filePathNormalized}`);
        await compileMJMLFile(filePathNormalized);
      }
    });
}

//----------------------------------------------------------//
//      c.  Clean Distributable Files
//----------------------------------------------------------//

async function clean() {
    await deleteSync([`${basePath.dist}/**/*`]);
}

//----------------------------------------------------------//
//      d.  Build Distributable Files
//----------------------------------------------------------//

const build = series(clean, emails);

//----------------------------------------------------------//
//  05. EXPORTS
//----------------------------------------------------------//

export {
    build,
    build as default,
    clean,
    fileWatch as watch,
    emails
};

Step 2: Configure Package Dependencies

Create a package.json file in your project root:

json

{
  "name": "mjml-compiler",
  "version": "1.0.0",
  "description": "Automated MJML compilation for email template development",
  "main": "gulpfile.js",
  "scripts": {
    "build": "gulp build",
    "watch": "gulp watch",
    "clean": "gulp clean",
    "dev": "gulp watch"
  },
  "keywords": ["mjml", "email", "automation", "gulp"],
  "author": "Your Name",
  "license": "MIT",
  "type": "module",
  "devDependencies": {
    "del": "^8.0.0",
    "gulp": "^5.0.1",
    "mjml": "^4.15.3",
    "prettier": "^3.6.2"
  }
}

Step 3: Install Dependencies

  1. Open terminal/command prompt in your project directory
  2. Run: npm install
  3. Wait for package installation to complete
  4. Optional: Run npm update --save to get latest package versions

Step 4: Understanding the Available Commands

Your Gulp setup provides these powerful commands:

gulp watch (or npm run watch)

  • Monitors all MJML files in the src/ folder
  • Automatically compiles to HTML when you save changes
  • Smart handling of include files (recompiles all templates when includes change)
  • Provides real-time feedback in terminal

gulp build (or npm run build)

  • Cleans the dist/ folder completely
  • Recompiles all MJML files from scratch
  • Perfect for clean builds or CI/CD processes

gulp clean

  • Removes all files from dist/ folder
  • Useful for starting fresh or troubleshooting

Step 5: Start the Automated Workflow

  1. In terminal, navigate to your project directory
  2. Run: gulp watch
  3. You’ll see output like:[14:32:15] Using gulpfile ~/email-project/gulpfile.js [14:32:15] Starting 'watch'...
  4. Create or edit an MJML file in the src/ folder
  5. Save the file and watch the magic happen:✏ File changed: src/newsletter.mjml ✔ Compiled src/newsletter.mjml → dist/newsletter.html

Step 6: Working with Include Files

The automation handles shared components intelligently:

Create shared components:

xml

<!-- src/includes/header.mjml -->
<mj-section background-color="#2c3e50" padding="20px">
  <mj-column>
    <mj-text color="#ffffff" font-size="24px" align="center">
      Your Company Name
    </mj-text>
  </mj-column>
</mj-section>

Use in main templates:

xml

<!-- src/newsletter.mjml -->
<mjml>
  <mj-body>
    <mj-include path="./includes/header.mjml" />
    
    <!-- Your template content here -->
    
    <mj-include path="./includes/footer.mjml" />
  </mj-body>
</mjml>

Smart recompilation:

  • When you change newsletter.mjml, only that file recompiles
  • When you change includes/header.mjml, ALL templates recompile
  • This ensures shared components stay synchronized across all templates

Advanced Configuration Options

Customize File Paths:

javascript

const basePath = {
    src: './templates',      // Change source folder
    dist: './output'         // Change output folder
};

Prettier HTML Formatting:

javascript

const formattedHtml = await prettier.format(html, {
  parser: 'html',
  printWidth: 120,         // Line width
  tabWidth: 2,             // Indentation
  useTabs: false,          // Use spaces
  singleQuote: false,      // Double quotes for attributes
});

Add More File Types:

javascript

// Watch for CSS changes too
watch([`${filePath.emails.src}/**/*.{mjml,css}`], ...)

Integrating with Marketing Cloud Workflow

Complete Development Process:

  1. Design in MJML with live preview in VS Code
  2. Auto-compile with Gulp watch running
  3. Copy HTML from dist/ folder
  4. Paste into Marketing Cloud Content Builder
  5. Test and deploy your email campaigns

File Organization Tips:

src/
├── includes/
│   ├── brand-header.mjml
│   ├── social-footer.mjml
│   └── unsubscribe-footer.mjml
├── campaigns/
│   ├── 2025-q1-newsletter.mjml
│   └── black-friday-promo.mjml
└── transactional/
    ├── welcome-email.mjml
    └── password-reset.mjml

Troubleshooting Common Issues

Gulp Command Not Found:

bash

# Install Gulp CLI globally
npm install -g gulp-cli

Permission Errors on Windows:

bash

# Run PowerShell as Administrator, then:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

MJML Compilation Errors:

  • Check MJML syntax in VS Code first
  • Review error messages in terminal output
  • Verify include file paths are correct

File Watching Not Working:

  • Ensure you’re in the correct project directory
  • Check that src/ folder exists and contains MJML files
  • Try restarting the watch command

Performance Tips

For Large Projects:

  • Use gulp build only when necessary (slower but thorough)
  • Keep gulp watch running during active development
  • Organize templates in subfolders for better management

CI/CD Integration:

json

{
  "scripts": {
    "deploy": "gulp build && npm run upload-to-marketing-cloud"
  }
}

Benefits of This Automated Workflow

Time Savings:

  • Eliminates 5-10 manual export steps per template change
  • Reduces context switching between tools
  • Instant feedback on MJML syntax errors

Quality Improvements:

  • Consistent HTML formatting with Prettier
  • Automatic include synchronization
  • Reduced human error in export process

Developer Experience:

  • Focus on design, not file management
  • Real-time compilation feedback
  • Professional development workflow

Need help setting up automated development workflows for your email marketing?  Contact Knihter for professional email development and Marketing Cloud automation services. We specialize in streamlining complex development processes.

Related Services: