npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@flexiweb/email-templates

v3.66.0

Published

Email-Template builder plugin for Flexiweb powered by grapesjs and MJML

Downloads

146

Readme

@flexiweb/email-templates

A comprehensive email template plugin for PayloadCMS v3 providing MJML-based email template editing, validations, and utilities for the Flexiweb ecosystem.

License Documentation Website

Quick Links: LicenseDocumentationWebsite


Table of Contents


Introduction

@flexiweb/email-templates is a powerful, production-ready plugin for PayloadCMS v3 that provides a comprehensive email template management system. Built with GrapesJS and MJML, this plugin enables visual email template editing while maintaining clean, responsive HTML output.

The plugin is designed to work seamlessly with the Flexiweb ecosystem and provides:

  • Visual Email Editor: Drag-and-drop email template builder powered by GrapesJS and MJML
  • Template Variables: Dynamic content substitution using the Eta templating engine
  • Validation System: Ensures MJML integrity and prevents invalid template states
  • Media Management: Dedicated collection for email template assets
  • Access Control: Role-based permissions for template management
  • Full Overridability: Customize every aspect of the plugin to fit your needs

Key Principles

  • 🎨 Visual Editing: Intuitive drag-and-drop interface for creating email templates
  • 📧 MJML-Based: Uses MJML for clean, responsive email HTML generation
  • 🔧 Fully Overridable: Every component can be customized or extended
  • 🌍 i18n Ready: Built-in support for multiple languages (German, English)
  • 🛡️ Type Safe: Full TypeScript support with comprehensive type definitions
  • ♿ Accessible: Components follow accessibility best practices

Features

🎨 Visual Email Editor

  • Drag-and-drop interface powered by GrapesJS
  • MJML component support for responsive email design
  • Real-time preview
  • Image upload and management
  • Save functionality with automatic HTML generation

📝 Template Management

  • Create, read, update, and delete email templates
  • Template type system for organizing templates
  • Enable/disable templates
  • Variable documentation system

✅ Validation System

  • MJML syntax validation
  • Prevents use of <mj-raw> tags for security
  • Ensures both MJML and HTML are synchronized
  • HTML editing restrictions (only via editor)

🖼️ Media Management

  • Dedicated media collection for email assets
  • Image upload support
  • Automatic asset loading in editor

🔐 Access Control

  • Role-based permissions
  • System-level access for critical operations
  • Editor-level access for content management

Getting Started

Installation

Install the package using your preferred package manager:

# Using pnpm (recommended)
pnpm add @flexiweb/email-templates

# Using npm
npm install @flexiweb/email-templates

# Using yarn
yarn add @flexiweb/email-templates

Peer Dependencies

This plugin requires the following peer dependencies:

Basic Usage

1. Add the Plugin to Your Payload Config

import { flexiwebEmailTemplatesPlugin } from '@flexiweb/email-templates'
import { buildConfig } from 'payload'

export default buildConfig({
  plugins: [
    flexiwebEmailTemplatesPlugin({
      // Optional: Configure plugin options
      overrides: {
        emailTemplates: {
          // Customize email templates collection
        },
      },
    }),
  ],
  // ... rest of your config
})

2. Access the Email Template Editor

Once a template is created, you can access the visual editor by:

  1. Opening an email template in the admin panel
  2. Clicking the "Open in editor" button in the "Email content" field
  3. Or navigate directly to: /admin/email-templates/{templateId}/editor

3. Use Templates in Your Application

import { substitueVariablesInString } from '@flexiweb/email-templates/utils'

// Fetch your template
const template = await payload.find({
  collection: 'email-templates',
  where: { type: { equals: 'welcome-email' } },
})

// Prepare variables
const variables = {
  recipient: {
    firstName: 'John',
    lastName: 'Doe',
    fullName: 'John Doe',
    email: '[email protected]',
  },
  handler: {
    email: '[email protected]',
    fullName: 'Support Team',
  },
  currentDate: new Date().toLocaleDateString(),
  currentYear: new Date().getFullYear().toString(),
  currentMonth: (new Date().getMonth() + 1).toString(),
  currentDay: new Date().getDate().toString(),
}

// Render the template
const renderedHtml = substitueVariablesInString(template.docs[0].html, variables)

Collections

Email Templates Collection

The email-templates collection manages all email templates in your system.

Fields

  • enabled (checkbox): Whether the template is active
  • name (text, required): Template name
  • type (select, required, unique): Template type identifier
  • mjml (textarea, read-only): MJML source code (edited in visual editor)
  • html (textarea, read-only): Generated HTML (edited in visual editor)
  • variables (array): Documentation of available template variables
    • variable (text, required): Variable name (e.g., <%= it.recipient.firstName %>)
    • description (text): Description of what the variable represents

Access Rules

  • Create: System users only
  • Read: Editor users and above
  • Update: Editor users and above
  • Delete: System users only

Note: The variables field can only be updated by system users.

Default Columns

  • name
  • type

Admin Configuration

  • Group: "Templates" (localized)
  • Title Field: name

Email Template Media Collection

The email-template-media collection stores media assets used in email templates.

Fields

  • Standard Payload upload fields

Upload Configuration

  • MIME Types: image/*
  • Static Directory: files/flexiweb/email-templates/media

Access Rules

  • Create: Editor users and above
  • Read: Public (all users)
  • Update: Editor users and above
  • Delete: Editor users and above

Admin Configuration

  • Group: "Templates" (localized)

Validations

MJML Validation

The isValidCleanMjml validation ensures that:

  1. The MJML is valid and starts with <mjml>
  2. No <mj-raw> tags are present (security restriction)
  3. Respects override rules for system users

Usage:

import { isValidCleanMjml } from '@flexiweb/email-templates/validations'

{
  name: 'mjml',
  type: 'textarea',
  validate: isValidCleanMjml(),
}

With Override:

{
  name: 'mjml',
  type: 'textarea',
  validate: isValidCleanMjml({ override: false }),
}

MJML and HTML Changed Validation

The bothMjmlAndHtmlChanged validation ensures that when updating a template, both MJML and HTML fields are provided together. This maintains synchronization between the source MJML and the generated HTML.

Usage:

import { bothMjmlAndHtmlChanged } from '@flexiweb/email-templates/validations'

{
  name: 'mjml',
  type: 'textarea',
  validate: bothMjmlAndHtmlChanged(),
}

Note: This validation only runs on update operations, not on create.

HTML Field Protection

The HTML field has additional validation that:

  1. Allows system users to edit HTML directly
  2. Only allows HTML editing from the admin panel (via the visual editor)
  3. Prevents direct API updates to HTML without MJML

This ensures HTML is always generated from MJML through the visual editor, maintaining consistency.


Email Template Editor

How It Works

The email template editor is a custom PayloadCMS admin view that provides a visual interface for creating and editing email templates.

Architecture

  1. GrapesJS: Provides the drag-and-drop interface
  2. grapesjs-mjml: Adds MJML component support
  3. Real-time Preview: Shows how the email will look
  4. Save Command: Converts MJML to HTML and saves both

Editor Initialization

When the editor loads:

  1. Fetches the template's current MJML (or uses default)
  2. Initializes GrapesJS with MJML plugin
  3. Disables <mj-raw> blocks for security
  4. Loads existing media assets
  5. Sets up save functionality

Save Process

When saving:

  1. Extracts MJML from the editor
  2. Extracts rendered HTML from the preview iframe
  3. Sends both to the Payload API via PATCH request
  4. Updates both mjml and html fields simultaneously

Editor Features

  • Drag-and-Drop Components: Add MJML components visually
  • Component Library: Access to all MJML components (sections, columns, text, images, buttons, etc.)
  • Image Upload: Upload images directly from the editor
  • Asset Management: Browse and reuse previously uploaded images
  • Real-time Preview: See changes as you edit
  • Responsive Design: Preview on different screen sizes
  • Save Button: Custom save command in the editor toolbar

Accessing the Editor

The editor is accessible at:

/admin/email-templates/{templateId}/editor

A "Redirect to Editor" button field is included in the template form for easy access.


Constants & Defaults

Default Template Variables

The plugin provides a set of default template variables that can be used as examples or starting points:

import { DEFAULT_EMAIL_TEMPLATE_VARIABLES } from '@flexiweb/email-templates/constants'

// Available variables:
// - <%= it.recipient.firstName %>
// - <%= it.recipient.lastName %>
// - <%= it.recipient.fullName %>
// - <%= it.recipient.email %>
// - <%= it.handler.email %>
// - <%= it.handler.fullName %>
// - <%= it.currentDate %>
// - <%= it.currentYear %>
// - <%= it.currentMonth %>
// - <%= it.currentDay %>

Important: These are indicative only. The actual variables available depend on what you pass when rendering the template. See Templating System for details.

Default MJML Template

A default MJML template is provided for new templates:

import { DEFAULT_EMAIL_TEMPLATE_MJML } from '@flexiweb/email-templates/constants'

This template includes:

  • Basic structure with header, body, and footer
  • Example sections demonstrating MJML components
  • Sample variable usage
  • Responsive layout examples

Default HTML Template

A corresponding default HTML template is also available:

import { DEFAULT_EMAIL_TEMPLATE_HTML } from '@flexiweb/email-templates/constants'

Media Static Directory

The static directory for email template media:

import { EMAIL_TEMPLATE_MEDIA_STATIC_DIR } from '@flexiweb/email-templates/constants'

// Value: 'files/flexiweb/email-templates/media'

Templating System

Variable Syntax

The plugin uses the Eta templating engine for variable substitution. Variables follow this syntax:

<%= it.variableName %>

For nested objects:

<%= it.recipient.firstName %>
<%= it.recipient.email %>

Using Variables

Variables shown in the template's variables field are indicative only. They document what variables are intended to be used, but the actual variables available depend on what you pass when rendering the template.

Example Template

<mj-text>
  Hello <%= it.recipient.firstName %>!
  
  Your order <%= it.orderNumber %> has been processed.
  
  Thank you,
  <%= it.handler.fullName %>
</mj-text>

Rendering the Template

import { substitueVariablesInString } from '@flexiweb/email-templates/utils'

const template = await payload.find({
  collection: 'email-templates',
  where: { type: { equals: 'order-confirmation' } },
})

const variables = {
  recipient: {
    firstName: 'John',
  },
  orderNumber: '12345',
  handler: {
    fullName: 'Support Team',
  },
}

const renderedHtml = substitueVariablesInString(
  template.docs[0].html,
  variables,
)

Passing Variables via Hooks

The recommended approach is to pass variables through hooks or service functions when sending emails. Here's an example pattern:

// hooks/emailService.ts
import { substitueVariablesInString } from '@flexiweb/email-templates/utils'
import type { Payload } from 'payload'

export async function sendEmail(
  payload: Payload,
  templateType: string,
  recipient: { email: string; firstName: string; lastName: string },
  additionalVariables: Record<string, unknown> = {},
) {
  // Fetch the template
  const templateResult = await payload.find({
    collection: 'email-templates',
    where: {
      type: { equals: templateType },
      enabled: { equals: true },
    },
    limit: 1,
  })

  if (templateResult.docs.length === 0) {
    throw new Error(`Template "${templateType}" not found or disabled`)
  }

  const template = templateResult.docs[0]

  // Prepare variables
  const now = new Date()
  const variables = {
    recipient: {
      firstName: recipient.firstName,
      lastName: recipient.lastName,
      fullName: `${recipient.firstName} ${recipient.lastName}`,
      email: recipient.email,
    },
    handler: {
      email: '[email protected]',
      fullName: 'Support Team',
    },
    currentDate: now.toLocaleDateString(),
    currentYear: now.getFullYear().toString(),
    currentMonth: (now.getMonth() + 1).toString(),
    currentDay: now.getDate().toString(),
    ...additionalVariables, // Merge any additional variables
  }

  // Render the template
  const renderedHtml = substitueVariablesInString(template.html, variables)

  // Send the email (using your email service)
  await sendEmailWithService({
    to: recipient.email,
    subject: `Welcome ${recipient.firstName}!`,
    html: renderedHtml,
  })
}

Using in Payload Hooks

import { AfterCreateHook } from 'payload'

export const sendWelcomeEmail: AfterCreateHook = async ({ doc, req }) => {
  if (doc.collection === 'users' && doc.email) {
    await sendEmail(req.payload, 'welcome-email', {
      email: doc.email,
      firstName: doc.firstName || 'User',
      lastName: doc.lastName || '',
    }, {
      // Additional context-specific variables
      signupDate: doc.createdAt,
      accountType: doc.role,
    })
  }
}

Variable Escaping

The plugin includes utilities for escaping and unescaping template tags:

import { escapeTags, unescapeTags } from '@flexiweb/email-templates/utils'

// Escape tags (useful for displaying template code)
const escaped = escapeTags('<%= it.name %>') // Returns: '&lt;%= it.name %&gt;'

// Unescape tags (done automatically during rendering)
const unescaped = unescapeTags('&lt;%= it.name %&gt;') // Returns: '<%= it.name %>'

Custom Fields

RedirectToEditorButton

A custom field that provides a button to open the email template editor.

Usage:

This field is automatically included in the email templates collection. It appears when editing a template and provides a link to the visual editor.

Customization:

import { RedirectToEditorButtonField } from '@flexiweb/email-templates/fields'

// The field is already included, but you can customize it via overrides
flexiwebEmailTemplatesPlugin({
  overrides: {
    emailTemplates: {
      fieldOverrides: {
        // Customize other fields, but RedirectToEditorButton is handled automatically
      },
    },
  },
})

Translations

The plugin includes comprehensive translations for German and English.

Translation Keys

All translation keys follow the pattern: flexiweb-email-templates:{category}:{key}

Categories:

  • editor:* - Editor-related messages
  • fields:* - Field labels and messages
  • validation:* - Validation error messages

Using Translations

Translations are automatically merged when you use the plugin:

import { flexiwebEmailTemplatesPlugin } from '@flexiweb/email-templates'

export default buildConfig({
  plugins: [flexiwebEmailTemplatesPlugin()],
  // Translations are automatically added
})

Extending Translations

You can extend or override translations in your Payload config:

export default buildConfig({
  plugins: [flexiwebEmailTemplatesPlugin()],
  i18n: {
    translations: {
      'flexiweb-email-templates:editor:saved': {
        en: 'Template saved!',
        de: 'Vorlage gespeichert!',
      },
    },
  },
})

Utilities

Template Variable Utilities

import {
  getTemplateVariables,
  substitueVariablesInString,
  escapeTags,
  unescapeTags,
} from '@flexiweb/email-templates/utils'

getTemplateVariables(template)

Extracts the variables array from a template document.

const variables = getTemplateVariables(template)
// Returns: Array of { variable: string, description?: string }[]

substitueVariablesInString(input, variables)

Renders a template string with variables using the Eta engine.

const rendered = substitueVariablesInString(
  '<%= it.name %>',
  { name: 'John' }
)
// Returns: 'John'

escapeTags(input)

Escapes Eta template tags for safe display.

const escaped = escapeTags('<%= it.name %>')
// Returns: '&lt;%= it.name %&gt;'

unescapeTags(input)

Unescapes Eta template tags (automatically called during rendering).

const unescaped = unescapeTags('&lt;%= it.name %&gt;')
// Returns: '<%= it.name %>'

Overrides & Customization

The plugin is fully customizable at every level.

Plugin-Level Overrides

flexiwebEmailTemplatesPlugin({
  disabled: false, // Disable plugin functionality
  overrides: {
    emailTemplates: {
      additionalFields: [], // Add custom fields
      fieldOverrides: {
        // Override field configurations
        nameOverrides: {
          label: { en: 'Template Name', de: 'Vorlagenname' },
        },
        typeOverrides: {
          options: [
            { label: 'Welcome Email', value: 'welcome' },
            { label: 'Order Confirmation', value: 'order-confirmation' },
          ],
        },
      },
      overrides: {
        // Override collection configuration
        admin: {
          defaultColumns: ['name', 'type', 'enabled'],
        },
      },
    },
  },
})

Field-Level Overrides

You can override any field property:

flexiwebEmailTemplatesPlugin({
  overrides: {
    emailTemplates: {
      fieldOverrides: {
        enabledOverrides: {
          defaultValue: true,
          label: { en: 'Active', de: 'Aktiv' },
        },
        mjmlOverrides: {
          admin: {
            position: 'main',
            readOnly: false, // Allow direct editing (not recommended)
          },
        },
      },
    },
  },
})

Access Control Overrides

You can customize access rules by overriding the collection:

import { createEmailTemplatesAccess } from '@flexiweb/email-templates/collections/email-templates/access'

flexiwebEmailTemplatesPlugin({
  overrides: {
    emailTemplates: {
      overrides: {
        access: {
          create: ({ req: { user } }) => {
            // Custom access logic
            return user?.role === 'admin'
          },
        },
      },
    },
  },
})

License

This software is provided free of charge for use, modification, and distribution by any individual or legal entity ("Licensee") whose total annual gross revenue does not exceed USD 250,000.

If the Licensee's annual gross revenue exceeds USD 250,000, a paid commercial license is required. For commercial licensing, contact the maintainer at [email protected].

See LICENSE.md for full license details.


Built with ❤️ for the PayloadCMS community

WebsiteGitHubDocumentation