npm package: @loadspark/email-templates
Description: !!!Payload version: 3.69.0!!! Email-Template builder plugin for loadspark powered by grapesjs and MJML
Author: loadspark
Latest version: 0.0.1

# @loadspark/email-templates

<div align="center">

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

<br />

[![License](https://img.shields.io/badge/license-mit-blue.svg)](LICENSE.md)
[![Documentation](https://img.shields.io/badge/docs-Available-green.svg)](https://loadspark.dev)
[![Website](https://img.shields.io/badge/website-loadspark.dev-orange.svg)](https://loadspark.dev)

**Quick Links:** [License](#license) • [Documentation](https://loadspark.dev/docs) • [Website](https://loadspark.dev)

</div>

---

**Supported Payload version: 3.69.0**

## Table of Contents

- [Introduction](#introduction)
- [Features](#features)
- [Getting Started](#getting-started)
  - [Installation](#installation)
  - [Basic Usage](#basic-usage)
- [Collections](#collections)
  - [Email Templates Collection](#email-templates-collection)
  - [Email Template Media Collection](#email-template-media-collection)
- [Validations](#validations)
  - [MJML Validation](#mjml-validation)
  - [MJML and HTML Changed Validation](#mjml-and-html-changed-validation)
- [Email Template Editor](#email-template-editor)
  - [How It Works](#how-it-works)
  - [Features](#editor-features)
- [Constants & Defaults](#constants--defaults)
  - [Default Template Variables](#default-template-variables)
  - [Default MJML Template](#default-mjml-template)
  - [Media Static Directory](#media-static-directory)
- [Templating System](#templating-system)
  - [Variable Syntax](#variable-syntax)
  - [Using Variables](#using-variables)
  - [Passing Variables via Hooks](#passing-variables-via-hooks)
- [Custom Fields](#custom-fields)
- [Translations](#translations)
- [Utilities](#utilities)
- [Overrides & Customization](#overrides--customization)
- [License](#license)

---

## Introduction

`@loadspark/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 loadspark 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:

```bash
# Using pnpm (recommended)
pnpm add @loadspark/email-templates

# Using npm
npm install @loadspark/email-templates

# Using yarn
yarn add @loadspark/email-templates
```

### Peer Dependencies

This plugin requires the following peer dependencies:

- `payload@3.69.0`
- `@payloadcms/translations@3.69.0`
- `@loadspark/core@0.0.4`

### Basic Usage

#### 1. Add the Plugin to Your Payload Config

```typescript
import { loadsparkEmailTemplatesPlugin } from '@loadspark/email-templates'
import { buildConfig } from 'payload'

export default buildConfig({
  plugins: [
    loadsparkEmailTemplatesPlugin({
      // 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

```typescript
import { substitueVariablesInString } from '@loadspark/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: 'john@example.com',
  },
  handler: {
    email: 'support@example.com',
    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/loadspark/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:**

```typescript
import { isValidCleanMjml } from '@loadspark/email-templates/validations'

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

**With Override:**

```typescript
{
  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:**

```typescript
import { bothMjmlAndHtmlChanged } from '@loadspark/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:

```typescript
import { DEFAULT_EMAIL_TEMPLATE_VARIABLES } from '@loadspark/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](#templating-system) for details.

### Default MJML Template

A default MJML template is provided for new templates:

```typescript
import { DEFAULT_EMAIL_TEMPLATE_MJML } from '@loadspark/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:

```typescript
import { DEFAULT_EMAIL_TEMPLATE_HTML } from '@loadspark/email-templates/constants'
```

### Media Static Directory

The static directory for email template media:

```typescript
import { EMAIL_TEMPLATE_MEDIA_STATIC_DIR } from '@loadspark/email-templates/constants'

// Value: 'files/loadspark/email-templates/media'
```

---

## Templating System

### Variable Syntax

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

```html
<%= it.variableName %>
```

For nested objects:

```html
<%= 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

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

#### Rendering the Template

```typescript
import { substitueVariablesInString } from '@loadspark/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:

```typescript
// hooks/emailService.ts
import { substitueVariablesInString } from '@loadspark/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: 'support@example.com',
      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

```typescript
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:

```typescript
import { escapeTags, unescapeTags } from '@loadspark/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:**

```typescript
import { RedirectToEditorButtonField } from '@loadspark/email-templates/fields'

// The field is already included, but you can customize it via overrides
loadsparkEmailTemplatesPlugin({
  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: `loadspark-emailTemplates:{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:

```typescript
import { loadsparkEmailTemplatesPlugin } from '@loadspark/email-templates'

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

### Extending Translations

You can extend or override translations in your Payload config:

```typescript
export default buildConfig({
  plugins: [loadsparkEmailTemplatesPlugin()],
  i18n: {
    translations: {
      'loadspark-emailTemplates:editor:saved': {
        en: 'Template saved!',
        de: 'Vorlage gespeichert!',
      },
    },
  },
})
```

---

## Utilities

### Template Variable Utilities

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

#### `getTemplateVariables(template)`

Extracts the variables array from a template document.

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

#### `substitueVariablesInString(input, variables)`

Renders a template string with variables using the Eta engine.

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

#### `escapeTags(input)`

Escapes Eta template tags for safe display.

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

#### `unescapeTags(input)`

Unescapes Eta template tags (automatically called during rendering).

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

---

## Overrides & Customization

The plugin is fully customizable at every level.

### Plugin-Level Overrides

```typescript
loadsparkEmailTemplatesPlugin({
  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:

```typescript
loadsparkEmailTemplatesPlugin({
  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:

```typescript
import { createEmailTemplatesAccess } from '@loadspark/email-templates/collections/email-templates/access'

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

---

## License

MIT