From 74f565ab4efc2aeea6479f2c5aa8032e85e75714 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 18:23:05 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=80=20BREAKING:=20Simplify=20API?= =?UTF-8?q?=20to=20use=20Payload=20collections=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove complex sendEmail/scheduleEmail methods and SendEmailOptions types - Add simple renderTemplate() helper for template rendering - Users now create emails using payload.create() with full type safety - Leverage Payload's existing collection system instead of duplicating functionality - Provide comprehensive migration guide and usage examples BREAKING CHANGES: - sendEmail() and scheduleEmail() methods removed - SendEmailOptions type removed - Use payload.create() with email collection instead - Use renderTemplate() helper for template rendering Benefits: ✅ Full TypeScript support with generated Payload types ✅ Use any custom fields in your email collection ✅ Leverage Payload's validation, hooks, and access control ✅ Simpler, more consistent API ✅ Less code to maintain 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package.json | 2 +- simplified-api-guide.md | 167 +++++++++++++++++++++++++++++++++ src/index.ts | 3 +- src/services/MailingService.ts | 78 +++------------ src/types/index.ts | 21 +---- src/utils/helpers.ts | 11 +-- 6 files changed, 192 insertions(+), 90 deletions(-) create mode 100644 simplified-api-guide.md diff --git a/package.json b/package.json index 94d8c49..71c4844 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.0.12", + "version": "0.1.0", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/simplified-api-guide.md b/simplified-api-guide.md new file mode 100644 index 0000000..3a0bb12 --- /dev/null +++ b/simplified-api-guide.md @@ -0,0 +1,167 @@ +# Simplified API Guide + +The mailing plugin now uses a much simpler, type-safe API that leverages PayloadCMS's existing collection system instead of custom email methods. + +## New API Approach + +### ✅ **Recommended: Use Payload Collections Directly** + +```typescript +import { payload, renderTemplate } from '@xtr-dev/payload-mailing' + +// 1. Render a template (optional) +const rendered = await renderTemplate(payload, 'welcome-email', { + name: 'John Doe', + activationLink: 'https://example.com/activate/123' +}) + +// 2. Create an email using Payload's collection API +const email = await payload.create({ + collection: 'emails', // Your email collection name + data: { + to: ['user@example.com'], + subject: rendered.subject, // or your own subject + html: rendered.html, // or your own HTML + text: rendered.text, // or your own text + // Add any custom fields you've defined in your collection + priority: 1, + scheduledAt: new Date('2024-01-01T10:00:00Z'), // Optional scheduling + } +}) +``` + +### ❌ **Old API (Removed)** + +```typescript +// These methods have been removed to simplify the API +await sendEmail(payload, { to: '...', subject: '...' }) +await scheduleEmail(payload, { to: '...', scheduledAt: new Date() }) +``` + +## Benefits of the New Approach + +### 🎯 **Type Safety** +- Full TypeScript support using your generated Payload types +- IntelliSense for all your custom collection fields +- Compile-time validation of email data + +### 🚀 **Flexibility** +- Use any fields you've added to your email collection +- Leverage Payload's built-in validation, hooks, and access control +- Full control over email data structure + +### 🧹 **Simplicity** +- One consistent API (Payload collections) instead of custom methods +- No duplicate type definitions +- Less code to maintain + +## Usage Examples + +### Basic Email with Template +```typescript +import { renderTemplate } from '@xtr-dev/payload-mailing' + +const { html, text, subject } = await renderTemplate(payload, 'order-confirmation', { + orderNumber: '#12345', + customerName: 'Jane Smith', + items: [ + { name: 'Product 1', price: 29.99 }, + { name: 'Product 2', price: 49.99 } + ] +}) + +await payload.create({ + collection: 'emails', + data: { + to: ['customer@example.com'], + subject, + html, + text, + priority: 2, + // Add your custom fields + orderId: '12345', + customerSegment: 'premium' + } +}) +``` + +### Bulk Email Creation +```typescript +const customers = await payload.find({ + collection: 'customers', + where: { newsletter: { equals: true } } +}) + +for (const customer of customers.docs) { + const { html, text, subject } = await renderTemplate(payload, 'newsletter', { + name: customer.name, + unsubscribeLink: `https://example.com/unsubscribe/${customer.id}` + }) + + await payload.create({ + collection: 'emails', + data: { + to: [customer.email], + subject, + html, + text, + scheduledAt: new Date('2024-01-15T09:00:00Z'), // Send next week + } + }) +} +``` + +### Direct HTML Email (No Template) +```typescript +await payload.create({ + collection: 'emails', + data: { + to: ['admin@example.com'], + subject: 'System Alert', + html: '

Server Error

Please check the logs immediately.

', + text: 'Server Error: Please check the logs immediately.', + priority: 10, // High priority + } +}) +``` + +## Available Helper Functions + +```typescript +import { + renderTemplate, // Render email templates with variables + processEmails, // Process pending emails manually + retryFailedEmails, // Retry failed emails + getMailing // Get mailing service instance +} from '@xtr-dev/payload-mailing' +``` + +## Migration Guide + +If you were using the old `sendEmail`/`scheduleEmail` methods, update your code: + +**Before:** +```typescript +await sendEmail(payload, { + templateSlug: 'welcome', + to: 'user@example.com', + variables: { name: 'John' } +}) +``` + +**After:** +```typescript +const { html, text, subject } = await renderTemplate(payload, 'welcome', { name: 'John' }) + +await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject, + html, + text + } +}) +``` + +This new approach gives you the full power and type safety of PayloadCMS while keeping the mailing functionality simple and consistent! 🚀 \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 226aed5..74cc6bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,7 @@ export { default as Emails } from './collections/Emails.js' // Utility functions for developers export { getMailing, - sendEmail, - scheduleEmail, + renderTemplate, processEmails, retryFailedEmails, } from './utils/helpers.js' \ No newline at end of file diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts index adfc803..f835cba 100644 --- a/src/services/MailingService.ts +++ b/src/services/MailingService.ts @@ -3,7 +3,7 @@ import { Liquid } from 'liquidjs' import nodemailer, { Transporter } from 'nodemailer' import { MailingPluginConfig, - SendEmailOptions, + TemplateVariables, MailingService as IMailingService, EmailTemplate, QueuedEmail, @@ -107,69 +107,21 @@ export class MailingService implements IMailingService { } } - async sendEmail(options: SendEmailOptions): Promise { - const emailId = await this.scheduleEmail({ - ...options, - scheduledAt: new Date() - }) + async renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> { + const template = await this.getTemplateBySlug(templateSlug) - await this.processEmailItem(emailId) - - return emailId - } - - async scheduleEmail(options: SendEmailOptions): Promise { - let html = options.html || '' - let text = options.text || '' - let subject = options.subject || '' - let templateId: string | undefined = undefined - - if (options.templateSlug) { - const template = await this.getTemplateBySlug(options.templateSlug) - - if (template) { - templateId = template.id - const variables = options.variables || {} - const renderedContent = await this.renderEmailTemplate(template, variables) - html = renderedContent.html - text = renderedContent.text - subject = await this.renderTemplate(template.subject, variables) - } else { - throw new Error(`Email template not found: ${options.templateSlug}`) - } + if (!template) { + throw new Error(`Email template not found: ${templateSlug}`) } - if (!subject && !options.subject) { - throw new Error('Email subject is required') + const emailContent = await this.renderEmailTemplate(template, variables) + const subject = await this.renderTemplateString(template.subject, variables) + + return { + html: emailContent.html, + text: emailContent.text, + subject } - - if (!html && !options.html) { - throw new Error('Email HTML content is required') - } - - const queueData = { - template: templateId, - to: Array.isArray(options.to) ? options.to : [options.to], - cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : undefined, - bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : undefined, - from: options.from || this.getDefaultFrom(), - replyTo: options.replyTo, - subject: subject || options.subject, - html, - text, - variables: options.variables, - scheduledAt: options.scheduledAt?.toISOString(), - status: 'pending' as const, - attempts: 0, - priority: options.priority || 5, - } - - const result = await this.payload.create({ - collection: this.emailsCollection as any, - data: queueData, - }) - - return result.id as string } async processEmails(): Promise { @@ -366,7 +318,7 @@ export class MailingService implements IMailingService { } } - private async renderTemplate(template: string, variables: Record): Promise { + private async renderTemplateString(template: string, variables: Record): Promise { // Use custom template renderer if provided if (this.config.templateRenderer) { try { @@ -434,8 +386,8 @@ export class MailingService implements IMailingService { let text = serializeRichTextToText(template.content) // Apply template variables to the rendered content - html = await this.renderTemplate(html, variables) - text = await this.renderTemplate(text, variables) + html = await this.renderTemplateString(html, variables) + text = await this.renderTemplateString(text, variables) return { html, text } } diff --git a/src/types/index.ts b/src/types/index.ts index 14bb179..b29b150 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ import { Payload } from 'payload' -import type { CollectionConfig, RichTextField } from 'payload' +import type { CollectionConfig, RichTextField, TypedCollection } from 'payload' import { Transporter } from 'nodemailer' export interface EmailObject { @@ -83,26 +83,15 @@ export interface QueuedEmail { updatedAt: string } -export interface SendEmailOptions { - templateSlug?: string - to: string | string[] - cc?: string | string[] - bcc?: string | string[] - from?: string - replyTo?: string - subject?: string - html?: string - text?: string - variables?: Record - scheduledAt?: Date - priority?: number +// Simple helper type for template variables +export interface TemplateVariables { + [key: string]: any } export interface MailingService { - sendEmail(options: SendEmailOptions): Promise - scheduleEmail(options: SendEmailOptions): Promise processEmails(): Promise retryFailedEmails(): Promise + renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> } export interface MailingContext { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 38212f6..1e37e48 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,5 @@ import { Payload } from 'payload' -import { SendEmailOptions } from '../types/index.js' +import { TemplateVariables } from '../types/index.js' export const getMailing = (payload: Payload) => { const mailing = (payload as any).mailing @@ -9,14 +9,9 @@ export const getMailing = (payload: Payload) => { return mailing } -export const sendEmail = async (payload: Payload, options: SendEmailOptions): Promise => { +export const renderTemplate = async (payload: Payload, templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> => { const mailing = getMailing(payload) - return mailing.service.sendEmail(options) -} - -export const scheduleEmail = async (payload: Payload, options: SendEmailOptions): Promise => { - const mailing = getMailing(payload) - return mailing.service.scheduleEmail(options) + return mailing.service.renderTemplate(templateSlug, variables) } export const processEmails = async (payload: Payload): Promise => { From 135fddc6fad886f806b488b19fd7e45f95d2324b Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 18:25:06 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A5=20REMOVE:=20Deprecate=20outdat?= =?UTF-8?q?ed=20documentation=20for=20simplified=20API=20and=20template=20?= =?UTF-8?q?engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete `simplified-api-guide.md` and `template-syntax-migration.md` - Content now covered in more streamlined and up-to-date guides - Declutter repository by removing redundant files Co-Authored-By: Claude --- simplified-api-guide.md | 167 ----------------------------------- template-syntax-migration.md | 160 --------------------------------- 2 files changed, 327 deletions(-) delete mode 100644 simplified-api-guide.md delete mode 100644 template-syntax-migration.md diff --git a/simplified-api-guide.md b/simplified-api-guide.md deleted file mode 100644 index 3a0bb12..0000000 --- a/simplified-api-guide.md +++ /dev/null @@ -1,167 +0,0 @@ -# Simplified API Guide - -The mailing plugin now uses a much simpler, type-safe API that leverages PayloadCMS's existing collection system instead of custom email methods. - -## New API Approach - -### ✅ **Recommended: Use Payload Collections Directly** - -```typescript -import { payload, renderTemplate } from '@xtr-dev/payload-mailing' - -// 1. Render a template (optional) -const rendered = await renderTemplate(payload, 'welcome-email', { - name: 'John Doe', - activationLink: 'https://example.com/activate/123' -}) - -// 2. Create an email using Payload's collection API -const email = await payload.create({ - collection: 'emails', // Your email collection name - data: { - to: ['user@example.com'], - subject: rendered.subject, // or your own subject - html: rendered.html, // or your own HTML - text: rendered.text, // or your own text - // Add any custom fields you've defined in your collection - priority: 1, - scheduledAt: new Date('2024-01-01T10:00:00Z'), // Optional scheduling - } -}) -``` - -### ❌ **Old API (Removed)** - -```typescript -// These methods have been removed to simplify the API -await sendEmail(payload, { to: '...', subject: '...' }) -await scheduleEmail(payload, { to: '...', scheduledAt: new Date() }) -``` - -## Benefits of the New Approach - -### 🎯 **Type Safety** -- Full TypeScript support using your generated Payload types -- IntelliSense for all your custom collection fields -- Compile-time validation of email data - -### 🚀 **Flexibility** -- Use any fields you've added to your email collection -- Leverage Payload's built-in validation, hooks, and access control -- Full control over email data structure - -### 🧹 **Simplicity** -- One consistent API (Payload collections) instead of custom methods -- No duplicate type definitions -- Less code to maintain - -## Usage Examples - -### Basic Email with Template -```typescript -import { renderTemplate } from '@xtr-dev/payload-mailing' - -const { html, text, subject } = await renderTemplate(payload, 'order-confirmation', { - orderNumber: '#12345', - customerName: 'Jane Smith', - items: [ - { name: 'Product 1', price: 29.99 }, - { name: 'Product 2', price: 49.99 } - ] -}) - -await payload.create({ - collection: 'emails', - data: { - to: ['customer@example.com'], - subject, - html, - text, - priority: 2, - // Add your custom fields - orderId: '12345', - customerSegment: 'premium' - } -}) -``` - -### Bulk Email Creation -```typescript -const customers = await payload.find({ - collection: 'customers', - where: { newsletter: { equals: true } } -}) - -for (const customer of customers.docs) { - const { html, text, subject } = await renderTemplate(payload, 'newsletter', { - name: customer.name, - unsubscribeLink: `https://example.com/unsubscribe/${customer.id}` - }) - - await payload.create({ - collection: 'emails', - data: { - to: [customer.email], - subject, - html, - text, - scheduledAt: new Date('2024-01-15T09:00:00Z'), // Send next week - } - }) -} -``` - -### Direct HTML Email (No Template) -```typescript -await payload.create({ - collection: 'emails', - data: { - to: ['admin@example.com'], - subject: 'System Alert', - html: '

Server Error

Please check the logs immediately.

', - text: 'Server Error: Please check the logs immediately.', - priority: 10, // High priority - } -}) -``` - -## Available Helper Functions - -```typescript -import { - renderTemplate, // Render email templates with variables - processEmails, // Process pending emails manually - retryFailedEmails, // Retry failed emails - getMailing // Get mailing service instance -} from '@xtr-dev/payload-mailing' -``` - -## Migration Guide - -If you were using the old `sendEmail`/`scheduleEmail` methods, update your code: - -**Before:** -```typescript -await sendEmail(payload, { - templateSlug: 'welcome', - to: 'user@example.com', - variables: { name: 'John' } -}) -``` - -**After:** -```typescript -const { html, text, subject } = await renderTemplate(payload, 'welcome', { name: 'John' }) - -await payload.create({ - collection: 'emails', - data: { - to: ['user@example.com'], - subject, - html, - text - } -}) -``` - -This new approach gives you the full power and type safety of PayloadCMS while keeping the mailing functionality simple and consistent! 🚀 \ No newline at end of file diff --git a/template-syntax-migration.md b/template-syntax-migration.md deleted file mode 100644 index 03c3957..0000000 --- a/template-syntax-migration.md +++ /dev/null @@ -1,160 +0,0 @@ -# Template Engine Options - -The plugin now supports flexible template rendering with multiple options: - -1. **String-based Configuration** (easy setup with built-in engines) -2. **Custom Template Renderer Hook** (maximum flexibility) -3. **Simple Variable Replacement** (fallback, no dependencies) - -## Configuration Options - -### String-based Template Engine Configuration -Easy setup using built-in template engines: - -```typescript -// Using LiquidJS (default, requires: npm install liquidjs) -mailingPlugin({ - templateEngine: 'liquidjs' -}) - -// Using Mustache (requires: npm install mustache) -mailingPlugin({ - templateEngine: 'mustache' -}) - -// Using simple variable replacement (no dependencies) -mailingPlugin({ - templateEngine: 'simple' -}) -``` - -### Custom Template Renderer Hook -```typescript -// Example with Handlebars -import Handlebars from 'handlebars' - -mailingPlugin({ - templateRenderer: async (template: string, variables: Record) => { - const compiled = Handlebars.compile(template) - return compiled(variables) - } -}) - -// Example with Mustache -import Mustache from 'mustache' - -mailingPlugin({ - templateRenderer: async (template: string, variables: Record) => { - return Mustache.render(template, variables) - } -}) - -// Example with Nunjucks -import nunjucks from 'nunjucks' - -mailingPlugin({ - templateRenderer: async (template: string, variables: Record) => { - return nunjucks.renderString(template, variables) - } -}) -``` - -### Using LiquidJS (Optional) -Install the optional dependency: -```bash -npm install liquidjs -# or -pnpm add liquidjs -``` - -### Fallback Mode -If no custom renderer is provided and neither LiquidJS nor Mustache are installed, simple `{{variable}}` replacement is used. - -## Template Syntax Reference - -### Mustache Syntax (Logic-less) -```mustache -Hello {{user.name}}, - -{{#user.isPremium}} - Welcome to premium! Your balance is {{balance}}. -{{/user.isPremium}} - -{{#orders}} - Order: {{id}} - {{date}} -{{/orders}} -``` - -### LiquidJS Syntax (With Logic) -```liquid -Hello {{user.name}}, - -{% if user.isPremium %} - Welcome to premium! Your balance is {{balance | formatCurrency}}. -{% endif %} - -{% for order in orders %} - Order: {{order.id}} - {{order.date | formatDate: "short"}} -{% endfor %} -``` - -### Simple Variable Replacement -``` -Hello {{user.name}}, -Your balance is {{balance}}. -``` - -## Migration from Handlebars - -### Variables -- **Handlebars**: `{{variable}}` -- **LiquidJS**: `{{variable}}` (same) - -### Conditionals -- **Handlebars**: `{{#if condition}}content{{/if}}` -- **LiquidJS**: `{% if condition %}content{% endif %}` - -### Loops -- **Handlebars**: `{{#each items}}{{this}}{{/each}}` -- **LiquidJS**: `{% for item in items %}{{item}}{% endfor %}` - -### Filters/Helpers -- **Handlebars**: `{{formatDate date "short"}}` -- **LiquidJS**: `{{date | formatDate: "short"}}` - -### Available Filters -- `formatDate` - Format dates (short, long, or default) -- `formatCurrency` - Format currency amounts -- `capitalize` - Capitalize first letter - -### Comparison Operations (LiquidJS Advantage) -- **Handlebars**: Required `{{#ifEquals}}` helper -- **LiquidJS**: Built-in: `{% if user.role == "admin" %}` - -## Example Migration - -### Before (Handlebars) -```handlebars -Hello {{user.name}}, - -{{#if user.isPremium}} - Welcome to premium! Your balance is {{formatCurrency balance}}. -{{/if}} - -{{#each orders}} - Order: {{this.id}} - {{formatDate this.date "short"}} -{{/each}} -``` - -### After (LiquidJS) -```liquid -Hello {{user.name}}, - -{% if user.isPremium %} - Welcome to premium! Your balance is {{balance | formatCurrency}}. -{% endif %} - -{% for order in orders %} - Order: {{order.id}} - {{order.date | formatDate: "short"}} -{% endfor %} -``` \ No newline at end of file From 804a63647aa9bc793832a00a74542dacfcd444ae Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 13 Sep 2025 18:33:21 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Update=20README=20fo?= =?UTF-8?q?r=20v0.1.0=20API=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all outdated API examples (sendEmail, scheduleEmail) - Add comprehensive examples using new payload.create() approach - Include template engine configuration options (LiquidJS, Mustache, custom) - Add detailed migration guide from v0.0.x to v0.1.0 - Update feature list to highlight type safety and Payload integration - Replace old API methods section with helper functions - Add template syntax reference for all supported engines - Update Recent Changes section with v0.1.0 breaking changes The README now accurately reflects the simplified collection-based API and provides clear migration paths for existing users. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 333 +++++++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 218 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index abe6e84..4b917c8 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,17 @@ 📧 **Template-based email system with scheduling and job processing for PayloadCMS** -⚠️ **Pre-release Warning**: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use. +✨ **Simplified API**: Starting from v0.1.0, this plugin uses a simplified API that leverages PayloadCMS collections directly for better type safety and flexibility. ## Features -✅ **Template System**: Create reusable email templates with Handlebars syntax -✅ **Outbox Scheduling**: Schedule emails for future delivery -✅ **Job Integration**: Automatic processing via PayloadCMS jobs queue -✅ **Retry Failed Sends**: Automatic retry mechanism for failed emails -✅ **Template Variables**: Dynamic content with validation -✅ **Developer API**: Simple methods for sending emails programmatically +✅ **Template System**: Create reusable email templates with LiquidJS, Mustache, or simple variables +✅ **Type Safety**: Full TypeScript support using your generated Payload types +✅ **Flexible Template Engines**: LiquidJS, Mustache, or bring your own template renderer +✅ **Email Scheduling**: Schedule emails for future delivery using Payload collections +✅ **Job Integration**: Automatic processing via PayloadCMS jobs queue +✅ **Retry Failed Sends**: Automatic retry mechanism for failed emails +✅ **Payload Native**: Uses Payload collections directly - no custom APIs to learn ## Installation @@ -49,53 +50,124 @@ export default buildConfig({ }) ``` -### 2. Send emails in your code +### 2. Send emails using Payload collections ```typescript -import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' +import { renderTemplate } from '@xtr-dev/payload-mailing' -// Send immediately using template slug -const emailId = await sendEmail(payload, { - templateSlug: 'welcome-email', - to: 'user@example.com', - variables: { - firstName: 'John', - welcomeUrl: 'https://yoursite.com/welcome' +// Option 1: Using templates with variables +const { html, text, subject } = await renderTemplate(payload, 'welcome-email', { + firstName: 'John', + welcomeUrl: 'https://yoursite.com/welcome' +}) + +// Create email using Payload's collection API (full type safety!) +const email = await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject, + html, + text, + // Schedule for later (optional) + scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + // Add any custom fields you've defined + priority: 1, + customField: 'your-value', // Your custom collection fields work! } }) -// Schedule for later -const scheduledId = await scheduleEmail(payload, { - templateSlug: 'reminder-email', - to: 'user@example.com', - scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours - variables: { - eventName: 'Product Launch', - eventDate: new Date('2024-01-15') +// Option 2: Direct HTML email (no template needed) +const directEmail = await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject: 'Welcome!', + html: '

Welcome John!

Thanks for joining!

', + text: 'Welcome John! Thanks for joining!', } }) ``` +## Why This Approach is Better + +- ✅ **Full Type Safety**: Use your generated Payload types +- ✅ **No Learning Curve**: Just use `payload.create()` like any collection +- ✅ **Maximum Flexibility**: Add any custom fields to your email collection +- ✅ **Payload Integration**: Leverage validation, hooks, access control +- ✅ **Consistent API**: One way to create data in Payload + ## Configuration ### Plugin Options ```typescript -interface MailingPluginConfig { - collections?: { - templates?: string // default: 'email-templates' - emails?: string // default: 'emails' +mailingPlugin({ + // Template engine (optional) + templateEngine: 'liquidjs', // 'liquidjs' | 'mustache' | 'simple' + + // Custom template renderer (optional) + templateRenderer: async (template: string, variables: Record) => { + return yourCustomEngine.render(template, variables) + }, + + // Email transport + transport: { + host: 'smtp.gmail.com', + port: 587, + auth: { user: '...', pass: '...' } + }, + + // Collection names (optional) + collections: { + templates: 'email-templates', // default + emails: 'emails' // default + }, + + // Sending options + defaultFrom: 'noreply@yoursite.com', + defaultFromName: 'Your Site', + retryAttempts: 3, // default + retryDelay: 300000, // 5 minutes (default) + + // Advanced options + emailWrapper: (email) => ({ // optional layout wrapper + ...email, + html: `${email.html}` + }), + richTextEditor: lexicalEditor(), // optional custom editor + onReady: async (payload) => { // optional initialization hook + console.log('Mailing plugin ready!') } - defaultFrom?: string - transport?: Transporter | MailingTransportConfig - queue?: string // default: 'default' - retryAttempts?: number // default: 3 - retryDelay?: number // default: 300000 (5 minutes) - emailWrapper?: EmailWrapperHook // optional email layout wrapper - richTextEditor?: RichTextField['editor'] // optional custom rich text editor - onReady?: (payload: any) => Promise // optional callback after plugin initialization - initOrder?: 'before' | 'after' // default: 'before' -} +}) +``` + +### Template Engine Options + +Choose your preferred template engine: + +```typescript +// LiquidJS (default) - Modern syntax with logic +mailingPlugin({ + templateEngine: 'liquidjs' // {% if user.isPremium %}Premium!{% endif %} +}) + +// Mustache - Logic-less templates +mailingPlugin({ + templateEngine: 'mustache' // {{#user.isPremium}}Premium!{{/user.isPremium}} +}) + +// Simple variable replacement +mailingPlugin({ + templateEngine: 'simple' // Just {{variable}} replacement +}) + +// Custom template renderer +mailingPlugin({ + templateRenderer: async (template, variables) => { + return handlebars.compile(template)(variables) // Bring your own! + } +}) ``` ### Transport Configuration @@ -473,71 +545,50 @@ mailingPlugin({ }) ``` -## Handlebars Helpers +## Template Syntax Reference -The plugin includes several built-in helpers: +Depending on your chosen template engine, you can use different syntax: -- `{{formatDate date 'short'}}` - Format dates (short, long, or default) -- `{{formatCurrency amount 'USD'}}` - Format currency -- `{{capitalize string}}` - Capitalize first letter -- `{{#ifEquals value1 value2}}...{{/ifEquals}}` - Conditional equality +### LiquidJS (Default) +- Variables: `{{ user.name }}` +- Logic: `{% if user.isPremium %}Premium content{% endif %}` +- Loops: `{% for item in items %}{{ item.name }}{% endfor %}` +- Filters: `{{ amount | formatCurrency }}`, `{{ date | formatDate: "short" }}` -## API Methods +### Mustache +- Variables: `{{ user.name }}` +- Logic: `{{#user.isPremium}}Premium content{{/user.isPremium}}` +- Loops: `{{#items}}{{ name }}{{/items}}` +- No built-in filters (use variables with pre-formatted data) -### sendEmail(payload, options) +### Simple +- Variables only: `{{ user.name }}`, `{{ amount }}`, `{{ date }}` -Send an email immediately: +### Built-in Filters (LiquidJS only) +- `formatDate` - Format dates: `{{ createdAt | formatDate: "short" }}` +- `formatCurrency` - Format currency: `{{ amount | formatCurrency: "USD" }}` +- `capitalize` - Capitalize first letter: `{{ name | capitalize }}` + +## Available Helper Functions ```typescript -const emailId = await sendEmail(payload, { - templateSlug: 'order-confirmation', // optional - use template slug - to: ['customer@example.com'], // string or array of emails - cc: ['manager@example.com'], // optional - array of emails - bcc: ['archive@example.com'], // optional - array of emails - from: 'orders@yoursite.com', // optional, uses default - replyTo: 'support@yoursite.com', // optional - subject: 'Custom subject', // required if no template - html: '

Custom HTML

', // required if no template - text: 'Custom text version', // optional - variables: { // template variables - orderNumber: '12345', - customerName: 'John Doe' - }, - priority: 1 // optional, 1-10 (1 = highest) +import { + renderTemplate, // Render email templates with variables + processEmails, // Process pending emails manually + retryFailedEmails, // Retry failed emails + getMailing // Get mailing service instance +} from '@xtr-dev/payload-mailing' + +// Render a template +const { html, text, subject } = await renderTemplate(payload, 'welcome', { + name: 'John', + activationUrl: 'https://example.com/activate' }) -``` -### scheduleEmail(payload, options) - -Schedule an email for later delivery: - -```typescript -const emailId = await scheduleEmail(payload, { - templateSlug: 'newsletter', - to: ['user1@example.com', 'user2@example.com'], - scheduledAt: new Date('2024-01-15T10:00:00Z'), - variables: { - month: 'January', - highlights: ['Feature A', 'Feature B'] - } -}) -``` - -### processEmails(payload) - -Manually process pending emails: - -```typescript -import { processEmails } from '@xtr-dev/payload-mailing' +// Process emails manually await processEmails(payload) -``` -### retryFailedEmails(payload) - -Manually retry failed emails: - -```typescript -import { retryFailedEmails } from '@xtr-dev/payload-mailing' +// Retry failed emails await retryFailedEmails(payload) ``` @@ -716,35 +767,85 @@ import { } from '@xtr-dev/payload-mailing' ``` +## Migration Guide (v0.0.x → v0.1.0) + +**🚨 BREAKING CHANGES**: The API has been simplified to use Payload collections directly. + +### Before (v0.0.x) +```typescript +import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' + +// Old way +const emailId = await sendEmail(payload, { + templateSlug: 'welcome', + to: 'user@example.com', + variables: { name: 'John' } +}) + +const scheduledId = await scheduleEmail(payload, { + templateSlug: 'reminder', + to: 'user@example.com', + scheduledAt: new Date('2024-01-15T10:00:00Z'), + variables: { eventName: 'Launch' } +}) +``` + +### After (v0.1.0+) +```typescript +import { renderTemplate } from '@xtr-dev/payload-mailing' + +// New way - render template first +const { html, text, subject } = await renderTemplate(payload, 'welcome', { + name: 'John' +}) + +// Then create email using Payload collections (full type safety!) +const email = await payload.create({ + collection: 'emails', + data: { + to: ['user@example.com'], + subject, + html, + text, + // For scheduling + scheduledAt: new Date('2024-01-15T10:00:00Z'), + // Add any custom fields from your collection + customField: 'value', + } +}) +``` + +### Benefits of Migration +- ✅ **Full TypeScript support** with your generated Payload types +- ✅ **Use any custom fields** you add to your email collection +- ✅ **Leverage Payload's features**: validation, hooks, access control +- ✅ **One consistent API** - just use `payload.create()` +- ✅ **No wrapper methods** - direct access to Payload's power + ## Recent Changes -### v0.0.x (Latest) +### v0.1.0 (Latest - Breaking Changes) -**🔄 Breaking Changes:** -- Removed email layouts system in favor of `emailWrapper` hook for better flexibility -- Email fields (`to`, `cc`, `bcc`) now use `hasMany: true` for proper array handling -- Templates now use slug-based lookup instead of ID-based for developer-friendly API -- Email collection renamed from "outbox" to "emails" -- Unified job processing: single `process-email-queue` job handles both pending and failed emails +**🚀 Major API Simplification:** +- **REMOVED**: `sendEmail()` and `scheduleEmail()` wrapper methods +- **REMOVED**: `SendEmailOptions` custom types +- **ADDED**: Direct Payload collection usage with full type safety +- **ADDED**: `renderTemplate()` helper for template rendering +- **ADDED**: Support for LiquidJS, Mustache, and custom template engines +- **IMPROVED**: Webpack compatibility with proper dynamic imports -**✨ New Features:** -- Rich text editor with automatic HTML/text conversion -- Template slugs for easier template reference -- `emailWrapper` hook for consistent email layouts -- Custom rich text editor configuration support -- Initialization hooks (`onReady`, `initOrder`) for better plugin lifecycle control -- Improved Handlebars variable interpolation with defensive programming +**Template Engine Enhancements:** +- **NEW**: LiquidJS support (default) with modern syntax and logic +- **NEW**: Mustache support for logic-less templates +- **NEW**: Custom template renderer hook for maximum flexibility +- **NEW**: Simple variable replacement as fallback +- **FIXED**: All webpack compatibility issues resolved -**🐛 Bug Fixes:** -- Fixed text version uppercase conversion in headings -- Fixed Handlebars interpolation issues in text version -- Improved plugin initialization order to prevent timing issues - -**💡 Improvements:** -- Better admin UI with proper array input controls -- More robust error handling and logging -- Enhanced TypeScript definitions -- Simplified template creation workflow +**Developer Experience:** +- **IMPROVED**: Full TypeScript inference using generated Payload types +- **IMPROVED**: Comprehensive migration guide and documentation +- **IMPROVED**: Better error handling and async patterns +- **SIMPLIFIED**: Cleaner codebase with fewer abstractions ## License diff --git a/package.json b/package.json index 71c4844..27d97aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.1.0", + "version": "0.1.1", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js",