payload-plugin-newsletter 0.19.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 CHANGED
@@ -1,3 +1,43 @@
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
+
1
41
  ## [0.19.0] - 2025-07-30
2
42
 
3
43
  ### 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
@@ -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;
@@ -1870,11 +1876,14 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1870
1876
  const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
1871
1877
  req.payload.logger?.info("Populating media fields for email preview...");
1872
1878
  const populatedContent = await populateMediaFields(content, req.payload, config);
1879
+ const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
1873
1880
  const htmlContent = await convertToEmailSafeHtml(populatedContent, {
1874
- wrapInTemplate: true,
1881
+ wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
1875
1882
  preheader,
1883
+ subject,
1876
1884
  mediaUrl,
1877
- customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
1885
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
1886
+ customWrapper: emailPreviewConfig?.customWrapper
1878
1887
  });
1879
1888
  return Response.json({
1880
1889
  success: true,