payload-plugin-newsletter 0.10.0 → 0.11.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/CHANGELOG.md +32 -0
- package/README.md +5 -0
- package/dist/components.cjs +104 -26
- package/dist/components.cjs.map +1 -1
- package/dist/components.js +104 -26
- package/dist/components.js.map +1 -1
- package/dist/fields.cjs +109 -9
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.js +113 -9
- package/dist/fields.js.map +1 -1
- package/dist/index.cjs +221 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +225 -48
- package/dist/index.js.map +1 -1
- package/dist/utils.cjs +104 -26
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +104 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [0.11.0] - 2025-07-20
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Enhanced rich text editor for broadcast emails with:
|
|
5
|
+
- Fixed toolbar showing available formatting options
|
|
6
|
+
- Inline toolbar for text selection
|
|
7
|
+
- Image upload support with Media collection integration
|
|
8
|
+
- Custom email blocks (Button and Divider)
|
|
9
|
+
- Enhanced link feature with "open in new tab" option
|
|
10
|
+
- Comprehensive image handling in email HTML conversion:
|
|
11
|
+
- Responsive images with proper email-safe HTML
|
|
12
|
+
- Support for captions and alt text
|
|
13
|
+
- Automatic media URL handling for different storage backends
|
|
14
|
+
- Media collection setup documentation (`docs/guides/media-collection-setup.md`)
|
|
15
|
+
- Prerequisites section in README mentioning Media collection requirement
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Removed 'name' field from Broadcasts collection (now uses 'subject' as title)
|
|
19
|
+
- Updated Broadcast collection admin UI:
|
|
20
|
+
- Uses subject as the display title
|
|
21
|
+
- Shows recipientCount in default columns instead of name
|
|
22
|
+
- Enhanced `convertToEmailSafeHtml` utility to handle:
|
|
23
|
+
- Upload nodes for images
|
|
24
|
+
- Custom block nodes (button and divider)
|
|
25
|
+
- Media URL configuration
|
|
26
|
+
- Improved link handling with target attribute support
|
|
27
|
+
- Broadcast API sync now uses subject as the name field
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Broadcast hooks now properly handle the absence of name field
|
|
31
|
+
- Email preview components updated to work without channel references
|
|
32
|
+
|
|
1
33
|
## [0.10.0] - 2025-07-20
|
|
2
34
|
|
|
3
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ A complete newsletter management plugin for [Payload CMS](https://github.com/pay
|
|
|
22
22
|
- ✅ **Email Validation** - Built-in validation for email client compatibility (v0.9.0+)
|
|
23
23
|
- 📝 **Email-Safe Editor** - Rich text editor limited to email-compatible features (v0.9.0+)
|
|
24
24
|
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- Payload CMS v3.0.0 or higher
|
|
28
|
+
- A Media collection configured in your Payload project (required for image support in broadcasts)
|
|
29
|
+
|
|
25
30
|
## Quick Start
|
|
26
31
|
|
|
27
32
|
### 1. Install the plugin
|
package/dist/components.cjs
CHANGED
|
@@ -917,9 +917,17 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
917
917
|
"ol",
|
|
918
918
|
"li",
|
|
919
919
|
"blockquote",
|
|
920
|
-
"hr"
|
|
920
|
+
"hr",
|
|
921
|
+
"img",
|
|
922
|
+
"div",
|
|
923
|
+
"table",
|
|
924
|
+
"tr",
|
|
925
|
+
"td",
|
|
926
|
+
"th",
|
|
927
|
+
"tbody",
|
|
928
|
+
"thead"
|
|
921
929
|
],
|
|
922
|
-
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
|
|
930
|
+
ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
|
|
923
931
|
ALLOWED_STYLES: {
|
|
924
932
|
"*": [
|
|
925
933
|
"color",
|
|
@@ -950,58 +958,62 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
950
958
|
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
|
|
951
959
|
};
|
|
952
960
|
async function convertToEmailSafeHtml(editorState, options) {
|
|
953
|
-
const rawHtml = await lexicalToEmailHtml(editorState);
|
|
961
|
+
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
|
|
954
962
|
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
955
963
|
if (options?.wrapInTemplate) {
|
|
956
964
|
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
957
965
|
}
|
|
958
966
|
return sanitizedHtml;
|
|
959
967
|
}
|
|
960
|
-
async function lexicalToEmailHtml(editorState) {
|
|
968
|
+
async function lexicalToEmailHtml(editorState, mediaUrl) {
|
|
961
969
|
const { root } = editorState;
|
|
962
970
|
if (!root || !root.children) {
|
|
963
971
|
return "";
|
|
964
972
|
}
|
|
965
|
-
const html = root.children.map((node) => convertNode(node)).join("");
|
|
973
|
+
const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
|
|
966
974
|
return html;
|
|
967
975
|
}
|
|
968
|
-
function convertNode(node) {
|
|
976
|
+
function convertNode(node, mediaUrl) {
|
|
969
977
|
switch (node.type) {
|
|
970
978
|
case "paragraph":
|
|
971
|
-
return convertParagraph(node);
|
|
979
|
+
return convertParagraph(node, mediaUrl);
|
|
972
980
|
case "heading":
|
|
973
|
-
return convertHeading(node);
|
|
981
|
+
return convertHeading(node, mediaUrl);
|
|
974
982
|
case "list":
|
|
975
|
-
return convertList(node);
|
|
983
|
+
return convertList(node, mediaUrl);
|
|
976
984
|
case "listitem":
|
|
977
|
-
return convertListItem(node);
|
|
985
|
+
return convertListItem(node, mediaUrl);
|
|
978
986
|
case "blockquote":
|
|
979
|
-
return convertBlockquote(node);
|
|
987
|
+
return convertBlockquote(node, mediaUrl);
|
|
980
988
|
case "text":
|
|
981
989
|
return convertText(node);
|
|
982
990
|
case "link":
|
|
983
|
-
return convertLink(node);
|
|
991
|
+
return convertLink(node, mediaUrl);
|
|
984
992
|
case "linebreak":
|
|
985
993
|
return "<br>";
|
|
994
|
+
case "upload":
|
|
995
|
+
return convertUpload(node, mediaUrl);
|
|
996
|
+
case "block":
|
|
997
|
+
return convertBlock(node, mediaUrl);
|
|
986
998
|
default:
|
|
987
999
|
if (node.children) {
|
|
988
|
-
return node.children.map(convertNode).join("");
|
|
1000
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
989
1001
|
}
|
|
990
1002
|
return "";
|
|
991
1003
|
}
|
|
992
1004
|
}
|
|
993
|
-
function convertParagraph(node) {
|
|
1005
|
+
function convertParagraph(node, mediaUrl) {
|
|
994
1006
|
const align = getAlignment(node.format);
|
|
995
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1007
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
996
1008
|
if (!children.trim()) {
|
|
997
1009
|
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
998
1010
|
}
|
|
999
1011
|
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
1000
1012
|
}
|
|
1001
|
-
function convertHeading(node) {
|
|
1013
|
+
function convertHeading(node, mediaUrl) {
|
|
1002
1014
|
const tag = node.tag || "h1";
|
|
1003
1015
|
const align = getAlignment(node.format);
|
|
1004
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1016
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1005
1017
|
const styles = {
|
|
1006
1018
|
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
|
|
1007
1019
|
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
@@ -1010,18 +1022,18 @@ function convertHeading(node) {
|
|
|
1010
1022
|
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
|
|
1011
1023
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1012
1024
|
}
|
|
1013
|
-
function convertList(node) {
|
|
1025
|
+
function convertList(node, mediaUrl) {
|
|
1014
1026
|
const tag = node.listType === "number" ? "ol" : "ul";
|
|
1015
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1027
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1016
1028
|
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;";
|
|
1017
1029
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1018
1030
|
}
|
|
1019
|
-
function convertListItem(node) {
|
|
1020
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1031
|
+
function convertListItem(node, mediaUrl) {
|
|
1032
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1021
1033
|
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
|
|
1022
1034
|
}
|
|
1023
|
-
function convertBlockquote(node) {
|
|
1024
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1035
|
+
function convertBlockquote(node, mediaUrl) {
|
|
1036
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1025
1037
|
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
|
|
1026
1038
|
return `<blockquote style="${style}">${children}</blockquote>`;
|
|
1027
1039
|
}
|
|
@@ -1041,10 +1053,76 @@ function convertText(node) {
|
|
|
1041
1053
|
}
|
|
1042
1054
|
return text;
|
|
1043
1055
|
}
|
|
1044
|
-
function convertLink(node) {
|
|
1045
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1056
|
+
function convertLink(node, mediaUrl) {
|
|
1057
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1046
1058
|
const url = node.fields?.url || "#";
|
|
1047
|
-
|
|
1059
|
+
const newTab = node.fields?.newTab ?? false;
|
|
1060
|
+
const targetAttr = newTab ? ' target="_blank"' : "";
|
|
1061
|
+
const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
|
|
1062
|
+
return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
|
|
1063
|
+
}
|
|
1064
|
+
function convertUpload(node, mediaUrl) {
|
|
1065
|
+
const upload = node.value;
|
|
1066
|
+
if (!upload) return "";
|
|
1067
|
+
let src = "";
|
|
1068
|
+
if (typeof upload === "string") {
|
|
1069
|
+
src = upload;
|
|
1070
|
+
} else if (upload.url) {
|
|
1071
|
+
src = upload.url;
|
|
1072
|
+
} else if (upload.filename && mediaUrl) {
|
|
1073
|
+
src = `${mediaUrl}/${upload.filename}`;
|
|
1074
|
+
}
|
|
1075
|
+
const alt = node.fields?.altText || upload.alt || "";
|
|
1076
|
+
const caption = node.fields?.caption || "";
|
|
1077
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
1078
|
+
if (caption) {
|
|
1079
|
+
return `
|
|
1080
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1081
|
+
${imgHtml}
|
|
1082
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
1083
|
+
</div>
|
|
1084
|
+
`;
|
|
1085
|
+
}
|
|
1086
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
1087
|
+
}
|
|
1088
|
+
function convertBlock(node, mediaUrl) {
|
|
1089
|
+
const blockType = node.fields?.blockName;
|
|
1090
|
+
switch (blockType) {
|
|
1091
|
+
case "button":
|
|
1092
|
+
return convertButtonBlock(node.fields);
|
|
1093
|
+
case "divider":
|
|
1094
|
+
return convertDividerBlock(node.fields);
|
|
1095
|
+
default:
|
|
1096
|
+
if (node.children) {
|
|
1097
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
1098
|
+
}
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function convertButtonBlock(fields) {
|
|
1103
|
+
const text = fields?.text || "Click here";
|
|
1104
|
+
const url = fields?.url || "#";
|
|
1105
|
+
const style = fields?.style || "primary";
|
|
1106
|
+
const styles = {
|
|
1107
|
+
primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
|
|
1108
|
+
secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
|
|
1109
|
+
outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
|
|
1110
|
+
};
|
|
1111
|
+
const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
1112
|
+
return `
|
|
1113
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1114
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
1115
|
+
</div>
|
|
1116
|
+
`;
|
|
1117
|
+
}
|
|
1118
|
+
function convertDividerBlock(fields) {
|
|
1119
|
+
const style = fields?.style || "solid";
|
|
1120
|
+
const styles = {
|
|
1121
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
1122
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
1123
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
1124
|
+
};
|
|
1125
|
+
return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
1048
1126
|
}
|
|
1049
1127
|
function getAlignment(format) {
|
|
1050
1128
|
if (!format) return "left";
|