payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
744 lines (589 loc) • 18 kB
Markdown
# Payload Newsletter Plugin
[](https://www.npmjs.com/package/payload-plugin-newsletter)
[](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
- 🔄 **Bidirectional Sync** - Sync unsubscribes from email services back to Payload
- 👁️ **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+)
## 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
### 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
},
// 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
## Unsubscribe Sync
The plugin supports bidirectional synchronization of unsubscribe states between Payload and your email service:
```typescript
features: {
unsubscribeSync: {
enabled: true,
schedule: '0 * * * *', // Hourly sync
queue: 'newsletter-sync' // Optional custom queue name
}
}
```
This feature:
- Polls your email service for unsubscribed users
- Updates their status in Payload automatically
- Supports both Broadcast and Resend providers
- Can run on a schedule or be triggered manually
For more details, see the [Unsubscribe Sync documentation](./docs/unsubscribe-sync.md).
## 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: 'https://broadcast.yoursite.com',
tokens: {
production: process.env.BROADCAST_TOKEN,
development: process.env.BROADCAST_DEV_TOKEN,
},
fromAddress: 'hello@yoursite.com',
fromName: 'Your Newsletter',
},
}
```
## 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,
},
})
```
## 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