payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
337 lines (334 loc) • 11.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/exports/utils.ts
var utils_exports = {};
__export(utils_exports, {
EMAIL_SAFE_CONFIG: () => EMAIL_SAFE_CONFIG,
convertToEmailSafeHtml: () => convertToEmailSafeHtml,
validateEmailHtml: () => validateEmailHtml
});
module.exports = __toCommonJS(utils_exports);
// src/utils/emailSafeHtml.ts
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
var EMAIL_SAFE_CONFIG = {
ALLOWED_TAGS: [
"p",
"br",
"strong",
"b",
"em",
"i",
"u",
"strike",
"s",
"span",
"a",
"h1",
"h2",
"h3",
"ul",
"ol",
"li",
"blockquote",
"hr"
],
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
ALLOWED_STYLES: {
"*": [
"color",
"background-color",
"font-size",
"font-weight",
"font-style",
"text-decoration",
"text-align",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"line-height",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color"
]
},
FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
};
async function convertToEmailSafeHtml(editorState, options) {
const rawHtml = await lexicalToEmailHtml(editorState);
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
if (options?.wrapInTemplate) {
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
}
return sanitizedHtml;
}
async function lexicalToEmailHtml(editorState) {
const { root } = editorState;
if (!root || !root.children) {
return "";
}
const html = root.children.map((node) => convertNode(node)).join("");
return html;
}
function convertNode(node) {
switch (node.type) {
case "paragraph":
return convertParagraph(node);
case "heading":
return convertHeading(node);
case "list":
return convertList(node);
case "listitem":
return convertListItem(node);
case "blockquote":
return convertBlockquote(node);
case "text":
return convertText(node);
case "link":
return convertLink(node);
case "linebreak":
return "<br>";
default:
if (node.children) {
return node.children.map(convertNode).join("");
}
return "";
}
}
function convertParagraph(node) {
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
if (!children.trim()) {
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
}
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
}
function convertHeading(node) {
const tag = node.tag || "h1";
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
const styles = {
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
};
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertList(node) {
const tag = node.listType === "number" ? "ol" : "ul";
const children = node.children?.map(convertNode).join("") || "";
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;";
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertListItem(node) {
const children = node.children?.map(convertNode).join("") || "";
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
}
function convertBlockquote(node) {
const children = node.children?.map(convertNode).join("") || "";
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
return `<blockquote style="${style}">${children}</blockquote>`;
}
function convertText(node) {
let text = escapeHtml(node.text || "");
if (node.format & 1) {
text = `<strong>${text}</strong>`;
}
if (node.format & 2) {
text = `<em>${text}</em>`;
}
if (node.format & 8) {
text = `<u>${text}</u>`;
}
if (node.format & 4) {
text = `<strike>${text}</strike>`;
}
return text;
}
function convertLink(node) {
const children = node.children?.map(convertNode).join("") || "";
const url = node.fields?.url || "#";
return `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${children}</a>`;
}
function getAlignment(format) {
if (!format) return "left";
if (format & 2) return "center";
if (format & 3) return "right";
if (format & 4) return "justify";
return "left";
}
function escapeHtml(text) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function wrapInEmailTemplate(content, preheader) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
</head>
<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;">
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
<tr>
<td align="center" style="padding: 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
<tr>
<td style="padding: 40px 30px;">
${content}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
// src/utils/validateEmailHtml.ts
function validateEmailHtml(html) {
const warnings = [];
const errors = [];
const sizeInBytes = new Blob([html]).size;
if (sizeInBytes > 102400) {
warnings.push(`Email size (${Math.round(sizeInBytes / 1024)}KB) exceeds Gmail's 102KB limit - email may be clipped`);
}
if (html.includes("position:") && (html.includes("position: absolute") || html.includes("position: fixed"))) {
errors.push("Absolute/fixed positioning is not supported in most email clients");
}
if (html.includes("display: flex") || html.includes("display: grid")) {
errors.push("Flexbox and Grid layouts are not supported in many email clients");
}
if (html.includes("@media")) {
warnings.push("Media queries may not work in all email clients");
}
const hasJavaScript = html.includes("<script") || html.includes("onclick") || html.includes("onload") || html.includes("javascript:");
if (hasJavaScript) {
errors.push("JavaScript is not supported in email and will be stripped by email clients");
}
const hasExternalStyles = html.includes("<link") && html.includes("stylesheet");
if (hasExternalStyles) {
errors.push("External stylesheets are not supported - use inline styles only");
}
if (html.includes("<form") || html.includes("<input") || html.includes("<button")) {
errors.push("Forms and form elements are not reliably supported in email");
}
const unsupportedTags = [
"video",
"audio",
"iframe",
"embed",
"object",
"canvas",
"svg"
];
for (const tag of unsupportedTags) {
if (html.includes(`<${tag}`)) {
errors.push(`<${tag}> tags are not supported in email`);
}
}
const imageCount = (html.match(/<img/g) || []).length;
const linkCount = (html.match(/<a/g) || []).length;
if (imageCount > 20) {
warnings.push(`High number of images (${imageCount}) may affect email performance`);
}
const imagesWithoutAlt = (html.match(/<img(?![^>]*\balt\s*=)[^>]*>/g) || []).length;
if (imagesWithoutAlt > 0) {
warnings.push(`${imagesWithoutAlt} image(s) missing alt text - important for accessibility`);
}
const linksWithoutTarget = (html.match(/<a(?![^>]*\btarget\s*=)[^>]*>/g) || []).length;
if (linksWithoutTarget > 0) {
warnings.push(`${linksWithoutTarget} link(s) missing target="_blank" attribute`);
}
if (html.includes("margin: auto") || html.includes("margin:auto")) {
warnings.push('margin: auto is not supported in Outlook - use align="center" or tables for centering');
}
if (html.includes("background-image")) {
warnings.push("Background images are not reliably supported - consider using <img> tags instead");
}
if (html.match(/\d+\s*(rem|em)/)) {
warnings.push("rem/em units may render inconsistently - use px for reliable sizing");
}
if (html.match(/margin[^:]*:\s*-\d+/)) {
errors.push("Negative margins are not supported in many email clients");
}
const personalizationTags = html.match(/\{\{([^}]+)\}\}/g) || [];
const validTags = ["subscriber.name", "subscriber.email", "subscriber.firstName", "subscriber.lastName"];
for (const tag of personalizationTags) {
const tagContent = tag.replace(/[{}]/g, "").trim();
if (!validTags.includes(tagContent)) {
warnings.push(`Unknown personalization tag: ${tag}`);
}
}
return {
valid: errors.length === 0,
warnings,
errors,
stats: {
sizeInBytes,
imageCount,
linkCount,
hasExternalStyles,
hasJavaScript
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
EMAIL_SAFE_CONFIG,
convertToEmailSafeHtml,
validateEmailHtml
});
//# sourceMappingURL=utils.cjs.map