payload-plugin-newsletter 0.18.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.
@@ -963,6 +963,12 @@ async function convertToEmailSafeHtml(editorState, options) {
963
963
  const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
964
964
  const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
965
965
  if (options?.wrapInTemplate) {
966
+ if (options.customWrapper) {
967
+ return await Promise.resolve(options.customWrapper(sanitizedHtml, {
968
+ preheader: options.preheader,
969
+ subject: options.subject
970
+ }));
971
+ }
966
972
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
967
973
  }
968
974
  return sanitizedHtml;
@@ -1016,9 +1022,9 @@ async function convertParagraph(node, mediaUrl, customBlockConverter) {
1016
1022
  );
1017
1023
  const children = childParts.join("");
1018
1024
  if (!children.trim()) {
1019
- return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
1025
+ return '<p class="mobile-margin-bottom-16" style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
1020
1026
  }
1021
- return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
1027
+ 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
1028
  }
1023
1029
  async function convertHeading(node, mediaUrl, customBlockConverter) {
1024
1030
  const tag = node.tag || "h1";
@@ -1032,8 +1038,14 @@ async function convertHeading(node, mediaUrl, customBlockConverter) {
1032
1038
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
1033
1039
  h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
1034
1040
  };
1041
+ const mobileClasses = {
1042
+ h1: "mobile-font-size-24",
1043
+ h2: "mobile-font-size-20",
1044
+ h3: "mobile-font-size-16"
1045
+ };
1035
1046
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
1036
- return `<${tag} style="${style}">${children}</${tag}>`;
1047
+ const mobileClass = mobileClasses[tag] || mobileClasses.h3;
1048
+ return `<${tag} class="${mobileClass}" style="${style}">${children}</${tag}>`;
1037
1049
  }
1038
1050
  async function convertList(node, mediaUrl, customBlockConverter) {
1039
1051
  const tag = node.listType === "number" ? "ol" : "ul";
@@ -1041,8 +1053,8 @@ async function convertList(node, mediaUrl, customBlockConverter) {
1041
1053
  (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1042
1054
  );
1043
1055
  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}>`;
1056
+ 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;";
1057
+ return `<${tag} class="mobile-margin-bottom-16" style="${style}">${children}</${tag}>`;
1046
1058
  }
1047
1059
  async function convertListItem(node, mediaUrl, customBlockConverter) {
1048
1060
  const childParts = await Promise.all(
@@ -1099,16 +1111,16 @@ function convertUpload(node, mediaUrl) {
1099
1111
  }
1100
1112
  const alt = node.fields?.altText || upload.alt || "";
1101
1113
  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;" />`;
1114
+ 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
1115
  if (caption) {
1104
1116
  return `
1105
- <div style="margin: 0 0 16px 0; text-align: center;">
1117
+ <div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">
1106
1118
  ${imgHtml}
1107
- <p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
1119
+ <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
1120
  </div>
1109
1121
  `;
1110
1122
  }
1111
- return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
1123
+ return `<div style="margin: 0 0 16px 0; text-align: center;" class="mobile-margin-bottom-16">${imgHtml}</div>`;
1112
1124
  }
1113
1125
  async function convertBlock(node, mediaUrl, customBlockConverter) {
1114
1126
  const blockType = node.fields?.blockName || node.blockName;
@@ -1181,11 +1193,14 @@ function escapeHtml(text) {
1181
1193
  }
1182
1194
  function wrapInEmailTemplate(content, preheader) {
1183
1195
  return `<!DOCTYPE html>
1184
- <html lang="en">
1196
+ <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
1197
  <head>
1186
1198
  <meta charset="UTF-8">
1187
1199
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1188
- <title>Email</title>
1200
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
1201
+ <meta name="x-apple-disable-message-reformatting">
1202
+ <title>Newsletter</title>
1203
+
1189
1204
  <!--[if mso]>
1190
1205
  <noscript>
1191
1206
  <xml>
@@ -1195,16 +1210,155 @@ function wrapInEmailTemplate(content, preheader) {
1195
1210
  </xml>
1196
1211
  </noscript>
1197
1212
  <![endif]-->
1213
+
1214
+ <style>
1215
+ /* Reset and base styles */
1216
+ * {
1217
+ -webkit-text-size-adjust: 100%;
1218
+ -ms-text-size-adjust: 100%;
1219
+ }
1220
+
1221
+ body {
1222
+ margin: 0 !important;
1223
+ padding: 0 !important;
1224
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
1225
+ font-size: 16px;
1226
+ line-height: 1.5;
1227
+ color: #1A1A1A;
1228
+ background-color: #f8f9fa;
1229
+ -webkit-font-smoothing: antialiased;
1230
+ -moz-osx-font-smoothing: grayscale;
1231
+ }
1232
+
1233
+ table {
1234
+ border-spacing: 0 !important;
1235
+ border-collapse: collapse !important;
1236
+ table-layout: fixed !important;
1237
+ margin: 0 auto !important;
1238
+ }
1239
+
1240
+ table table table {
1241
+ table-layout: auto;
1242
+ }
1243
+
1244
+ img {
1245
+ -ms-interpolation-mode: bicubic;
1246
+ max-width: 100%;
1247
+ height: auto;
1248
+ border: 0;
1249
+ outline: none;
1250
+ text-decoration: none;
1251
+ }
1252
+
1253
+ /* Responsive styles */
1254
+ @media only screen and (max-width: 640px) {
1255
+ .mobile-hide {
1256
+ display: none !important;
1257
+ }
1258
+
1259
+ .mobile-center {
1260
+ text-align: center !important;
1261
+ }
1262
+
1263
+ .mobile-width-100 {
1264
+ width: 100% !important;
1265
+ max-width: 100% !important;
1266
+ }
1267
+
1268
+ .mobile-padding {
1269
+ padding: 20px !important;
1270
+ }
1271
+
1272
+ .mobile-padding-sm {
1273
+ padding: 16px !important;
1274
+ }
1275
+
1276
+ .mobile-font-size-14 {
1277
+ font-size: 14px !important;
1278
+ }
1279
+
1280
+ .mobile-font-size-16 {
1281
+ font-size: 16px !important;
1282
+ }
1283
+
1284
+ .mobile-font-size-20 {
1285
+ font-size: 20px !important;
1286
+ line-height: 1.3 !important;
1287
+ }
1288
+
1289
+ .mobile-font-size-24 {
1290
+ font-size: 24px !important;
1291
+ line-height: 1.2 !important;
1292
+ }
1293
+
1294
+ /* Stack sections on mobile */
1295
+ .mobile-stack {
1296
+ display: block !important;
1297
+ width: 100% !important;
1298
+ }
1299
+
1300
+ /* Mobile-specific spacing */
1301
+ .mobile-margin-bottom-16 {
1302
+ margin-bottom: 16px !important;
1303
+ }
1304
+
1305
+ .mobile-margin-bottom-20 {
1306
+ margin-bottom: 20px !important;
1307
+ }
1308
+ }
1309
+
1310
+ /* Dark mode support */
1311
+ @media (prefers-color-scheme: dark) {
1312
+ .dark-mode-bg {
1313
+ background-color: #1a1a1a !important;
1314
+ }
1315
+
1316
+ .dark-mode-text {
1317
+ color: #ffffff !important;
1318
+ }
1319
+
1320
+ .dark-mode-border {
1321
+ border-color: #333333 !important;
1322
+ }
1323
+ }
1324
+
1325
+ /* Outlook-specific fixes */
1326
+ <!--[if mso]>
1327
+ <style>
1328
+ table {
1329
+ border-collapse: collapse;
1330
+ border-spacing: 0;
1331
+ border: none;
1332
+ margin: 0;
1333
+ }
1334
+
1335
+ div, p {
1336
+ margin: 0;
1337
+ }
1338
+ </style>
1339
+ <![endif]-->
1340
+ </style>
1198
1341
  </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;">
1342
+ <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;">
1343
+ ${preheader ? `
1344
+ <!-- Preheader text -->
1345
+ <div style="display: none; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: transparent;">
1346
+ ${escapeHtml(preheader)}
1347
+ </div>
1348
+ ` : ""}
1349
+
1350
+ <!-- Main container -->
1351
+ <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0; background-color: #f8f9fa;">
1202
1352
  <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;">
1353
+ <td align="center" style="padding: 20px 10px;">
1354
+ <!-- Email wrapper -->
1355
+ <table role="presentation" cellpadding="0" cellspacing="0" width="600" class="mobile-width-100" style="margin: 0 auto; max-width: 600px;">
1205
1356
  <tr>
1206
- <td style="padding: 40px 30px;">
1207
- ${content}
1357
+ <td class="mobile-padding" style="padding: 0;">
1358
+ <!-- Content area with light background -->
1359
+ <div style="background-color: #ffffff; padding: 40px 30px; border-radius: 8px;" class="mobile-padding">
1360
+ ${content}
1361
+ </div>
1208
1362
  </td>
1209
1363
  </tr>
1210
1364
  </table>
@@ -1709,11 +1863,14 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1709
1863
  const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
1710
1864
  req.payload.logger?.info("Populating media fields for email preview...");
1711
1865
  const populatedContent = await populateMediaFields(content, req.payload, config);
1866
+ const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
1712
1867
  const htmlContent = await convertToEmailSafeHtml(populatedContent, {
1713
- wrapInTemplate: true,
1868
+ wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
1714
1869
  preheader,
1870
+ subject,
1715
1871
  mediaUrl,
1716
- customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
1872
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
1873
+ customWrapper: emailPreviewConfig?.customWrapper
1717
1874
  });
1718
1875
  return Response.json({
1719
1876
  success: true,