payload-plugin-newsletter 0.17.4 → 0.19.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
@@ -3637,9 +3637,9 @@ async function convertParagraph(node, mediaUrl, customBlockConverter) {
3637
3637
  );
3638
3638
  const children = childParts.join("");
3639
3639
  if (!children.trim()) {
3640
- return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
3640
+ return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
3641
3641
  }
3642
- return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
3642
+ 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>`;
3643
3643
  }
3644
3644
  async function convertHeading(node, mediaUrl, customBlockConverter) {
3645
3645
  const tag = node.tag || "h1";
@@ -3653,8 +3653,14 @@ async function convertHeading(node, mediaUrl, customBlockConverter) {
3653
3653
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
3654
3654
  h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
3655
3655
  };
3656
+ const mobileClasses = {
3657
+ h1: "mobile-font-size-24",
3658
+ h2: "mobile-font-size-20",
3659
+ h3: "mobile-font-size-16"
3660
+ };
3656
3661
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
3657
- return `<${tag} style="${style}">${children}</${tag}>`;
3662
+ const mobileClass = mobileClasses[tag] || mobileClasses.h3;
3663
+ return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
3658
3664
  }
3659
3665
  async function convertList(node, mediaUrl, customBlockConverter) {
3660
3666
  const tag = node.listType === "number" ? "ol" : "ul";
@@ -3662,8 +3668,8 @@ async function convertList(node, mediaUrl, customBlockConverter) {
3662
3668
  (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
3663
3669
  );
3664
3670
  const children = childParts.join("");
3665
- 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;";
3666
- return `<${tag} style="${style}">${children}</${tag}>`;
3671
+ 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;";
3672
+ return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
3667
3673
  }
3668
3674
  async function convertListItem(node, mediaUrl, customBlockConverter) {
3669
3675
  const childParts = await Promise.all(
@@ -3720,16 +3726,16 @@ function convertUpload(node, mediaUrl) {
3720
3726
  }
3721
3727
  const alt = node.fields?.altText || upload.alt || "";
3722
3728
  const caption = node.fields?.caption || "";
3723
- const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
3729
+ 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;" />`;
3724
3730
  if (caption) {
3725
3731
  return `
3726
- <div style="margin: 0 0 16px 0; text-align: center;">
3732
+ <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
3727
3733
  ${imgHtml}
3728
- <p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
3734
+ <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>
3729
3735
  </div>
3730
3736
  `;
3731
3737
  }
3732
- return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
3738
+ return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
3733
3739
  }
3734
3740
  async function convertBlock(node, mediaUrl, customBlockConverter) {
3735
3741
  const blockType = node.fields?.blockName || node.blockName;
@@ -3802,11 +3808,14 @@ function escapeHtml(text) {
3802
3808
  }
3803
3809
  function wrapInEmailTemplate(content, preheader) {
3804
3810
  return `<!DOCTYPE html>
3805
- <html lang="en">
3811
+ <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">
3806
3812
  <head>
3807
3813
  <meta charset="UTF-8">
3808
3814
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
3809
- <title>Email</title>
3815
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
3816
+ <meta name="x-apple-disable-message-reformatting">
3817
+ <title>Newsletter</title>
3818
+
3810
3819
  <!--[if mso]>
3811
3820
  <noscript>
3812
3821
  <xml>
@@ -3816,16 +3825,155 @@ function wrapInEmailTemplate(content, preheader) {
3816
3825
  </xml>
3817
3826
  </noscript>
3818
3827
  <![endif]-->
3828
+
3829
+ <style>
3830
+ /* Reset and base styles */
3831
+ * {
3832
+ -webkit-text-size-adjust: 100%;
3833
+ -ms-text-size-adjust: 100%;
3834
+ }
3835
+
3836
+ body {
3837
+ margin: 0 !important;
3838
+ padding: 0 !important;
3839
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
3840
+ font-size: 16px;
3841
+ line-height: 1.5;
3842
+ color: #1A1A1A;
3843
+ background-color: #f8f9fa;
3844
+ -webkit-font-smoothing: antialiased;
3845
+ -moz-osx-font-smoothing: grayscale;
3846
+ }
3847
+
3848
+ table {
3849
+ border-spacing: 0 !important;
3850
+ border-collapse: collapse !important;
3851
+ table-layout: fixed !important;
3852
+ margin: 0 auto !important;
3853
+ }
3854
+
3855
+ table table table {
3856
+ table-layout: auto;
3857
+ }
3858
+
3859
+ img {
3860
+ -ms-interpolation-mode: bicubic;
3861
+ max-width: 100%;
3862
+ height: auto;
3863
+ border: 0;
3864
+ outline: none;
3865
+ text-decoration: none;
3866
+ }
3867
+
3868
+ /* Responsive styles */
3869
+ @media only screen and (max-width: 640px) {
3870
+ .mobile-hide {
3871
+ display: none !important;
3872
+ }
3873
+
3874
+ .mobile-center {
3875
+ text-align: center !important;
3876
+ }
3877
+
3878
+ .mobile-width-100 {
3879
+ width: 100% !important;
3880
+ max-width: 100% !important;
3881
+ }
3882
+
3883
+ .mobile-padding {
3884
+ padding: 20px !important;
3885
+ }
3886
+
3887
+ .mobile-padding-sm {
3888
+ padding: 16px !important;
3889
+ }
3890
+
3891
+ .mobile-font-size-14 {
3892
+ font-size: 14px !important;
3893
+ }
3894
+
3895
+ .mobile-font-size-16 {
3896
+ font-size: 16px !important;
3897
+ }
3898
+
3899
+ .mobile-font-size-20 {
3900
+ font-size: 20px !important;
3901
+ line-height: 1.3 !important;
3902
+ }
3903
+
3904
+ .mobile-font-size-24 {
3905
+ font-size: 24px !important;
3906
+ line-height: 1.2 !important;
3907
+ }
3908
+
3909
+ /* Stack sections on mobile */
3910
+ .mobile-stack {
3911
+ display: block !important;
3912
+ width: 100% !important;
3913
+ }
3914
+
3915
+ /* Mobile-specific spacing */
3916
+ .mobile-margin-bottom-16 {
3917
+ margin-bottom: 16px !important;
3918
+ }
3919
+
3920
+ .mobile-margin-bottom-20 {
3921
+ margin-bottom: 20px !important;
3922
+ }
3923
+ }
3924
+
3925
+ /* Dark mode support */
3926
+ @media (prefers-color-scheme: dark) {
3927
+ .dark-mode-bg {
3928
+ background-color: #1a1a1a !important;
3929
+ }
3930
+
3931
+ .dark-mode-text {
3932
+ color: #ffffff !important;
3933
+ }
3934
+
3935
+ .dark-mode-border {
3936
+ border-color: #333333 !important;
3937
+ }
3938
+ }
3939
+
3940
+ /* Outlook-specific fixes */
3941
+ <!--[if mso]>
3942
+ <style>
3943
+ table {
3944
+ border-collapse: collapse;
3945
+ border-spacing: 0;
3946
+ border: none;
3947
+ margin: 0;
3948
+ }
3949
+
3950
+ div, p {
3951
+ margin: 0;
3952
+ }
3953
+ </style>
3954
+ <![endif]-->
3955
+ </style>
3819
3956
  </head>
3820
- <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: #333333; background-color: #f3f4f6;">
3821
- ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
3822
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
3957
+ <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;">
3958
+ ${preheader ? `
3959
+ <!-- Preheader text -->
3960
+ <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
3961
+ ${escapeHtml(preheader)}
3962
+ </div>
3963
+ ` : ""}
3964
+
3965
+ <!-- Main container -->
3966
+ <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
3823
3967
  <tr>
3824
- <td align="center" style="padding: 20px 0;">
3825
- <table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
3968
+ <td align="center" style="padding: 20px 10px;">
3969
+ <!-- Email wrapper -->
3970
+ <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
3826
3971
  <tr>
3827
- <td style="padding: 40px 30px;">
3828
- ${content}
3972
+ <td class="mobile-padding" style="padding: 0;">
3973
+ <!-- Content area with light background -->
3974
+ <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
3975
+ ${content}
3976
+ </div>
3829
3977
  </td>
3830
3978
  </tr>
3831
3979
  </table>
@@ -4172,6 +4320,86 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
4172
4320
  };
4173
4321
 
4174
4322
  // src/endpoints/broadcasts/preview.ts
4323
+ async function populateMediaFields(content, payload, config) {
4324
+ if (!content || typeof content !== "object") return content;
4325
+ if (content.root?.children) {
4326
+ for (const child of content.root.children) {
4327
+ await populateBlockMediaFields(child, payload, config);
4328
+ }
4329
+ }
4330
+ return content;
4331
+ }
4332
+ async function populateBlockMediaFields(node, payload, config) {
4333
+ if (node.type === "block" && node.fields) {
4334
+ const blockType = node.fields.blockType || node.fields.blockName;
4335
+ const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
4336
+ const blockConfig = customBlocks.find((b) => b.slug === blockType);
4337
+ if (blockConfig && blockConfig.fields) {
4338
+ for (const field of blockConfig.fields) {
4339
+ if (field.type === "upload" && field.relationTo && node.fields[field.name]) {
4340
+ const fieldValue = node.fields[field.name];
4341
+ if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
4342
+ try {
4343
+ const media = await payload.findByID({
4344
+ collection: field.relationTo,
4345
+ id: fieldValue,
4346
+ depth: 0
4347
+ });
4348
+ if (media) {
4349
+ node.fields[field.name] = media;
4350
+ payload.logger?.info(`Populated ${field.name} for block ${blockType}:`, {
4351
+ mediaId: fieldValue,
4352
+ mediaUrl: media.url,
4353
+ filename: media.filename
4354
+ });
4355
+ }
4356
+ } catch (error) {
4357
+ payload.logger?.error(`Failed to populate ${field.name} for block ${blockType}:`, error);
4358
+ }
4359
+ }
4360
+ }
4361
+ if (field.type === "array" && field.fields) {
4362
+ const arrayValue = node.fields[field.name];
4363
+ if (Array.isArray(arrayValue)) {
4364
+ for (const arrayItem of arrayValue) {
4365
+ if (arrayItem && typeof arrayItem === "object") {
4366
+ for (const arrayField of field.fields) {
4367
+ if (arrayField.type === "upload" && arrayField.relationTo && arrayItem[arrayField.name]) {
4368
+ const arrayFieldValue = arrayItem[arrayField.name];
4369
+ if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
4370
+ try {
4371
+ const media = await payload.findByID({
4372
+ collection: arrayField.relationTo,
4373
+ id: arrayFieldValue,
4374
+ depth: 0
4375
+ });
4376
+ if (media) {
4377
+ arrayItem[arrayField.name] = media;
4378
+ payload.logger?.info(`Populated array ${arrayField.name} for block ${blockType}:`, {
4379
+ mediaId: arrayFieldValue,
4380
+ mediaUrl: media.url,
4381
+ filename: media.filename
4382
+ });
4383
+ }
4384
+ } catch (error) {
4385
+ payload.logger?.error(`Failed to populate array ${arrayField.name} for block ${blockType}:`, error);
4386
+ }
4387
+ }
4388
+ }
4389
+ }
4390
+ }
4391
+ }
4392
+ }
4393
+ }
4394
+ }
4395
+ }
4396
+ }
4397
+ if (node.children) {
4398
+ for (const child of node.children) {
4399
+ await populateBlockMediaFields(child, payload, config);
4400
+ }
4401
+ }
4402
+ }
4175
4403
  var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
4176
4404
  return {
4177
4405
  path: "/preview",
@@ -4187,7 +4415,9 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
4187
4415
  }, { status: 400 });
4188
4416
  }
4189
4417
  const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
4190
- const htmlContent = await convertToEmailSafeHtml(content, {
4418
+ req.payload.logger?.info("Populating media fields for email preview...");
4419
+ const populatedContent = await populateMediaFields(content, req.payload, config);
4420
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
4191
4421
  wrapInTemplate: true,
4192
4422
  preheader,
4193
4423
  mediaUrl,
@@ -4486,8 +4716,9 @@ var createBroadcastsCollection = (pluginConfig) => {
4486
4716
  }
4487
4717
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
4488
4718
  const provider = new BroadcastApiProvider2(providerConfig);
4489
- req.payload.logger.info("Converting content to HTML...");
4490
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
4719
+ req.payload.logger.info("Populating media fields and converting content to HTML...");
4720
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
4721
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
4491
4722
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4492
4723
  });
4493
4724
  if (!htmlContent || htmlContent.trim() === "") {
@@ -4586,7 +4817,8 @@ var createBroadcastsCollection = (pluginConfig) => {
4586
4817
  return doc;
4587
4818
  }
4588
4819
  req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
4589
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
4820
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
4821
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
4590
4822
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4591
4823
  });
4592
4824
  if (!htmlContent || htmlContent.trim() === "") {
@@ -4647,7 +4879,8 @@ var createBroadcastsCollection = (pluginConfig) => {
4647
4879
  updates.preheader = doc.contentSection?.preheader;
4648
4880
  }
4649
4881
  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
4650
- updates.content = await convertToEmailSafeHtml(doc.contentSection?.content, {
4882
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
4883
+ updates.content = await convertToEmailSafeHtml(populatedContent, {
4651
4884
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4652
4885
  });
4653
4886
  }