payload-plugin-newsletter 0.16.10 → 0.17.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/dist/index.js CHANGED
@@ -3174,62 +3174,73 @@ async function convertToEmailSafeHtml(editorState, options) {
3174
3174
  if (!editorState) {
3175
3175
  return "";
3176
3176
  }
3177
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
3177
+ const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
3178
3178
  const sanitizedHtml = DOMPurify2.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
3179
3179
  if (options?.wrapInTemplate) {
3180
3180
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
3181
3181
  }
3182
3182
  return sanitizedHtml;
3183
3183
  }
3184
- async function lexicalToEmailHtml(editorState, mediaUrl) {
3184
+ async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
3185
3185
  const { root } = editorState;
3186
3186
  if (!root || !root.children) {
3187
3187
  return "";
3188
3188
  }
3189
- const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
3190
- return html;
3189
+ const htmlParts = await Promise.all(
3190
+ root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
3191
+ );
3192
+ return htmlParts.join("");
3191
3193
  }
3192
- function convertNode(node, mediaUrl) {
3194
+ async function convertNode(node, mediaUrl, customBlockConverter) {
3193
3195
  switch (node.type) {
3194
3196
  case "paragraph":
3195
- return convertParagraph(node, mediaUrl);
3197
+ return convertParagraph(node, mediaUrl, customBlockConverter);
3196
3198
  case "heading":
3197
- return convertHeading(node, mediaUrl);
3199
+ return convertHeading(node, mediaUrl, customBlockConverter);
3198
3200
  case "list":
3199
- return convertList(node, mediaUrl);
3201
+ return convertList(node, mediaUrl, customBlockConverter);
3200
3202
  case "listitem":
3201
- return convertListItem(node, mediaUrl);
3203
+ return convertListItem(node, mediaUrl, customBlockConverter);
3202
3204
  case "blockquote":
3203
- return convertBlockquote(node, mediaUrl);
3205
+ return convertBlockquote(node, mediaUrl, customBlockConverter);
3204
3206
  case "text":
3205
3207
  return convertText(node);
3206
3208
  case "link":
3207
- return convertLink(node, mediaUrl);
3209
+ return convertLink(node, mediaUrl, customBlockConverter);
3208
3210
  case "linebreak":
3209
3211
  return "<br>";
3210
3212
  case "upload":
3211
3213
  return convertUpload(node, mediaUrl);
3212
3214
  case "block":
3213
- return convertBlock(node, mediaUrl);
3215
+ return await convertBlock(node, mediaUrl, customBlockConverter);
3214
3216
  default:
3215
3217
  if (node.children) {
3216
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
3218
+ const childParts = await Promise.all(
3219
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
3220
+ );
3221
+ return childParts.join("");
3217
3222
  }
3218
3223
  return "";
3219
3224
  }
3220
3225
  }
3221
- function convertParagraph(node, mediaUrl) {
3226
+ async function convertParagraph(node, mediaUrl, customBlockConverter) {
3222
3227
  const align = getAlignment(node.format);
3223
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3228
+ const childParts = await Promise.all(
3229
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3230
+ );
3231
+ const children = childParts.join("");
3224
3232
  if (!children.trim()) {
3225
3233
  return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
3226
3234
  }
3227
3235
  return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
3228
3236
  }
3229
- function convertHeading(node, mediaUrl) {
3237
+ async function convertHeading(node, mediaUrl, customBlockConverter) {
3230
3238
  const tag = node.tag || "h1";
3231
3239
  const align = getAlignment(node.format);
3232
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3240
+ const childParts = await Promise.all(
3241
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3242
+ );
3243
+ const children = childParts.join("");
3233
3244
  const styles2 = {
3234
3245
  h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
3235
3246
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
@@ -3238,18 +3249,27 @@ function convertHeading(node, mediaUrl) {
3238
3249
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
3239
3250
  return `<${tag} style="${style}">${children}</${tag}>`;
3240
3251
  }
3241
- function convertList(node, mediaUrl) {
3252
+ async function convertList(node, mediaUrl, customBlockConverter) {
3242
3253
  const tag = node.listType === "number" ? "ol" : "ul";
3243
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3254
+ const childParts = await Promise.all(
3255
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3256
+ );
3257
+ const children = childParts.join("");
3244
3258
  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;";
3245
3259
  return `<${tag} style="${style}">${children}</${tag}>`;
3246
3260
  }
3247
- function convertListItem(node, mediaUrl) {
3248
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3261
+ async function convertListItem(node, mediaUrl, customBlockConverter) {
3262
+ const childParts = await Promise.all(
3263
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3264
+ );
3265
+ const children = childParts.join("");
3249
3266
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
3250
3267
  }
3251
- function convertBlockquote(node, mediaUrl) {
3252
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3268
+ async function convertBlockquote(node, mediaUrl, customBlockConverter) {
3269
+ const childParts = await Promise.all(
3270
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3271
+ );
3272
+ const children = childParts.join("");
3253
3273
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
3254
3274
  return `<blockquote style="${style}">${children}</blockquote>`;
3255
3275
  }
@@ -3269,8 +3289,11 @@ function convertText(node) {
3269
3289
  }
3270
3290
  return text;
3271
3291
  }
3272
- function convertLink(node, mediaUrl) {
3273
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3292
+ async function convertLink(node, mediaUrl, customBlockConverter) {
3293
+ const childParts = await Promise.all(
3294
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3295
+ );
3296
+ const children = childParts.join("");
3274
3297
  const url = node.fields?.url || "#";
3275
3298
  const newTab = node.fields?.newTab ?? false;
3276
3299
  const targetAttr = newTab ? ' target="_blank"' : "";
@@ -3301,8 +3324,18 @@ function convertUpload(node, mediaUrl) {
3301
3324
  }
3302
3325
  return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
3303
3326
  }
3304
- function convertBlock(node, mediaUrl) {
3305
- const blockType = node.fields?.blockName;
3327
+ async function convertBlock(node, mediaUrl, customBlockConverter) {
3328
+ const blockType = node.fields?.blockName || node.blockName;
3329
+ if (customBlockConverter) {
3330
+ try {
3331
+ const customHtml = await customBlockConverter(node, mediaUrl);
3332
+ if (customHtml) {
3333
+ return customHtml;
3334
+ }
3335
+ } catch (error) {
3336
+ console.error(`Custom block converter error for ${blockType}:`, error);
3337
+ }
3338
+ }
3306
3339
  switch (blockType) {
3307
3340
  case "button":
3308
3341
  return convertButtonBlock(node.fields);
@@ -3310,7 +3343,10 @@ function convertBlock(node, mediaUrl) {
3310
3343
  return convertDividerBlock(node.fields);
3311
3344
  default:
3312
3345
  if (node.children) {
3313
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
3346
+ const childParts = await Promise.all(
3347
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
3348
+ );
3349
+ return childParts.join("");
3314
3350
  }
3315
3351
  return "";
3316
3352
  }
@@ -3437,7 +3473,8 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
3437
3473
  }
3438
3474
  const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
3439
3475
  wrapInTemplate: true,
3440
- preheader: broadcast.preheader
3476
+ preheader: broadcast.preheader,
3477
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3441
3478
  });
3442
3479
  const emailService = req.payload.newsletterEmailService;
3443
3480
  if (!emailService) {
@@ -3475,6 +3512,47 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
3475
3512
  };
3476
3513
  };
3477
3514
 
3515
+ // src/endpoints/broadcasts/preview.ts
3516
+ var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
3517
+ return {
3518
+ path: `/api/${collectionSlug}/preview`,
3519
+ method: "post",
3520
+ handler: async (req) => {
3521
+ try {
3522
+ const data = await (req.json?.() || Promise.resolve({}));
3523
+ const { content, preheader, subject } = data;
3524
+ if (!content) {
3525
+ return Response.json({
3526
+ success: false,
3527
+ error: "Content is required for preview"
3528
+ }, { status: 400 });
3529
+ }
3530
+ const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
3531
+ const htmlContent = await convertToEmailSafeHtml(content, {
3532
+ wrapInTemplate: true,
3533
+ preheader,
3534
+ mediaUrl,
3535
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3536
+ });
3537
+ return Response.json({
3538
+ success: true,
3539
+ preview: {
3540
+ subject: subject || "Preview",
3541
+ preheader: preheader || "",
3542
+ html: htmlContent
3543
+ }
3544
+ });
3545
+ } catch (error) {
3546
+ console.error("Failed to generate email preview:", error);
3547
+ return Response.json({
3548
+ success: false,
3549
+ error: "Failed to generate email preview"
3550
+ }, { status: 500 });
3551
+ }
3552
+ }
3553
+ };
3554
+ };
3555
+
3478
3556
  // src/endpoints/broadcasts/index.ts
3479
3557
  var createBroadcastManagementEndpoints = (config) => {
3480
3558
  if (!config.features?.newsletterManagement?.enabled) {
@@ -3484,7 +3562,8 @@ var createBroadcastManagementEndpoints = (config) => {
3484
3562
  return [
3485
3563
  createSendBroadcastEndpoint(config, collectionSlug),
3486
3564
  createScheduleBroadcastEndpoint(config, collectionSlug),
3487
- createTestBroadcastEndpoint(config, collectionSlug)
3565
+ createTestBroadcastEndpoint(config, collectionSlug),
3566
+ createBroadcastPreviewEndpoint(config, collectionSlug)
3488
3567
  ];
3489
3568
  };
3490
3569
 
@@ -4410,7 +4489,9 @@ var createBroadcastsCollection = (pluginConfig) => {
4410
4489
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
4411
4490
  const provider = new BroadcastApiProvider2(providerConfig);
4412
4491
  req.payload.logger.info("Converting content to HTML...");
4413
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
4492
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
4493
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4494
+ });
4414
4495
  if (!htmlContent || htmlContent.trim() === "") {
4415
4496
  req.payload.logger.info("Skipping provider sync - content is empty after conversion");
4416
4497
  return doc;
@@ -4507,7 +4588,9 @@ var createBroadcastsCollection = (pluginConfig) => {
4507
4588
  return doc;
4508
4589
  }
4509
4590
  req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
4510
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
4591
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
4592
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4593
+ });
4511
4594
  if (!htmlContent || htmlContent.trim() === "") {
4512
4595
  req.payload.logger.info("Skipping provider sync - content is empty after conversion");
4513
4596
  return doc;
@@ -4566,7 +4649,9 @@ var createBroadcastsCollection = (pluginConfig) => {
4566
4649
  updates.preheader = doc.contentSection?.preheader;
4567
4650
  }
4568
4651
  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
4569
- updates.content = await convertToEmailSafeHtml(doc.contentSection?.content);
4652
+ updates.content = await convertToEmailSafeHtml(doc.contentSection?.content, {
4653
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4654
+ });
4570
4655
  }
4571
4656
  if (doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens) {
4572
4657
  updates.trackOpens = doc.settings.trackOpens;