# Payload Newsletter Plugin

[![npm version](https://img.shields.io/npm/v/payload-plugin-newsletter.svg?cache=300)](https://www.npmjs.com/package/payload-plugin-newsletter)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A complete newsletter management plugin for [Payload CMS](https://github.com/payloadcms/payload) that provides subscriber management, magic link authentication, and email service integration out of the box.

> **Important**: Version 0.8.7+ includes critical fixes for Payload v3 compatibility. If you're using Payload v3, please ensure you're on at least version 0.8.7 of this plugin.

## Features

- 📧 **Complete Subscriber Management** - Ready-to-use subscriber collection with all essential fields
- 🔐 **Magic Link Authentication** - Passwordless authentication for subscribers (separate from Payload auth)
- 📨 **Email Service Integration** - Built-in support for Resend and Broadcast
- 📅 **Newsletter Scheduling** - Schedule newsletters from your articles collection
- ⚛️ **React Components** - Pre-built signup forms and preference management UI
- 🌍 **Internationalization** - Multi-language support built-in
- 📊 **Analytics Ready** - UTM tracking and signup metadata collection
- ⚙️ **Admin UI Configuration** - Manage email settings through Payload admin panel
- 🔄 **Real-time Webhook Sync** - Receive subscriber and broadcast events from email services via webhooks
- 👁️ **Email Preview** - Real-time preview with desktop/mobile views (v0.9.0+)
- ✅ **Email Validation** - Built-in validation for email client compatibility (v0.9.0+)
- 📝 **Email-Safe Editor** - Rich text editor limited to email-compatible features (v0.9.0+)
- 📬 **Broadcast Management** - Create and send email campaigns with provider sync (v0.10.0+)
- 🎨 **React Email Templates** - Customizable email templates with React Email (v0.12.0+)

## Prerequisites

- Payload CMS v3.0.0 or higher
- A Media collection configured in your Payload project (required for image support in broadcasts)

## Quick Start

### 1. Install the plugin

```bash
bun add payload-plugin-newsletter
# or
npm install payload-plugin-newsletter
# or
yarn add payload-plugin-newsletter
# or
pnpm add payload-plugin-newsletter
```

### 2. Add to your Payload config

```typescript
import { buildConfig } from 'payload/config'
import { newsletterPlugin } from 'payload-plugin-newsletter'

export default buildConfig({
  plugins: [
    newsletterPlugin({
      // Choose your email provider
      providers: {
        default: 'resend', // or 'broadcast'
        resend: {
          apiKey: process.env.RESEND_API_KEY,
          fromAddress: 'hello@yoursite.com',
          fromName: 'Your Newsletter',
          audienceIds: {
            en: {
              production: 'your_audience_id',
              development: 'your_dev_audience_id',
            },
          },
        },
      },
    }),
  ],
  // ... rest of your config
})
```

### 3. That's it! 🎉

The plugin automatically adds:
- A `subscribers` collection to manage your subscribers
- A `newsletter-settings` collection for email configurations (supports multiple environments)
- API endpoints for subscription and authentication
- Newsletter scheduling fields to your articles (optional)

## Basic Usage

### Frontend Integration

#### Simple Newsletter Signup Form

```tsx
import { NewsletterForm } from 'payload-plugin-newsletter/components'

export function MyHomepage() {
  return (
    <NewsletterForm 
      onSuccess={() => console.log('Subscribed!')}
      onError={(error) => console.error(error)}
    />
  )
}
```

#### Custom Signup Form

```tsx
async function handleSubscribe(email: string) {
  const response = await fetch('/api/newsletter/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  })
  
  if (!response.ok) {
    throw new Error('Subscription failed')
  }
  
  return response.json()
}
```

### Managing Subscribers

Subscribers can be managed through the Payload admin panel at `/admin/collections/subscribers`.

### Email Settings

After setup, configure email settings at `/admin/collections/newsletter-settings` in your admin panel. You can:
- Create multiple configurations (e.g., for different environments or purposes)
- Set one configuration as active at a time
- Switch between email providers
- Update API keys and settings
- Customize email templates
- Set subscription preferences

**Note**: Only one configuration can be active at a time. The plugin will use the active configuration for sending emails.

## Initial Setup

After installing the plugin, you'll need to:

1. **Create an email configuration**:
   - Go to `/admin/collections/newsletter-settings`
   - Click "Create New"
   - Give it a name (e.g., "Production" or "Development")
   - Configure your email provider settings
   - Set it as "Active"
   - Save

2. **Start collecting subscribers**:
   - Subscribers will appear in `/admin/collections/subscribers`
   - Use the provided React components or API endpoints

## Email Preview Features (v0.9.0+)

The plugin includes comprehensive email preview functionality to ensure your newsletters look great across all email clients.

### Email-Safe Rich Text Editor

The plugin provides a pre-configured Lexical editor with only email-compatible features:

```typescript
import { createEmailContentField } from 'payload-plugin-newsletter/fields'

const BroadcastsCollection = {
  fields: [
    createEmailContentField({
      name: 'content',
      required: true,
    })
  ]
}
```

Features included:
- Basic text formatting (bold, italic, underline, strikethrough)
- Simple links
- Ordered and unordered lists
- Headings (H1, H2, H3)
- Text alignment
- Blockquotes

### Real-Time Email Preview

The plugin includes a preview component that shows how your email will look:

```typescript
{
  name: 'preview',
  type: 'ui',
  admin: {
    components: {
      Field: 'payload-plugin-newsletter/components/EmailPreviewField'
    }
  }
}
```

Preview features:
- **Desktop & Mobile Views** - Switch between viewport sizes
- **Live Updates** - See changes as you type
- **Validation Warnings** - Catch compatibility issues before sending
- **Test Email** - Send a test to your inbox

### Email HTML Validation

Built-in validation checks for:
- HTML size limits (Gmail's 102KB limit)
- Unsupported CSS properties
- Missing alt text on images
- External resources that won't load
- JavaScript that will be stripped

## Broadcast Management (v0.10.0+)

Create and send email campaigns directly from Payload:

### Enable Broadcasts

```typescript
newsletterPlugin({
  features: {
    newsletterManagement: {
      enabled: true,
    }
  },
  providers: {
    default: 'broadcast',
    broadcast: {
      apiUrl: process.env.BROADCAST_API_URL,
      token: process.env.BROADCAST_TOKEN,
      fromAddress: 'newsletter@yoursite.com',
      fromName: 'Your Newsletter',
    }
  }
})
```

This adds a `broadcasts` collection with:
- Rich text editor with email-safe formatting
- Image uploads with Media collection integration
- Custom email blocks (buttons, dividers)
- Inline email preview with React Email
- Automatic sync with your email provider
- Draft/publish system with scheduled publishing support

### Send = Publish Workflow

The plugin integrates seamlessly with Payload's draft/publish system:

- **Draft**: Create and edit broadcasts without sending
- **Publish**: Publishing a broadcast automatically sends it via your configured email provider
- **Schedule**: Use Payload's scheduled publishing to send broadcasts at a future time

**How it works:**
1. Create a broadcast and save as draft
2. When ready, click "Publish" to send immediately
3. Or use "Schedule" to publish (and send) at a specific date/time

**Important**: Scheduled publishing requires configuring Payload's Jobs Queue. For Vercel deployments, add this to your `vercel.json`:

```json
{
  "crons": [
    {
      "path": "/api/payload-jobs/run",
      "schedule": "*/5 * * * *"
    }
  ]
}
```

And secure the endpoint in your `payload.config.ts`:

```typescript
export default buildConfig({
  // ... other config
  jobs: {
    access: {
      run: ({ req }) => {
        if (req.user) return true
        const authHeader = req.headers.get('authorization')
        return authHeader === `Bearer ${process.env.CRON_SECRET}`
      },
    },
  },
})
```

### Custom Email Templates (v0.12.0+)

Customize your email design with React Email templates:

```typescript
// email-templates/broadcast-template.tsx
import { Html, Body, Container, Text, Link } from '@react-email/components'

export default function BroadcastTemplate({ subject, preheader, content }) {
  return (
    <Html>
      <Body style={{ backgroundColor: '#ffffff', fontFamily: 'Arial, sans-serif' }}>
        <Container style={{ maxWidth: '600px', margin: '0 auto' }}>
          <Text style={{ fontSize: '16px', lineHeight: '1.6' }}>
            <div dangerouslySetInnerHTML={{ __html: content }} />
          </Text>
          <hr style={{ margin: '40px 0', border: '1px solid #e5e7eb' }} />
          <Text style={{ fontSize: '14px', color: '#6b7280', textAlign: 'center' }}>
            <Link href="{{unsubscribe_url}}" style={{ color: '#6b7280' }}>
              Unsubscribe
            </Link>
          </Text>
        </Container>
      </Body>
    </Html>
  )
}
```

The plugin automatically detects templates at `email-templates/broadcast-template.tsx`.

### Utilities

Convert Lexical content to email-safe HTML:

```typescript
import { convertToEmailSafeHtml } from 'payload-plugin-newsletter/utils'

const html = await convertToEmailSafeHtml(editorState)
```

Validate any HTML for email compatibility:

```typescript
import { validateEmailHtml } from 'payload-plugin-newsletter/utils'

const result = validateEmailHtml(html)
if (!result.valid) {
  console.error('Email issues:', result.errors)
}
```

## Configuration Options

### Minimal Configuration

```typescript
newsletterPlugin({
  providers: {
    default: 'resend',
    resend: {
      apiKey: process.env.RESEND_API_KEY,
      fromAddress: 'newsletter@yoursite.com',
      fromName: 'Your Newsletter',
    },
  },
})
```

### Full Configuration

```typescript
newsletterPlugin({
  // Subscriber collection slug (default: 'subscribers')
  subscribersSlug: 'newsletter-subscribers',
  
  // Email providers
  providers: {
    default: 'resend',
    resend: {
      apiKey: process.env.RESEND_API_KEY,
      fromAddress: 'newsletter@yoursite.com',
      fromName: 'Your Newsletter',
      audienceIds: {
        en: {
          production: 'aud_prod_123',
          development: 'aud_dev_123',
        },
        es: {
          production: 'aud_prod_456',
          development: 'aud_dev_456',
        },
      },
    },
  },
  
  // Magic link authentication
  auth: {
    enabled: true,
    tokenExpiration: '7d', // How long magic links are valid
    magicLinkPath: '/newsletter/verify', // Where to redirect for verification
  },
  
  // Features
  features: {
    // Lead magnets (e.g., downloadable PDFs)
    leadMagnets: {
      enabled: true,
      collection: 'media', // Which collection stores your lead magnets
    },
    
    // Post-signup surveys
    surveys: {
      enabled: true,
      questions: [
        {
          id: 'interests',
          question: 'What topics interest you?',
          type: 'multiselect',
          options: ['Tech', 'Business', 'Design'],
        },
      ],
    },
    
    // Newsletter scheduling for articles
    newsletterScheduling: {
      enabled: true,
      articlesCollection: 'posts', // Your articles/posts collection
    },
    
    // Broadcast management (v0.10.0+)
    newsletterManagement: {
      enabled: true, // Enables broadcasts collection
    },
    
    // UTM tracking
    utmTracking: {
      enabled: true,
      fields: ['source', 'medium', 'campaign', 'content', 'term'],
    },
  },
  
  // Internationalization
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr'],
  },
  
  // Custom hooks
  hooks: {
    afterSubscribe: async ({ doc, req }) => {
      // Send to analytics, CRM, etc.
      console.log('New subscriber:', doc.email)
    },
  },
})
```

## API Endpoints

The plugin adds these endpoints to your application:

### POST `/api/newsletter/subscribe`
Subscribe a new email address

```typescript
// Request
{
  "email": "user@example.com",
  "name": "John Doe", // optional
  "preferences": { // optional
    "newsletter": true,
    "announcements": false
  }
}

// Response
{
  "success": true,
  "subscriber": { /* subscriber object */ }
}
```

### POST `/api/newsletter/verify-magic-link`
Verify a magic link token

```typescript
// Request
{
  "token": "eyJhbGc..."
}

// Response
{
  "success": true,
  "subscriber": { /* subscriber object */ },
  "sessionToken": "eyJhbGc..."
}
```

### GET/POST `/api/newsletter/preferences`
Get or update subscriber preferences (requires magic link auth)

### POST `/api/newsletter/unsubscribe`
Unsubscribe an email address

### POST `/api/newsletter/signin`
Request a magic link for existing subscribers

```typescript
// Request
{
  "email": "user@example.com"
}

// Response
{
  "success": true,
  "message": "Check your email for the sign-in link"
}
```

### GET `/api/newsletter/me`
Get current authenticated subscriber (requires authentication)

```typescript
// Response
{
  "success": true,
  "subscriber": {
    "id": "123",
    "email": "user@example.com",
    "name": "John Doe",
    "status": "active",
    "preferences": { /* preferences */ }
  }
}
```

### POST `/api/newsletter/signout`
Sign out the current subscriber

```typescript
// Response
{
  "success": true,
  "message": "Signed out successfully"
}
```

## Authentication

The plugin provides complete magic link authentication for subscribers:

### Client-Side Authentication

Use the `useNewsletterAuth` hook in your React components:

```tsx
import { useNewsletterAuth } from 'payload-plugin-newsletter/client'

function MyComponent() {
  const { 
    subscriber, 
    isAuthenticated, 
    isLoading, 
    signOut, 
    refreshAuth 
  } = useNewsletterAuth()
  
  if (isLoading) return <div>Loading...</div>
  
  if (!isAuthenticated) {
    return <div>Please sign in to manage your preferences</div>
  }
  
  return (
    <div>
      <p>Welcome {subscriber.email}!</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  )
}
```

### Server-Side Authentication

For Next.js applications, use the session utilities:

```typescript
import { requireAuth, getServerSideAuth } from 'payload-plugin-newsletter'

// Protect a page - redirects to /auth/signin if not authenticated
export const getServerSideProps = requireAuth()

// Or with custom logic
export const getServerSideProps = requireAuth(async (context) => {
  // Your custom logic here
  const data = await fetchData()
  return { props: { data } }
})

// Manual authentication check
export const getServerSideProps = async (context) => {
  const { subscriber, isAuthenticated } = await getServerSideAuth(context)
  
  if (!isAuthenticated) {
    // Handle unauthenticated state
  }
  
  return {
    props: { subscriber }
  }
}
```

### Authentication Flow

1. **Subscribe**: New users receive a magic link email to verify their email
2. **Sign In**: Existing subscribers can request a new magic link via `/api/newsletter/signin`
3. **Verify**: Clicking the magic link verifies the email and creates a session
4. **Session**: Sessions are stored in httpOnly cookies (30-day expiry by default)
5. **Sign Out**: Clears the session cookie

### Configuration

```typescript
newsletterPlugin({
  auth: {
    enabled: true, // Enable/disable authentication
    tokenExpiration: '7d', // Magic link validity
    magicLinkPath: '/newsletter/verify', // Verification redirect path
  },
  // Email templates can be customized
  emails: {
    magicLink: {
      subject: 'Sign in to {{siteName}}',
    },
    welcome: {
      enabled: true,
      subject: 'Welcome to {{siteName}}!',
    },
    signIn: {
      subject: 'Sign in to your account',
    },
  },
})
```

## Newsletter Scheduling

If you enable newsletter scheduling, the plugin adds scheduling fields to your articles collection:

```typescript
features: {
  newsletterScheduling: {
    enabled: true,
    articlesCollection: 'articles', // Your existing collection
  }
}
```

This adds a "Newsletter Scheduling" group to your articles with:
- Schedule toggle
- Send date/time picker
- Audience segment selection
- Send status tracking

## Webhook Configuration (Broadcast)

The plugin supports real-time webhook integration with Broadcast for instant updates:

### Automatic Updates

When configured, the plugin automatically receives and processes:
- **Subscriber Events**: `subscribed`, `unsubscribed`
- **Broadcast Events**: All status changes (`scheduled`, `in_progress`, `sent`, etc.)

### Setup Instructions

1. **Save your Newsletter Settings** in the Payload admin to generate a webhook URL
2. **Configure in Broadcast**:
   - Go to your Broadcast dashboard → "Webhook Endpoints"
   - Click "Add Webhook Endpoint"
   - Paste the webhook URL from Payload
   - Select events:
     - Subscriber Events: `subscribed`, `unsubscribed`
     - Broadcast Events: All
   - Create the webhook and copy the webhook secret
3. **Add the webhook secret** to your Newsletter Settings in Payload
4. **Save and verify** the webhook connection

### Security

Webhooks are secured with:
- HMAC-SHA256 signature verification
- Timestamp validation (5-minute window)
- Secret key stored in Newsletter Settings

### Data Flow

- **Subscriber events** update the subscriber's status and metadata
- **Broadcast events** update the broadcast's status and delivery metrics
- All updates happen in real-time without polling

**Note**: Email engagement events (opens, clicks) remain in Broadcast for analytics.

## Email Providers

### Resend

[Resend](https://resend.com) is a modern email API for developers.

```typescript
providers: {
  default: 'resend',
  resend: {
    apiKey: process.env.RESEND_API_KEY,
    fromAddress: 'hello@yoursite.com',
    fromName: 'Your Newsletter',
    audienceIds: {
      en: {
        production: 'your_audience_id',
      },
    },
  },
}
```

### Broadcast

[Broadcast](https://sendbroadcast.net/) is a self-hosted email automation platform.

```typescript
providers: {
  default: 'broadcast',
  broadcast: {
    apiUrl: process.env.BROADCAST_API_URL,
    token: process.env.BROADCAST_TOKEN,
    // Optional: These can be set here as defaults or configured in the admin UI
    fromAddress: 'hello@yoursite.com',
    fromName: 'Your Newsletter',
    replyTo: 'replies@yoursite.com',
  },
}
```

**Note**: Settings configured in the Payload admin UI take precedence over these config values. The config values serve as defaults when settings haven't been configured yet.

## TypeScript

The plugin is fully typed. Import types as needed:

```typescript
import type { 
  NewsletterPluginConfig,
  Subscriber,
  EmailProvider 
} from 'payload-plugin-newsletter/types'
```

## Customization

### Custom Fields

Add custom fields to the subscribers collection:

```typescript
newsletterPlugin({
  fields: {
    additional: [
      {
        name: 'company',
        type: 'text',
        label: 'Company Name',
      },
      {
        name: 'role',
        type: 'select',
        options: ['developer', 'designer', 'manager'],
      },
    ],
  },
})
```

### Custom Email Templates

Override the default email templates:

```typescript
import { WelcomeEmail } from './emails/Welcome'

newsletterPlugin({
  templates: {
    welcome: WelcomeEmail,
  },
})
```

### Extending the Broadcasts Collection (v0.15.0+)

You can extend the Broadcasts collection with additional fields and custom email-compatible blocks:

```typescript
import type { Block } from 'payload'

const customBlock: Block = {
  slug: 'product-spotlight',
  labels: { singular: 'Product Spotlight', plural: 'Product Spotlights' },
  fields: [
    { name: 'product', type: 'relationship', relationTo: 'products', required: true },
    { name: 'description', type: 'textarea' }
  ]
}

newsletterPlugin({
  // ... existing config
  customizations: {
    broadcasts: {
      additionalFields: [
        {
          name: 'slug',
          type: 'text',
          required: true,
          admin: { position: 'sidebar' }
        }
      ],
      customBlocks: [customBlock], // Processed server-side for email compatibility
      fieldOverrides: {
        content: (defaultField) => ({
          ...defaultField,
          admin: {
            ...defaultField.admin,
            description: 'Custom description'
          }
        })
      },
      // Email preview customization (v0.20.0+)
      emailPreview: {
        // Disable default email template wrapping
        wrapInTemplate: false,
        
        // Or provide a custom wrapper function
        customWrapper: async (content, { subject, preheader }) => {
          return `
            <div class="my-custom-template">
              <h1>${subject}</h1>
              ${preheader ? `<p class="preheader">${preheader}</p>` : ''}
              <div class="content">${content}</div>
            </div>
          `
        }
      }
    }
  }
})
```

**Note**: Custom blocks are processed server-side to ensure email compatibility and prevent Next.js serialization errors.

### Email Preview Customization (v0.20.0+)

The plugin now supports full customization of email preview rendering. This is useful when you have custom email templates and want the preview to match what's actually sent.

#### Disable Default Template Wrapping

If you're using your own email template system, you can disable the default template wrapping:

```typescript
newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        wrapInTemplate: false // Show raw HTML without email template
      }
    }
  }
})
```

#### Custom Email Wrapper

Provide your own wrapper function to match your email service's template:

```typescript
newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        customWrapper: async (content, { subject, preheader }) => {
          // Return your custom email template
          return `
            <!DOCTYPE html>
            <html>
              <head>
                <title>${subject}</title>
                <!-- Your custom styles -->
              </head>
              <body>
                <div class="preheader">${preheader}</div>
                ${content}
                <!-- Your footer -->
              </body>
            </html>
          `
        }
      }
    }
  }
})
```

#### Advanced: Using with React Email

If you're using React Email for templates, you can integrate it with the preview:

```typescript
import { render } from '@react-email/render'
import { MyEmailTemplate } from './emails/MyEmailTemplate'

newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        customWrapper: async (content, { subject, preheader }) => {
          return await render(
            <MyEmailTemplate 
              subject={subject}
              preheader={preheader}
              content={content}
            />
          )
        }
      }
    }
  }
})
```

This ensures your preview exactly matches what subscribers will see.

For complete extensibility documentation, see the [Extension Points Guide](./docs/architecture/extension-points.md).

## Troubleshooting

### Common Issues

**"Already subscribed" error**
- The email already exists in the subscribers collection
- Check the admin panel to manage existing subscribers

**Magic links not working**
- Ensure `JWT_SECRET` is set in your environment variables
- Check that the `magicLinkPath` matches your frontend route

**Emails not sending**
- Verify your API keys are correct
- Check the email provider's dashboard for errors
- Ensure from address is verified with your provider

## Security

### Access Control

The plugin implements proper access control for all operations:

- **Subscriber data**: Users can only access and modify their own data via magic link authentication
- **Newsletter settings**: Only admin users can modify email provider settings and configurations
- **API endpoints**: All endpoints respect Payload's access control rules

#### Custom Admin Check

The plugin supports multiple admin authentication patterns out of the box:
- `user.roles.includes('admin')` - Role-based
- `user.isAdmin === true` - Boolean field
- `user.role === 'admin'` - Single role field
- `user.admin === true` - Admin boolean

If your setup uses a different pattern, configure a custom admin check:

```typescript
newsletterPlugin({
  access: {
    isAdmin: (user) => {
      // Your custom logic
      return user.customAdminField === true
    }
  },
  // ... other config
})
```

### Best Practices

- Always use environment variables for sensitive data (API keys, JWT secrets)
- Enable double opt-in for GDPR compliance
- Configure allowed domains to prevent spam subscriptions
- Set reasonable rate limits for subscriptions per IP

## Migration Guide

Coming from another newsletter system? The plugin stores subscribers in a standard Payload collection, making it easy to import existing data:

```typescript
// Example migration script
const existingSubscribers = await getFromOldSystem()

for (const subscriber of existingSubscribers) {
  await payload.create({
    collection: 'subscribers',
    data: {
      email: subscriber.email,
      name: subscriber.name,
      subscriptionStatus: 'active',
      // Map other fields as needed
    },
  })
}
```

## Contributing

We welcome contributions! Please see our [feedback and contribution guide](./FEEDBACK.md).

### Release Process

This project uses a developer-controlled release process:
- **Version bumps happen locally** - You control when and what type
- **CI/CD publishes automatically** - When it detects a version change
- **No bot commits** - Your local repo stays in sync

See [Release Documentation](./docs/RELEASE.md) for details.

## License

MIT