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.
@@ -1016,9 +1016,9 @@ async function convertParagraph(node, mediaUrl, customBlockConverter) {
1016
1016
  );
1017
1017
  const children = childParts.join("");
1018
1018
  if (!children.trim()) {
1019
- return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
1019
+ return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
1020
1020
  }
1021
- return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
1021
+ 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>`;
1022
1022
  }
1023
1023
  async function convertHeading(node, mediaUrl, customBlockConverter) {
1024
1024
  const tag = node.tag || "h1";
@@ -1032,8 +1032,14 @@ async function convertHeading(node, mediaUrl, customBlockConverter) {
1032
1032
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
1033
1033
  h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
1034
1034
  };
1035
+ const mobileClasses = {
1036
+ h1: "mobile-font-size-24",
1037
+ h2: "mobile-font-size-20",
1038
+ h3: "mobile-font-size-16"
1039
+ };
1035
1040
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
1036
- return `<${tag} style="${style}">${children}</${tag}>`;
1041
+ const mobileClass = mobileClasses[tag] || mobileClasses.h3;
1042
+ return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
1037
1043
  }
1038
1044
  async function convertList(node, mediaUrl, customBlockConverter) {
1039
1045
  const tag = node.listType === "number" ? "ol" : "ul";
@@ -1041,8 +1047,8 @@ async function convertList(node, mediaUrl, customBlockConverter) {
1041
1047
  (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1042
1048
  );
1043
1049
  const children = childParts.join("");
1044
- 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;";
1045
- return `<${tag} style="${style}">${children}</${tag}>`;
1050
+ 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;";
1051
+ return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
1046
1052
  }
1047
1053
  async function convertListItem(node, mediaUrl, customBlockConverter) {
1048
1054
  const childParts = await Promise.all(
@@ -1099,16 +1105,16 @@ function convertUpload(node, mediaUrl) {
1099
1105
  }
1100
1106
  const alt = node.fields?.altText || upload.alt || "";
1101
1107
  const caption = node.fields?.caption || "";
1102
- const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
1108
+ 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;" />`;
1103
1109
  if (caption) {
1104
1110
  return `
1105
- <div style="margin: 0 0 16px 0; text-align: center;">
1111
+ <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
1106
1112
  ${imgHtml}
1107
- <p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
1113
+ <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>
1108
1114
  </div>
1109
1115
  `;
1110
1116
  }
1111
- return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
1117
+ return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
1112
1118
  }
1113
1119
  async function convertBlock(node, mediaUrl, customBlockConverter) {
1114
1120
  const blockType = node.fields?.blockName || node.blockName;
@@ -1181,11 +1187,14 @@ function escapeHtml(text) {
1181
1187
  }
1182
1188
  function wrapInEmailTemplate(content, preheader) {
1183
1189
  return `<!DOCTYPE html>
1184
- <html lang="en">
1190
+ <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">
1185
1191
  <head>
1186
1192
  <meta charset="UTF-8">
1187
1193
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1188
- <title>Email</title>
1194
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
1195
+ <meta name="x-apple-disable-message-reformatting">
1196
+ <title>Newsletter</title>
1197
+
1189
1198
  <!--[if mso]>
1190
1199
  <noscript>
1191
1200
  <xml>
@@ -1195,16 +1204,155 @@ function wrapInEmailTemplate(content, preheader) {
1195
1204
  </xml>
1196
1205
  </noscript>
1197
1206
  <![endif]-->
1207
+
1208
+ <style>
1209
+ /* Reset and base styles */
1210
+ * {
1211
+ -webkit-text-size-adjust: 100%;
1212
+ -ms-text-size-adjust: 100%;
1213
+ }
1214
+
1215
+ body {
1216
+ margin: 0 !important;
1217
+ padding: 0 !important;
1218
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
1219
+ font-size: 16px;
1220
+ line-height: 1.5;
1221
+ color: #1A1A1A;
1222
+ background-color: #f8f9fa;
1223
+ -webkit-font-smoothing: antialiased;
1224
+ -moz-osx-font-smoothing: grayscale;
1225
+ }
1226
+
1227
+ table {
1228
+ border-spacing: 0 !important;
1229
+ border-collapse: collapse !important;
1230
+ table-layout: fixed !important;
1231
+ margin: 0 auto !important;
1232
+ }
1233
+
1234
+ table table table {
1235
+ table-layout: auto;
1236
+ }
1237
+
1238
+ img {
1239
+ -ms-interpolation-mode: bicubic;
1240
+ max-width: 100%;
1241
+ height: auto;
1242
+ border: 0;
1243
+ outline: none;
1244
+ text-decoration: none;
1245
+ }
1246
+
1247
+ /* Responsive styles */
1248
+ @media only screen and (max-width: 640px) {
1249
+ .mobile-hide {
1250
+ display: none !important;
1251
+ }
1252
+
1253
+ .mobile-center {
1254
+ text-align: center !important;
1255
+ }
1256
+
1257
+ .mobile-width-100 {
1258
+ width: 100% !important;
1259
+ max-width: 100% !important;
1260
+ }
1261
+
1262
+ .mobile-padding {
1263
+ padding: 20px !important;
1264
+ }
1265
+
1266
+ .mobile-padding-sm {
1267
+ padding: 16px !important;
1268
+ }
1269
+
1270
+ .mobile-font-size-14 {
1271
+ font-size: 14px !important;
1272
+ }
1273
+
1274
+ .mobile-font-size-16 {
1275
+ font-size: 16px !important;
1276
+ }
1277
+
1278
+ .mobile-font-size-20 {
1279
+ font-size: 20px !important;
1280
+ line-height: 1.3 !important;
1281
+ }
1282
+
1283
+ .mobile-font-size-24 {
1284
+ font-size: 24px !important;
1285
+ line-height: 1.2 !important;
1286
+ }
1287
+
1288
+ /* Stack sections on mobile */
1289
+ .mobile-stack {
1290
+ display: block !important;
1291
+ width: 100% !important;
1292
+ }
1293
+
1294
+ /* Mobile-specific spacing */
1295
+ .mobile-margin-bottom-16 {
1296
+ margin-bottom: 16px !important;
1297
+ }
1298
+
1299
+ .mobile-margin-bottom-20 {
1300
+ margin-bottom: 20px !important;
1301
+ }
1302
+ }
1303
+
1304
+ /* Dark mode support */
1305
+ @media (prefers-color-scheme: dark) {
1306
+ .dark-mode-bg {
1307
+ background-color: #1a1a1a !important;
1308
+ }
1309
+
1310
+ .dark-mode-text {
1311
+ color: #ffffff !important;
1312
+ }
1313
+
1314
+ .dark-mode-border {
1315
+ border-color: #333333 !important;
1316
+ }
1317
+ }
1318
+
1319
+ /* Outlook-specific fixes */
1320
+ <!--[if mso]>
1321
+ <style>
1322
+ table {
1323
+ border-collapse: collapse;
1324
+ border-spacing: 0;
1325
+ border: none;
1326
+ margin: 0;
1327
+ }
1328
+
1329
+ div, p {
1330
+ margin: 0;
1331
+ }
1332
+ </style>
1333
+ <![endif]-->
1334
+ </style>
1198
1335
  </head>
1199
- <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;">
1200
- ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
1201
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
1336
+ <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;">
1337
+ ${preheader ? `
1338
+ <!-- Preheader text -->
1339
+ <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
1340
+ ${escapeHtml(preheader)}
1341
+ </div>
1342
+ ` : ""}
1343
+
1344
+ <!-- Main container -->
1345
+ <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
1202
1346
  <tr>
1203
- <td align="center" style="padding: 20px 0;">
1204
- <table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
1347
+ <td align="center" style="padding: 20px 10px;">
1348
+ <!-- Email wrapper -->
1349
+ <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
1205
1350
  <tr>
1206
- <td style="padding: 40px 30px;">
1207
- ${content}
1351
+ <td class="mobile-padding" style="padding: 0;">
1352
+ <!-- Content area with light background -->
1353
+ <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
1354
+ ${content}
1355
+ </div>
1208
1356
  </td>
1209
1357
  </tr>
1210
1358
  </table>
@@ -1612,6 +1760,86 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
1612
1760
  };
1613
1761
 
1614
1762
  // src/endpoints/broadcasts/preview.ts
1763
+ async function populateMediaFields(content, payload, config) {
1764
+ if (!content || typeof content !== "object") return content;
1765
+ if (content.root?.children) {
1766
+ for (const child of content.root.children) {
1767
+ await populateBlockMediaFields(child, payload, config);
1768
+ }
1769
+ }
1770
+ return content;
1771
+ }
1772
+ async function populateBlockMediaFields(node, payload, config) {
1773
+ if (node.type === "block" && node.fields) {
1774
+ const blockType = node.fields.blockType || node.fields.blockName;
1775
+ const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
1776
+ const blockConfig = customBlocks.find((b) => b.slug === blockType);
1777
+ if (blockConfig && blockConfig.fields) {
1778
+ for (const field of blockConfig.fields) {
1779
+ if (field.type === "upload" && field.relationTo && node.fields[field.name]) {
1780
+ const fieldValue = node.fields[field.name];
1781
+ if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
1782
+ try {
1783
+ const media = await payload.findByID({
1784
+ collection: field.relationTo,
1785
+ id: fieldValue,
1786
+ depth: 0
1787
+ });
1788
+ if (media) {
1789
+ node.fields[field.name] = media;
1790
+ payload.logger?.info(`Populated ${field.name} for block ${blockType}:`, {
1791
+ mediaId: fieldValue,
1792
+ mediaUrl: media.url,
1793
+ filename: media.filename
1794
+ });
1795
+ }
1796
+ } catch (error) {
1797
+ payload.logger?.error(`Failed to populate ${field.name} for block ${blockType}:`, error);
1798
+ }
1799
+ }
1800
+ }
1801
+ if (field.type === "array" && field.fields) {
1802
+ const arrayValue = node.fields[field.name];
1803
+ if (Array.isArray(arrayValue)) {
1804
+ for (const arrayItem of arrayValue) {
1805
+ if (arrayItem && typeof arrayItem === "object") {
1806
+ for (const arrayField of field.fields) {
1807
+ if (arrayField.type === "upload" && arrayField.relationTo && arrayItem[arrayField.name]) {
1808
+ const arrayFieldValue = arrayItem[arrayField.name];
1809
+ if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
1810
+ try {
1811
+ const media = await payload.findByID({
1812
+ collection: arrayField.relationTo,
1813
+ id: arrayFieldValue,
1814
+ depth: 0
1815
+ });
1816
+ if (media) {
1817
+ arrayItem[arrayField.name] = media;
1818
+ payload.logger?.info(`Populated array ${arrayField.name} for block ${blockType}:`, {
1819
+ mediaId: arrayFieldValue,
1820
+ mediaUrl: media.url,
1821
+ filename: media.filename
1822
+ });
1823
+ }
1824
+ } catch (error) {
1825
+ payload.logger?.error(`Failed to populate array ${arrayField.name} for block ${blockType}:`, error);
1826
+ }
1827
+ }
1828
+ }
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ if (node.children) {
1838
+ for (const child of node.children) {
1839
+ await populateBlockMediaFields(child, payload, config);
1840
+ }
1841
+ }
1842
+ }
1615
1843
  var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1616
1844
  return {
1617
1845
  path: "/preview",
@@ -1627,7 +1855,9 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1627
1855
  }, { status: 400 });
1628
1856
  }
1629
1857
  const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
1630
- const htmlContent = await convertToEmailSafeHtml(content, {
1858
+ req.payload.logger?.info("Populating media fields for email preview...");
1859
+ const populatedContent = await populateMediaFields(content, req.payload, config);
1860
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
1631
1861
  wrapInTemplate: true,
1632
1862
  preheader,
1633
1863
  mediaUrl,
@@ -1926,8 +2156,9 @@ var createBroadcastsCollection = (pluginConfig) => {
1926
2156
  }
1927
2157
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1928
2158
  const provider = new BroadcastApiProvider2(providerConfig);
1929
- req.payload.logger.info("Converting content to HTML...");
1930
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
2159
+ req.payload.logger.info("Populating media fields and converting content to HTML...");
2160
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2161
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
1931
2162
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
1932
2163
  });
1933
2164
  if (!htmlContent || htmlContent.trim() === "") {
@@ -2026,7 +2257,8 @@ var createBroadcastsCollection = (pluginConfig) => {
2026
2257
  return doc;
2027
2258
  }
2028
2259
  req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
2029
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
2260
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2261
+ const htmlContent = await convertToEmailSafeHtml(populatedContent, {
2030
2262
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2031
2263
  });
2032
2264
  if (!htmlContent || htmlContent.trim() === "") {
@@ -2087,7 +2319,8 @@ var createBroadcastsCollection = (pluginConfig) => {
2087
2319
  updates.preheader = doc.contentSection?.preheader;
2088
2320
  }
2089
2321
  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
2090
- updates.content = await convertToEmailSafeHtml(doc.contentSection?.content, {
2322
+ const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2323
+ updates.content = await convertToEmailSafeHtml(populatedContent, {
2091
2324
  customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2092
2325
  });
2093
2326
  }