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.cjs CHANGED
@@ -3191,62 +3191,73 @@ async function convertToEmailSafeHtml(editorState, options) {
3191
3191
  if (!editorState) {
3192
3192
  return "";
3193
3193
  }
3194
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
3194
+ const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
3195
3195
  const sanitizedHtml = import_isomorphic_dompurify2.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
3196
3196
  if (options?.wrapInTemplate) {
3197
3197
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
3198
3198
  }
3199
3199
  return sanitizedHtml;
3200
3200
  }
3201
- async function lexicalToEmailHtml(editorState, mediaUrl) {
3201
+ async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
3202
3202
  const { root } = editorState;
3203
3203
  if (!root || !root.children) {
3204
3204
  return "";
3205
3205
  }
3206
- const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
3207
- return html;
3206
+ const htmlParts = await Promise.all(
3207
+ root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
3208
+ );
3209
+ return htmlParts.join("");
3208
3210
  }
3209
- function convertNode(node, mediaUrl) {
3211
+ async function convertNode(node, mediaUrl, customBlockConverter) {
3210
3212
  switch (node.type) {
3211
3213
  case "paragraph":
3212
- return convertParagraph(node, mediaUrl);
3214
+ return convertParagraph(node, mediaUrl, customBlockConverter);
3213
3215
  case "heading":
3214
- return convertHeading(node, mediaUrl);
3216
+ return convertHeading(node, mediaUrl, customBlockConverter);
3215
3217
  case "list":
3216
- return convertList(node, mediaUrl);
3218
+ return convertList(node, mediaUrl, customBlockConverter);
3217
3219
  case "listitem":
3218
- return convertListItem(node, mediaUrl);
3220
+ return convertListItem(node, mediaUrl, customBlockConverter);
3219
3221
  case "blockquote":
3220
- return convertBlockquote(node, mediaUrl);
3222
+ return convertBlockquote(node, mediaUrl, customBlockConverter);
3221
3223
  case "text":
3222
3224
  return convertText(node);
3223
3225
  case "link":
3224
- return convertLink(node, mediaUrl);
3226
+ return convertLink(node, mediaUrl, customBlockConverter);
3225
3227
  case "linebreak":
3226
3228
  return "<br>";
3227
3229
  case "upload":
3228
3230
  return convertUpload(node, mediaUrl);
3229
3231
  case "block":
3230
- return convertBlock(node, mediaUrl);
3232
+ return await convertBlock(node, mediaUrl, customBlockConverter);
3231
3233
  default:
3232
3234
  if (node.children) {
3233
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
3235
+ const childParts = await Promise.all(
3236
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
3237
+ );
3238
+ return childParts.join("");
3234
3239
  }
3235
3240
  return "";
3236
3241
  }
3237
3242
  }
3238
- function convertParagraph(node, mediaUrl) {
3243
+ async function convertParagraph(node, mediaUrl, customBlockConverter) {
3239
3244
  const align = getAlignment(node.format);
3240
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3245
+ const childParts = await Promise.all(
3246
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3247
+ );
3248
+ const children = childParts.join("");
3241
3249
  if (!children.trim()) {
3242
3250
  return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
3243
3251
  }
3244
3252
  return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
3245
3253
  }
3246
- function convertHeading(node, mediaUrl) {
3254
+ async function convertHeading(node, mediaUrl, customBlockConverter) {
3247
3255
  const tag = node.tag || "h1";
3248
3256
  const align = getAlignment(node.format);
3249
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3257
+ const childParts = await Promise.all(
3258
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3259
+ );
3260
+ const children = childParts.join("");
3250
3261
  const styles2 = {
3251
3262
  h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
3252
3263
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
@@ -3255,18 +3266,27 @@ function convertHeading(node, mediaUrl) {
3255
3266
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
3256
3267
  return `<${tag} style="${style}">${children}</${tag}>`;
3257
3268
  }
3258
- function convertList(node, mediaUrl) {
3269
+ async function convertList(node, mediaUrl, customBlockConverter) {
3259
3270
  const tag = node.listType === "number" ? "ol" : "ul";
3260
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3271
+ const childParts = await Promise.all(
3272
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3273
+ );
3274
+ const children = childParts.join("");
3261
3275
  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;";
3262
3276
  return `<${tag} style="${style}">${children}</${tag}>`;
3263
3277
  }
3264
- function convertListItem(node, mediaUrl) {
3265
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3278
+ async function convertListItem(node, mediaUrl, customBlockConverter) {
3279
+ const childParts = await Promise.all(
3280
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3281
+ );
3282
+ const children = childParts.join("");
3266
3283
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
3267
3284
  }
3268
- function convertBlockquote(node, mediaUrl) {
3269
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3285
+ async function convertBlockquote(node, mediaUrl, customBlockConverter) {
3286
+ const childParts = await Promise.all(
3287
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3288
+ );
3289
+ const children = childParts.join("");
3270
3290
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
3271
3291
  return `<blockquote style="${style}">${children}</blockquote>`;
3272
3292
  }
@@ -3286,8 +3306,11 @@ function convertText(node) {
3286
3306
  }
3287
3307
  return text;
3288
3308
  }
3289
- function convertLink(node, mediaUrl) {
3290
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
3309
+ async function convertLink(node, mediaUrl, customBlockConverter) {
3310
+ const childParts = await Promise.all(
3311
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3312
+ );
3313
+ const children = childParts.join("");
3291
3314
  const url = node.fields?.url || "#";
3292
3315
  const newTab = node.fields?.newTab ?? false;
3293
3316
  const targetAttr = newTab ? ' target="_blank"' : "";
@@ -3318,8 +3341,18 @@ function convertUpload(node, mediaUrl) {
3318
3341
  }
3319
3342
  return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
3320
3343
  }
3321
- function convertBlock(node, mediaUrl) {
3322
- const blockType = node.fields?.blockName;
3344
+ async function convertBlock(node, mediaUrl, customBlockConverter) {
3345
+ const blockType = node.fields?.blockName || node.blockName;
3346
+ if (customBlockConverter) {
3347
+ try {
3348
+ const customHtml = await customBlockConverter(node, mediaUrl);
3349
+ if (customHtml) {
3350
+ return customHtml;
3351
+ }
3352
+ } catch (error) {
3353
+ console.error(`Custom block converter error for ${blockType}:`, error);
3354
+ }
3355
+ }
3323
3356
  switch (blockType) {
3324
3357
  case "button":
3325
3358
  return convertButtonBlock(node.fields);
@@ -3327,7 +3360,10 @@ function convertBlock(node, mediaUrl) {
3327
3360
  return convertDividerBlock(node.fields);
3328
3361
  default:
3329
3362
  if (node.children) {
3330
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
3363
+ const childParts = await Promise.all(
3364
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
3365
+ );
3366
+ return childParts.join("");
3331
3367
  }
3332
3368
  return "";
3333
3369
  }
@@ -3454,7 +3490,8 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
3454
3490
  }
3455
3491
  const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
3456
3492
  wrapInTemplate: true,
3457
- preheader: broadcast.preheader
3493
+ preheader: broadcast.preheader,
3494
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3458
3495
  });
3459
3496
  const emailService = req.payload.newsletterEmailService;
3460
3497
  if (!emailService) {
@@ -3492,6 +3529,47 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
3492
3529
  };
3493
3530
  };
3494
3531
 
3532
+ // src/endpoints/broadcasts/preview.ts
3533
+ var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
3534
+ return {
3535
+ path: `/api/${collectionSlug}/preview`,
3536
+ method: "post",
3537
+ handler: async (req) => {
3538
+ try {
3539
+ const data = await (req.json?.() || Promise.resolve({}));
3540
+ const { content, preheader, subject } = data;
3541
+ if (!content) {
3542
+ return Response.json({
3543
+ success: false,
3544
+ error: "Content is required for preview"
3545
+ }, { status: 400 });
3546
+ }
3547
+ const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
3548
+ const htmlContent = await convertToEmailSafeHtml(content, {
3549
+ wrapInTemplate: true,
3550
+ preheader,
3551
+ mediaUrl,
3552
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
3553
+ });
3554
+ return Response.json({
3555
+ success: true,
3556
+ preview: {
3557
+ subject: subject || "Preview",
3558
+ preheader: preheader || "",
3559
+ html: htmlContent
3560
+ }
3561
+ });
3562
+ } catch (error) {
3563
+ console.error("Failed to generate email preview:", error);
3564
+ return Response.json({
3565
+ success: false,
3566
+ error: "Failed to generate email preview"
3567
+ }, { status: 500 });
3568
+ }
3569
+ }
3570
+ };
3571
+ };
3572
+
3495
3573
  // src/endpoints/broadcasts/index.ts
3496
3574
  var createBroadcastManagementEndpoints = (config) => {
3497
3575
  if (!config.features?.newsletterManagement?.enabled) {
@@ -3501,7 +3579,8 @@ var createBroadcastManagementEndpoints = (config) => {
3501
3579
  return [
3502
3580
  createSendBroadcastEndpoint(config, collectionSlug),
3503
3581
  createScheduleBroadcastEndpoint(config, collectionSlug),
3504
- createTestBroadcastEndpoint(config, collectionSlug)
3582
+ createTestBroadcastEndpoint(config, collectionSlug),
3583
+ createBroadcastPreviewEndpoint(config, collectionSlug)
3505
3584
  ];
3506
3585
  };
3507
3586
 
@@ -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;