payload-plugin-newsletter 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/PREVIEW_CUSTOMIZATION_TASK.md +201 -0
- package/README.md +97 -0
- package/dist/collections.cjs +177 -20
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +177 -20
- package/dist/collections.js.map +1 -1
- package/dist/components.cjs +314 -143
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +2 -0
- package/dist/components.d.ts +2 -0
- package/dist/components.js +251 -80
- package/dist/components.js.map +1 -1
- package/dist/index.cjs +201 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +198 -20
- package/dist/index.js.map +1 -1
- package/dist/types.d.cts +23 -0
- package/dist/types.d.ts +23 -0
- package/dist/utils.cjs +172 -18
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +5 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +172 -18
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
+
## [0.20.0] - 2025-07-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **Email Preview Customization** - Full control over email preview rendering
|
|
5
|
+
- New `emailPreview` customization options in `BroadcastCustomizations` interface
|
|
6
|
+
- `wrapInTemplate` option to disable default email template wrapping
|
|
7
|
+
- `customWrapper` function for custom email template wrappers
|
|
8
|
+
- Support for both sync and async custom wrapper functions
|
|
9
|
+
- Pass subject and preheader to custom wrapper for complete control
|
|
10
|
+
- `customPreviewComponent` option (planned) for replacing the entire preview component
|
|
11
|
+
|
|
12
|
+
### Enhanced
|
|
13
|
+
- **Plugin Configuration Context**
|
|
14
|
+
- Added `PluginConfigContext` for passing configuration throughout component tree
|
|
15
|
+
- `usePluginConfig` hook for required config access
|
|
16
|
+
- `usePluginConfigOptional` hook for safe optional config access
|
|
17
|
+
- Context provider integration in component hierarchy
|
|
18
|
+
|
|
19
|
+
- **Email Preview Component**
|
|
20
|
+
- Updated to respect email preview customization options
|
|
21
|
+
- Backward compatible with default template wrapping behavior
|
|
22
|
+
- Support for both prop-based and context-based config passing
|
|
23
|
+
- Seamless integration with custom wrapper functions
|
|
24
|
+
|
|
25
|
+
- **Preview Endpoint**
|
|
26
|
+
- Updated to use email preview customization options
|
|
27
|
+
- Consistent behavior between UI preview and API preview
|
|
28
|
+
- Maintains all existing functionality while adding customization
|
|
29
|
+
|
|
30
|
+
### Technical Improvements
|
|
31
|
+
- **Type Safety** - Full TypeScript support for all customization options
|
|
32
|
+
- **Backward Compatibility** - All changes maintain 100% backward compatibility
|
|
33
|
+
- **Flexible Architecture** - Easy to extend with additional customization options
|
|
34
|
+
- **Performance** - No performance impact when customization is not used
|
|
35
|
+
|
|
36
|
+
### Developer Experience
|
|
37
|
+
- **Easy Configuration** - Simple to disable template wrapping or provide custom wrapper
|
|
38
|
+
- **Context Support** - Components can access config without prop drilling
|
|
39
|
+
- **Comprehensive Types** - Well-documented interfaces with examples
|
|
40
|
+
|
|
41
|
+
## [0.19.0] - 2025-07-30
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- **Responsive Email Design** - Complete overhaul of email template for mobile-first responsive design
|
|
45
|
+
- Mobile-optimized email template with responsive CSS media queries
|
|
46
|
+
- Responsive typography with proper mobile font sizes and line heights
|
|
47
|
+
- Mobile-friendly image scaling and spacing
|
|
48
|
+
- Dark mode support with `prefers-color-scheme` media queries
|
|
49
|
+
- Comprehensive email client compatibility (Outlook, Gmail, Apple Mail)
|
|
50
|
+
- Mobile-specific CSS classes for precise responsive control
|
|
51
|
+
|
|
52
|
+
### Enhanced
|
|
53
|
+
- **Email Template Architecture**
|
|
54
|
+
- Completely redesigned `wrapInEmailTemplate` function with responsive structure
|
|
55
|
+
- Added proper viewport meta tags and email client compatibility headers
|
|
56
|
+
- Enhanced preheader text formatting with proper hiding techniques
|
|
57
|
+
- Improved table-based layout structure for email client consistency
|
|
58
|
+
- Better font rendering with `-webkit-font-smoothing` and `-moz-osx-font-smoothing`
|
|
59
|
+
|
|
60
|
+
- **Typography System**
|
|
61
|
+
- Responsive heading sizes that scale appropriately on mobile devices
|
|
62
|
+
- H1: 32px desktop → 24px mobile with optimized line-height
|
|
63
|
+
- H2: 24px desktop → 20px mobile with optimized line-height
|
|
64
|
+
- H3: 20px desktop → 16px mobile with optimized line-height
|
|
65
|
+
- Enhanced paragraph styling with consistent font-size and line-height
|
|
66
|
+
- Improved list styling with proper spacing and typography
|
|
67
|
+
|
|
68
|
+
- **Image Handling**
|
|
69
|
+
- Responsive images with `mobile-width-100` class for full-width scaling
|
|
70
|
+
- Enhanced image captions with mobile-optimized typography
|
|
71
|
+
- Proper image border-radius for modern email design
|
|
72
|
+
- Better image centering and spacing on all screen sizes
|
|
73
|
+
|
|
74
|
+
### Technical Improvements
|
|
75
|
+
- **CSS Media Queries** - Comprehensive mobile-first responsive design
|
|
76
|
+
- `@media only screen and (max-width: 640px)` breakpoint
|
|
77
|
+
- Mobile utility classes: `.mobile-hide`, `.mobile-center`, `.mobile-width-100`
|
|
78
|
+
- Mobile padding classes: `.mobile-padding`, `.mobile-padding-sm`
|
|
79
|
+
- Mobile typography classes: `.mobile-font-size-14/16/20/24`
|
|
80
|
+
- Mobile spacing classes: `.mobile-margin-bottom-16/20`
|
|
81
|
+
|
|
82
|
+
- **Email Client Compatibility**
|
|
83
|
+
- Outlook-specific MSO conditional comments and fixes
|
|
84
|
+
- Apple Mail message reformatting prevention
|
|
85
|
+
- Gmail and other client table-based layout optimization
|
|
86
|
+
- Cross-client font fallback stack
|
|
87
|
+
|
|
88
|
+
- **Dark Mode Support**
|
|
89
|
+
- CSS custom properties for dark mode backgrounds and text
|
|
90
|
+
- Proper border color adjustments for dark themes
|
|
91
|
+
- Future-ready design system for theme customization
|
|
92
|
+
|
|
93
|
+
### Breaking Changes
|
|
94
|
+
- None - all changes maintain backward compatibility with existing email content
|
|
95
|
+
|
|
96
|
+
### Browser/Client Support
|
|
97
|
+
- ✅ Outlook 2016+ (Windows/Mac)
|
|
98
|
+
- ✅ Gmail (Web/Mobile/App)
|
|
99
|
+
- ✅ Apple Mail (macOS/iOS)
|
|
100
|
+
- ✅ Yahoo Mail
|
|
101
|
+
- ✅ Thunderbird
|
|
102
|
+
- ✅ Mobile email clients (iOS/Android)
|
|
103
|
+
|
|
1
104
|
## [0.18.0] - 2025-07-30
|
|
2
105
|
|
|
3
106
|
### Added
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Email Preview Customization Task
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The email preview in the plugin currently wraps all content in a default email template, which conflicts with custom email templates that users might implement. We need to make the preview customizable so users can control how their emails are rendered in the preview.
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
- The plugin's `EmailPreview.tsx` always wraps content with `wrapInTemplate: true` (line 54)
|
|
8
|
+
- This causes styling differences between the preview and what's actually sent to email providers
|
|
9
|
+
- Users who implement custom email templates see inconsistent previews
|
|
10
|
+
|
|
11
|
+
## Required Changes
|
|
12
|
+
|
|
13
|
+
### 1. Update Type Definitions (`src/types/index.ts`)
|
|
14
|
+
|
|
15
|
+
Add email preview customization options to the `BroadcastCustomizations` interface:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export interface BroadcastCustomizations {
|
|
19
|
+
// ... existing fields ...
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Email preview customization options
|
|
23
|
+
*/
|
|
24
|
+
emailPreview?: {
|
|
25
|
+
/**
|
|
26
|
+
* Whether to wrap preview content in default email template
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
wrapInTemplate?: boolean
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Custom wrapper function for preview content
|
|
33
|
+
* Receives the converted HTML and should return wrapped HTML
|
|
34
|
+
*/
|
|
35
|
+
customWrapper?: (content: string, options?: {
|
|
36
|
+
subject?: string
|
|
37
|
+
preheader?: string
|
|
38
|
+
}) => string | Promise<string>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Custom preview component to replace the default one entirely
|
|
42
|
+
* If provided, this component will be used instead of the default EmailPreview
|
|
43
|
+
*/
|
|
44
|
+
customPreviewComponent?: string // Path to custom component for import map
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Update Email Preview Component (`src/components/Broadcasts/EmailPreview.tsx`)
|
|
50
|
+
|
|
51
|
+
Modify the component to use the customization options:
|
|
52
|
+
|
|
53
|
+
1. Import the plugin config (you'll need to pass it through props or context)
|
|
54
|
+
2. Update the `convertContent` function (around line 42) to respect customization options:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Around line 51-54, replace:
|
|
58
|
+
const emailHtml = await convertToEmailSafeHtml(content, {
|
|
59
|
+
wrapInTemplate: true,
|
|
60
|
+
preheader,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// With:
|
|
64
|
+
const emailHtml = await convertToEmailSafeHtml(content, {
|
|
65
|
+
wrapInTemplate: pluginConfig?.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
|
|
66
|
+
preheader,
|
|
67
|
+
customWrapper: pluginConfig?.customizations?.broadcasts?.emailPreview?.customWrapper,
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Update Email Safe HTML Utility (`src/utils/emailSafeHtml.ts`)
|
|
72
|
+
|
|
73
|
+
Modify the `convertToEmailSafeHtml` function to support custom wrapper:
|
|
74
|
+
|
|
75
|
+
1. Add `customWrapper` to the options interface (around line 36):
|
|
76
|
+
```typescript
|
|
77
|
+
options?: {
|
|
78
|
+
wrapInTemplate?: boolean
|
|
79
|
+
preheader?: string
|
|
80
|
+
mediaUrl?: string
|
|
81
|
+
customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>
|
|
82
|
+
payload?: any
|
|
83
|
+
populateFields?: string[] | ((blockType: string) => string[])
|
|
84
|
+
customWrapper?: (content: string, options?: { preheader?: string }) => string | Promise<string>
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. Update the wrapping logic (around line 54-57):
|
|
89
|
+
```typescript
|
|
90
|
+
// Optionally wrap in email template
|
|
91
|
+
if (options?.wrapInTemplate) {
|
|
92
|
+
if (options.customWrapper) {
|
|
93
|
+
return await Promise.resolve(options.customWrapper(sanitizedHtml, { preheader: options.preheader }))
|
|
94
|
+
}
|
|
95
|
+
return wrapInEmailTemplate(sanitizedHtml, options.preheader)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. Update Email Preview Field (`src/components/Broadcasts/EmailPreviewField.tsx`)
|
|
100
|
+
|
|
101
|
+
Pass the plugin config to the EmailPreview component. You'll need to:
|
|
102
|
+
|
|
103
|
+
1. Import and use a context or prop to get the plugin config
|
|
104
|
+
2. Pass it to the EmailPreview component (around line 158):
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
<EmailPreview
|
|
108
|
+
content={fields.content?.value as SerializedEditorState || null}
|
|
109
|
+
subject={fields.subject?.value as string || 'Email Subject'}
|
|
110
|
+
preheader={fields.preheader?.value as string}
|
|
111
|
+
mode={previewMode}
|
|
112
|
+
onValidation={handleValidation}
|
|
113
|
+
pluginConfig={pluginConfig} // Add this
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 5. Create Plugin Config Context (`src/contexts/PluginConfigContext.tsx`) - NEW FILE
|
|
118
|
+
|
|
119
|
+
Create a context to pass plugin config throughout the component tree:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import React, { createContext, useContext } from 'react'
|
|
123
|
+
import type { NewsletterPluginConfig } from '../types'
|
|
124
|
+
|
|
125
|
+
const PluginConfigContext = createContext<NewsletterPluginConfig | null>(null)
|
|
126
|
+
|
|
127
|
+
export const PluginConfigProvider: React.FC<{
|
|
128
|
+
config: NewsletterPluginConfig
|
|
129
|
+
children: React.ReactNode
|
|
130
|
+
}> = ({ config, children }) => {
|
|
131
|
+
return (
|
|
132
|
+
<PluginConfigContext.Provider value={config}>
|
|
133
|
+
{children}
|
|
134
|
+
</PluginConfigContext.Provider>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const usePluginConfig = () => {
|
|
139
|
+
const config = useContext(PluginConfigContext)
|
|
140
|
+
if (!config) {
|
|
141
|
+
throw new Error('usePluginConfig must be used within PluginConfigProvider')
|
|
142
|
+
}
|
|
143
|
+
return config
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 6. Update Broadcasts Collection (`src/collections/Broadcasts.ts`)
|
|
148
|
+
|
|
149
|
+
Wrap the admin UI components with the PluginConfigProvider. This is more complex and might require modifying how components are initialized.
|
|
150
|
+
|
|
151
|
+
## Testing Instructions
|
|
152
|
+
|
|
153
|
+
1. Create a test configuration that uses custom email preview:
|
|
154
|
+
```typescript
|
|
155
|
+
newsletterPlugin({
|
|
156
|
+
customizations: {
|
|
157
|
+
broadcasts: {
|
|
158
|
+
emailPreview: {
|
|
159
|
+
wrapInTemplate: false,
|
|
160
|
+
// Or with custom wrapper:
|
|
161
|
+
customWrapper: async (content, { preheader }) => {
|
|
162
|
+
return `<div class="my-custom-wrapper">${content}</div>`
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
2. Verify that:
|
|
171
|
+
- Default behavior (with template) still works when no customization is provided
|
|
172
|
+
- Setting `wrapInTemplate: false` shows raw content without wrapper
|
|
173
|
+
- Custom wrapper function is called and applied correctly
|
|
174
|
+
- Preview matches what's sent to email providers
|
|
175
|
+
|
|
176
|
+
## Backward Compatibility
|
|
177
|
+
|
|
178
|
+
- Default behavior must remain unchanged (wrap in template)
|
|
179
|
+
- All customization options should be optional
|
|
180
|
+
- Existing installations should work without any configuration changes
|
|
181
|
+
|
|
182
|
+
## Additional Considerations
|
|
183
|
+
|
|
184
|
+
1. **Custom Preview Component**: The `customPreviewComponent` option would allow complete replacement of the preview component, but this is a more complex feature that could be added later.
|
|
185
|
+
|
|
186
|
+
2. **Preview Context**: Consider passing additional context to the custom wrapper like:
|
|
187
|
+
- Current user
|
|
188
|
+
- Broadcast metadata
|
|
189
|
+
- Provider configuration
|
|
190
|
+
|
|
191
|
+
3. **Documentation**: Update the plugin README with examples of how to use these new customization options.
|
|
192
|
+
|
|
193
|
+
## Implementation Order
|
|
194
|
+
|
|
195
|
+
1. Start with type definitions
|
|
196
|
+
2. Update emailSafeHtml utility
|
|
197
|
+
3. Modify EmailPreview component
|
|
198
|
+
4. Test with various configurations
|
|
199
|
+
5. Add documentation
|
|
200
|
+
|
|
201
|
+
This approach provides maximum flexibility while maintaining backward compatibility and clean architecture.
|
package/README.md
CHANGED
|
@@ -806,6 +806,22 @@ newsletterPlugin({
|
|
|
806
806
|
description: 'Custom description'
|
|
807
807
|
}
|
|
808
808
|
})
|
|
809
|
+
},
|
|
810
|
+
// Email preview customization (v0.20.0+)
|
|
811
|
+
emailPreview: {
|
|
812
|
+
// Disable default email template wrapping
|
|
813
|
+
wrapInTemplate: false,
|
|
814
|
+
|
|
815
|
+
// Or provide a custom wrapper function
|
|
816
|
+
customWrapper: async (content, { subject, preheader }) => {
|
|
817
|
+
return `
|
|
818
|
+
<div class="my-custom-template">
|
|
819
|
+
<h1>${subject}</h1>
|
|
820
|
+
${preheader ? `<p class="preheader">${preheader}</p>` : ''}
|
|
821
|
+
<div class="content">${content}</div>
|
|
822
|
+
</div>
|
|
823
|
+
`
|
|
824
|
+
}
|
|
809
825
|
}
|
|
810
826
|
}
|
|
811
827
|
}
|
|
@@ -814,6 +830,87 @@ newsletterPlugin({
|
|
|
814
830
|
|
|
815
831
|
**Note**: Custom blocks are processed server-side to ensure email compatibility and prevent Next.js serialization errors.
|
|
816
832
|
|
|
833
|
+
### Email Preview Customization (v0.20.0+)
|
|
834
|
+
|
|
835
|
+
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.
|
|
836
|
+
|
|
837
|
+
#### Disable Default Template Wrapping
|
|
838
|
+
|
|
839
|
+
If you're using your own email template system, you can disable the default template wrapping:
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
newsletterPlugin({
|
|
843
|
+
customizations: {
|
|
844
|
+
broadcasts: {
|
|
845
|
+
emailPreview: {
|
|
846
|
+
wrapInTemplate: false // Show raw HTML without email template
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
})
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
#### Custom Email Wrapper
|
|
854
|
+
|
|
855
|
+
Provide your own wrapper function to match your email service's template:
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
newsletterPlugin({
|
|
859
|
+
customizations: {
|
|
860
|
+
broadcasts: {
|
|
861
|
+
emailPreview: {
|
|
862
|
+
customWrapper: async (content, { subject, preheader }) => {
|
|
863
|
+
// Return your custom email template
|
|
864
|
+
return `
|
|
865
|
+
<!DOCTYPE html>
|
|
866
|
+
<html>
|
|
867
|
+
<head>
|
|
868
|
+
<title>${subject}</title>
|
|
869
|
+
<!-- Your custom styles -->
|
|
870
|
+
</head>
|
|
871
|
+
<body>
|
|
872
|
+
<div class="preheader">${preheader}</div>
|
|
873
|
+
${content}
|
|
874
|
+
<!-- Your footer -->
|
|
875
|
+
</body>
|
|
876
|
+
</html>
|
|
877
|
+
`
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
})
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
#### Advanced: Using with React Email
|
|
886
|
+
|
|
887
|
+
If you're using React Email for templates, you can integrate it with the preview:
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
import { render } from '@react-email/render'
|
|
891
|
+
import { MyEmailTemplate } from './emails/MyEmailTemplate'
|
|
892
|
+
|
|
893
|
+
newsletterPlugin({
|
|
894
|
+
customizations: {
|
|
895
|
+
broadcasts: {
|
|
896
|
+
emailPreview: {
|
|
897
|
+
customWrapper: async (content, { subject, preheader }) => {
|
|
898
|
+
return await render(
|
|
899
|
+
<MyEmailTemplate
|
|
900
|
+
subject={subject}
|
|
901
|
+
preheader={preheader}
|
|
902
|
+
content={content}
|
|
903
|
+
/>
|
|
904
|
+
)
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
})
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
This ensures your preview exactly matches what subscribers will see.
|
|
913
|
+
|
|
817
914
|
For complete extensibility documentation, see the [Extension Points Guide](./docs/architecture/extension-points.md).
|
|
818
915
|
|
|
819
916
|
## Troubleshooting
|
package/dist/collections.cjs
CHANGED
|
@@ -976,6 +976,12 @@ async function convertToEmailSafeHtml(editorState, options) {
|
|
|
976
976
|
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
|
|
977
977
|
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
978
978
|
if (options?.wrapInTemplate) {
|
|
979
|
+
if (options.customWrapper) {
|
|
980
|
+
return await Promise.resolve(options.customWrapper(sanitizedHtml, {
|
|
981
|
+
preheader: options.preheader,
|
|
982
|
+
subject: options.subject
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
979
985
|
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
980
986
|
}
|
|
981
987
|
return sanitizedHtml;
|
|
@@ -1029,9 +1035,9 @@ async function convertParagraph(node, mediaUrl, customBlockConverter) {
|
|
|
1029
1035
|
);
|
|
1030
1036
|
const children = childParts.join("");
|
|
1031
1037
|
if (!children.trim()) {
|
|
1032
|
-
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
1038
|
+
return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
1033
1039
|
}
|
|
1034
|
-
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
1040
|
+
return `<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; text-align: ${align}; font-size: 16px; line-height: 1.5;">${children}</p>`;
|
|
1035
1041
|
}
|
|
1036
1042
|
async function convertHeading(node, mediaUrl, customBlockConverter) {
|
|
1037
1043
|
const tag = node.tag || "h1";
|
|
@@ -1045,8 +1051,14 @@ async function convertHeading(node, mediaUrl, customBlockConverter) {
|
|
|
1045
1051
|
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
1046
1052
|
h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
|
|
1047
1053
|
};
|
|
1054
|
+
const mobileClasses = {
|
|
1055
|
+
h1: "mobile-font-size-24",
|
|
1056
|
+
h2: "mobile-font-size-20",
|
|
1057
|
+
h3: "mobile-font-size-16"
|
|
1058
|
+
};
|
|
1048
1059
|
const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
|
|
1049
|
-
|
|
1060
|
+
const mobileClass = mobileClasses[tag] || mobileClasses.h3;
|
|
1061
|
+
return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
|
|
1050
1062
|
}
|
|
1051
1063
|
async function convertList(node, mediaUrl, customBlockConverter) {
|
|
1052
1064
|
const tag = node.listType === "number" ? "ol" : "ul";
|
|
@@ -1054,8 +1066,8 @@ async function convertList(node, mediaUrl, customBlockConverter) {
|
|
|
1054
1066
|
(node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
|
|
1055
1067
|
);
|
|
1056
1068
|
const children = childParts.join("");
|
|
1057
|
-
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
|
|
1058
|
-
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1069
|
+
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc; font-size: 16px; line-height: 1.5;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal; font-size: 16px; line-height: 1.5;";
|
|
1070
|
+
return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
|
|
1059
1071
|
}
|
|
1060
1072
|
async function convertListItem(node, mediaUrl, customBlockConverter) {
|
|
1061
1073
|
const childParts = await Promise.all(
|
|
@@ -1112,16 +1124,16 @@ function convertUpload(node, mediaUrl) {
|
|
|
1112
1124
|
}
|
|
1113
1125
|
const alt = node.fields?.altText || upload.alt || "";
|
|
1114
1126
|
const caption = node.fields?.caption || "";
|
|
1115
|
-
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
1127
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="mobile-width-100" style="max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px;" />`;
|
|
1116
1128
|
if (caption) {
|
|
1117
1129
|
return `
|
|
1118
|
-
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1130
|
+
<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
|
|
1119
1131
|
${imgHtml}
|
|
1120
|
-
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
1132
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic; text-align: center;" class="mobile-font-size-14">${escapeHtml(caption)}</p>
|
|
1121
1133
|
</div>
|
|
1122
1134
|
`;
|
|
1123
1135
|
}
|
|
1124
|
-
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
1136
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
|
|
1125
1137
|
}
|
|
1126
1138
|
async function convertBlock(node, mediaUrl, customBlockConverter) {
|
|
1127
1139
|
const blockType = node.fields?.blockName || node.blockName;
|
|
@@ -1194,11 +1206,14 @@ function escapeHtml(text) {
|
|
|
1194
1206
|
}
|
|
1195
1207
|
function wrapInEmailTemplate(content, preheader) {
|
|
1196
1208
|
return `<!DOCTYPE html>
|
|
1197
|
-
<html lang="en">
|
|
1209
|
+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
1198
1210
|
<head>
|
|
1199
1211
|
<meta charset="UTF-8">
|
|
1200
1212
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1201
|
-
<
|
|
1213
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
1214
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
1215
|
+
<title>Newsletter</title>
|
|
1216
|
+
|
|
1202
1217
|
<!--[if mso]>
|
|
1203
1218
|
<noscript>
|
|
1204
1219
|
<xml>
|
|
@@ -1208,16 +1223,155 @@ function wrapInEmailTemplate(content, preheader) {
|
|
|
1208
1223
|
</xml>
|
|
1209
1224
|
</noscript>
|
|
1210
1225
|
<![endif]-->
|
|
1226
|
+
|
|
1227
|
+
<style>
|
|
1228
|
+
/* Reset and base styles */
|
|
1229
|
+
* {
|
|
1230
|
+
-webkit-text-size-adjust: 100%;
|
|
1231
|
+
-ms-text-size-adjust: 100%;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
body {
|
|
1235
|
+
margin: 0 !important;
|
|
1236
|
+
padding: 0 !important;
|
|
1237
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
|
1238
|
+
font-size: 16px;
|
|
1239
|
+
line-height: 1.5;
|
|
1240
|
+
color: #1A1A1A;
|
|
1241
|
+
background-color: #f8f9fa;
|
|
1242
|
+
-webkit-font-smoothing: antialiased;
|
|
1243
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
table {
|
|
1247
|
+
border-spacing: 0 !important;
|
|
1248
|
+
border-collapse: collapse !important;
|
|
1249
|
+
table-layout: fixed !important;
|
|
1250
|
+
margin: 0 auto !important;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
table table table {
|
|
1254
|
+
table-layout: auto;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
img {
|
|
1258
|
+
-ms-interpolation-mode: bicubic;
|
|
1259
|
+
max-width: 100%;
|
|
1260
|
+
height: auto;
|
|
1261
|
+
border: 0;
|
|
1262
|
+
outline: none;
|
|
1263
|
+
text-decoration: none;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/* Responsive styles */
|
|
1267
|
+
@media only screen and (max-width: 640px) {
|
|
1268
|
+
.mobile-hide {
|
|
1269
|
+
display: none !important;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
.mobile-center {
|
|
1273
|
+
text-align: center !important;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.mobile-width-100 {
|
|
1277
|
+
width: 100% !important;
|
|
1278
|
+
max-width: 100% !important;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.mobile-padding {
|
|
1282
|
+
padding: 20px !important;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.mobile-padding-sm {
|
|
1286
|
+
padding: 16px !important;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.mobile-font-size-14 {
|
|
1290
|
+
font-size: 14px !important;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.mobile-font-size-16 {
|
|
1294
|
+
font-size: 16px !important;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.mobile-font-size-20 {
|
|
1298
|
+
font-size: 20px !important;
|
|
1299
|
+
line-height: 1.3 !important;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.mobile-font-size-24 {
|
|
1303
|
+
font-size: 24px !important;
|
|
1304
|
+
line-height: 1.2 !important;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/* Stack sections on mobile */
|
|
1308
|
+
.mobile-stack {
|
|
1309
|
+
display: block !important;
|
|
1310
|
+
width: 100% !important;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/* Mobile-specific spacing */
|
|
1314
|
+
.mobile-margin-bottom-16 {
|
|
1315
|
+
margin-bottom: 16px !important;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.mobile-margin-bottom-20 {
|
|
1319
|
+
margin-bottom: 20px !important;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/* Dark mode support */
|
|
1324
|
+
@media (prefers-color-scheme: dark) {
|
|
1325
|
+
.dark-mode-bg {
|
|
1326
|
+
background-color: #1a1a1a !important;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.dark-mode-text {
|
|
1330
|
+
color: #ffffff !important;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.dark-mode-border {
|
|
1334
|
+
border-color: #333333 !important;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/* Outlook-specific fixes */
|
|
1339
|
+
<!--[if mso]>
|
|
1340
|
+
<style>
|
|
1341
|
+
table {
|
|
1342
|
+
border-collapse: collapse;
|
|
1343
|
+
border-spacing: 0;
|
|
1344
|
+
border: none;
|
|
1345
|
+
margin: 0;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
div, p {
|
|
1349
|
+
margin: 0;
|
|
1350
|
+
}
|
|
1351
|
+
</style>
|
|
1352
|
+
<![endif]-->
|
|
1353
|
+
</style>
|
|
1211
1354
|
</head>
|
|
1212
|
-
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #
|
|
1213
|
-
${preheader ?
|
|
1214
|
-
|
|
1355
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1A1A1A; background-color: #f8f9fa;">
|
|
1356
|
+
${preheader ? `
|
|
1357
|
+
<!-- Preheader text -->
|
|
1358
|
+
<div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
|
|
1359
|
+
${escapeHtml(preheader)}
|
|
1360
|
+
</div>
|
|
1361
|
+
` : ""}
|
|
1362
|
+
|
|
1363
|
+
<!-- Main container -->
|
|
1364
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
|
|
1215
1365
|
<tr>
|
|
1216
|
-
<td align="center" style="padding: 20px
|
|
1217
|
-
|
|
1366
|
+
<td align="center" style="padding: 20px 10px;">
|
|
1367
|
+
<!-- Email wrapper -->
|
|
1368
|
+
<table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
|
|
1218
1369
|
<tr>
|
|
1219
|
-
<td style="padding:
|
|
1220
|
-
|
|
1370
|
+
<td class="mobile-padding" style="padding: 0;">
|
|
1371
|
+
<!-- Content area with light background -->
|
|
1372
|
+
<div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
|
|
1373
|
+
${content}
|
|
1374
|
+
</div>
|
|
1221
1375
|
</td>
|
|
1222
1376
|
</tr>
|
|
1223
1377
|
</table>
|
|
@@ -1722,11 +1876,14 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
|
|
|
1722
1876
|
const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
|
|
1723
1877
|
req.payload.logger?.info("Populating media fields for email preview...");
|
|
1724
1878
|
const populatedContent = await populateMediaFields(content, req.payload, config);
|
|
1879
|
+
const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
|
|
1725
1880
|
const htmlContent = await convertToEmailSafeHtml(populatedContent, {
|
|
1726
|
-
wrapInTemplate: true,
|
|
1881
|
+
wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
|
|
1727
1882
|
preheader,
|
|
1883
|
+
subject,
|
|
1728
1884
|
mediaUrl,
|
|
1729
|
-
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
|
|
1885
|
+
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
|
|
1886
|
+
customWrapper: emailPreviewConfig?.customWrapper
|
|
1730
1887
|
});
|
|
1731
1888
|
return Response.json({
|
|
1732
1889
|
success: true,
|