payload-plugin-newsletter 0.25.11 → 0.25.13
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 +21 -0
- package/dist/collections.cjs +69 -43
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +69 -43
- package/dist/collections.js.map +1 -1
- package/dist/server.d.ts +93 -2
- package/dist/server.js +95 -44
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Field, Block, RichTextField, Config, Endpoint, CollectionConfig, GlobalConfig } from 'payload';
|
|
1
|
+
import { Field, Block, RichTextField, Payload, Config, Endpoint, CollectionConfig, GlobalConfig } from 'payload';
|
|
2
|
+
import { SerializedEditorState } from 'lexical';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Core types for broadcast management functionality
|
|
@@ -632,6 +633,96 @@ interface SurveyQuestion {
|
|
|
632
633
|
required?: boolean;
|
|
633
634
|
}
|
|
634
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Recursively populates media fields in Lexical content.
|
|
638
|
+
* Resolves Media IDs to full media objects with URLs.
|
|
639
|
+
*
|
|
640
|
+
* @param content - Lexical editor state content
|
|
641
|
+
* @param payload - Payload instance for database queries
|
|
642
|
+
* @param config - Newsletter plugin configuration
|
|
643
|
+
* @returns Populated content with resolved media objects
|
|
644
|
+
*/
|
|
645
|
+
declare function populateMediaFields(content: unknown, payload: Payload, config: NewsletterPluginConfig): Promise<unknown>;
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Options for generating broadcast preview HTML
|
|
649
|
+
*/
|
|
650
|
+
interface PreviewOptions {
|
|
651
|
+
/** Email subject line */
|
|
652
|
+
subject: string;
|
|
653
|
+
/** Email preheader text (preview text in inbox) */
|
|
654
|
+
preheader?: string;
|
|
655
|
+
/** Whether to wrap content in email template (default: true) */
|
|
656
|
+
wrapInTemplate?: boolean;
|
|
657
|
+
/** Base URL for media files (optional, derived from payload.config.serverURL if not provided) */
|
|
658
|
+
mediaUrl?: string;
|
|
659
|
+
/** Additional document data to pass to custom wrapper */
|
|
660
|
+
documentData?: Record<string, unknown>;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Result from preview generation
|
|
664
|
+
*/
|
|
665
|
+
interface PreviewResult {
|
|
666
|
+
/** Generated HTML content */
|
|
667
|
+
html: string;
|
|
668
|
+
/** Subject line used */
|
|
669
|
+
subject: string;
|
|
670
|
+
/** Preheader text used (null if not provided) */
|
|
671
|
+
preheader: string | null;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Generates preview HTML from broadcast content.
|
|
675
|
+
*
|
|
676
|
+
* This utility handles the full preview generation pipeline:
|
|
677
|
+
* 1. Populates media fields (resolves Media IDs to full objects with URLs)
|
|
678
|
+
* 2. Converts Lexical content to email-safe HTML
|
|
679
|
+
* 3. Optionally wraps in email template
|
|
680
|
+
*
|
|
681
|
+
* @param content - Lexical editor state content from broadcast
|
|
682
|
+
* @param payload - Payload instance for database queries
|
|
683
|
+
* @param config - Newsletter plugin configuration
|
|
684
|
+
* @param options - Preview generation options
|
|
685
|
+
* @returns Preview result with HTML, subject, and preheader
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* ```typescript
|
|
689
|
+
* import { generateBroadcastPreviewHtml } from 'payload-plugin-newsletter'
|
|
690
|
+
*
|
|
691
|
+
* const preview = await generateBroadcastPreviewHtml(
|
|
692
|
+
* broadcast.content,
|
|
693
|
+
* payload,
|
|
694
|
+
* pluginConfig,
|
|
695
|
+
* {
|
|
696
|
+
* subject: broadcast.subject,
|
|
697
|
+
* preheader: broadcast.preheader,
|
|
698
|
+
* wrapInTemplate: true,
|
|
699
|
+
* }
|
|
700
|
+
* )
|
|
701
|
+
*
|
|
702
|
+
* // preview.html contains the rendered email HTML
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
declare function generateBroadcastPreviewHtml(content: SerializedEditorState | null | undefined, payload: Payload, config: NewsletterPluginConfig, options: PreviewOptions): Promise<PreviewResult>;
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Converts Lexical editor state to email-safe HTML
|
|
709
|
+
*/
|
|
710
|
+
declare function convertToEmailSafeHtml(editorState: SerializedEditorState | undefined | null, options?: {
|
|
711
|
+
wrapInTemplate?: boolean;
|
|
712
|
+
preheader?: string;
|
|
713
|
+
mediaUrl?: string;
|
|
714
|
+
customBlockConverter?: (node: any, mediaUrl?: string) => Promise<string>;
|
|
715
|
+
payload?: any;
|
|
716
|
+
populateFields?: string[] | ((blockType: string) => string[]);
|
|
717
|
+
customWrapper?: (content: string, options?: {
|
|
718
|
+
preheader?: string;
|
|
719
|
+
subject?: string;
|
|
720
|
+
documentData?: Record<string, any>;
|
|
721
|
+
}) => string | Promise<string>;
|
|
722
|
+
subject?: string;
|
|
723
|
+
documentData?: Record<string, any>;
|
|
724
|
+
}): Promise<string>;
|
|
725
|
+
|
|
635
726
|
declare module 'payload' {
|
|
636
727
|
interface BasePayload {
|
|
637
728
|
newsletterEmailService?: any;
|
|
@@ -710,4 +801,4 @@ declare class BroadcastProvider implements EmailProvider {
|
|
|
710
801
|
removeContact(email: string): Promise<void>;
|
|
711
802
|
}
|
|
712
803
|
|
|
713
|
-
export { BroadcastProvider, type NewsletterPluginConfig, ResendProvider, type Subscriber, createBroadcastsCollection, createNewsletterSettingsGlobal, createPreferencesEndpoint, createSubscribeEndpoint, createSubscribersCollection, createUnsubscribeEndpoint, createVerifyMagicLinkEndpoint, newsletterPlugin };
|
|
804
|
+
export { BroadcastProvider, type NewsletterPluginConfig, type PreviewOptions, type PreviewResult, ResendProvider, type Subscriber, convertToEmailSafeHtml, createBroadcastsCollection, createNewsletterSettingsGlobal, createPreferencesEndpoint, createSubscribeEndpoint, createSubscribersCollection, createUnsubscribeEndpoint, createVerifyMagicLinkEndpoint, generateBroadcastPreviewHtml, newsletterPlugin, populateMediaFields };
|
package/dist/server.js
CHANGED
|
@@ -4289,25 +4289,28 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4289
4289
|
};
|
|
4290
4290
|
};
|
|
4291
4291
|
|
|
4292
|
-
// src/
|
|
4292
|
+
// src/utils/mediaPopulation.ts
|
|
4293
4293
|
async function populateMediaFields(content, payload, config) {
|
|
4294
4294
|
if (!content || typeof content !== "object") return content;
|
|
4295
|
-
|
|
4296
|
-
|
|
4295
|
+
const typedContent = content;
|
|
4296
|
+
if (typedContent.root?.children) {
|
|
4297
|
+
for (const child of typedContent.root.children) {
|
|
4297
4298
|
await populateBlockMediaFields(child, payload, config);
|
|
4298
4299
|
}
|
|
4299
4300
|
}
|
|
4300
4301
|
return content;
|
|
4301
4302
|
}
|
|
4302
4303
|
async function populateBlockMediaFields(node, payload, config) {
|
|
4303
|
-
if (node
|
|
4304
|
-
|
|
4304
|
+
if (!node || typeof node !== "object") return;
|
|
4305
|
+
const typedNode = node;
|
|
4306
|
+
if (typedNode.type === "block" && typedNode.fields) {
|
|
4307
|
+
const blockType = typedNode.fields.blockType || typedNode.fields.blockName;
|
|
4305
4308
|
const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
|
|
4306
4309
|
const blockConfig = customBlocks.find((b) => b.slug === blockType);
|
|
4307
4310
|
if (blockConfig && blockConfig.fields) {
|
|
4308
4311
|
for (const field of blockConfig.fields) {
|
|
4309
|
-
if (field.type === "upload" && field.relationTo &&
|
|
4310
|
-
const fieldValue =
|
|
4312
|
+
if (field.type === "upload" && field.relationTo && typedNode.fields[field.name]) {
|
|
4313
|
+
const fieldValue = typedNode.fields[field.name];
|
|
4311
4314
|
const collectionName = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
|
|
4312
4315
|
if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
|
|
4313
4316
|
try {
|
|
@@ -4317,26 +4320,33 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4317
4320
|
depth: 0
|
|
4318
4321
|
});
|
|
4319
4322
|
if (media) {
|
|
4320
|
-
|
|
4321
|
-
payload.logger?.info(
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4323
|
+
typedNode.fields[field.name] = media;
|
|
4324
|
+
payload.logger?.info(
|
|
4325
|
+
{
|
|
4326
|
+
mediaId: fieldValue,
|
|
4327
|
+
mediaUrl: media.url,
|
|
4328
|
+
filename: media.filename
|
|
4329
|
+
},
|
|
4330
|
+
`Populated ${field.name} for block ${blockType}`
|
|
4331
|
+
);
|
|
4326
4332
|
}
|
|
4327
4333
|
} catch (error) {
|
|
4328
|
-
payload.logger?.error(
|
|
4334
|
+
payload.logger?.error(
|
|
4335
|
+
{ error: String(error) },
|
|
4336
|
+
`Failed to populate ${field.name} for block ${blockType}`
|
|
4337
|
+
);
|
|
4329
4338
|
}
|
|
4330
4339
|
}
|
|
4331
4340
|
}
|
|
4332
4341
|
if (field.type === "array" && field.fields) {
|
|
4333
|
-
const arrayValue =
|
|
4342
|
+
const arrayValue = typedNode.fields[field.name];
|
|
4334
4343
|
if (Array.isArray(arrayValue)) {
|
|
4335
4344
|
for (const arrayItem of arrayValue) {
|
|
4336
4345
|
if (arrayItem && typeof arrayItem === "object") {
|
|
4346
|
+
const typedArrayItem = arrayItem;
|
|
4337
4347
|
for (const arrayField of field.fields) {
|
|
4338
|
-
if (arrayField.type === "upload" && arrayField.relationTo &&
|
|
4339
|
-
const arrayFieldValue =
|
|
4348
|
+
if (arrayField.type === "upload" && arrayField.relationTo && typedArrayItem[arrayField.name]) {
|
|
4349
|
+
const arrayFieldValue = typedArrayItem[arrayField.name];
|
|
4340
4350
|
const arrayCollectionName = Array.isArray(arrayField.relationTo) ? arrayField.relationTo[0] : arrayField.relationTo;
|
|
4341
4351
|
if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
|
|
4342
4352
|
try {
|
|
@@ -4346,15 +4356,21 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4346
4356
|
depth: 0
|
|
4347
4357
|
});
|
|
4348
4358
|
if (media) {
|
|
4349
|
-
|
|
4350
|
-
payload.logger?.info(
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4359
|
+
typedArrayItem[arrayField.name] = media;
|
|
4360
|
+
payload.logger?.info(
|
|
4361
|
+
{
|
|
4362
|
+
mediaId: arrayFieldValue,
|
|
4363
|
+
mediaUrl: media.url,
|
|
4364
|
+
filename: media.filename
|
|
4365
|
+
},
|
|
4366
|
+
`Populated array ${arrayField.name} for block ${blockType}`
|
|
4367
|
+
);
|
|
4355
4368
|
}
|
|
4356
4369
|
} catch (error) {
|
|
4357
|
-
payload.logger?.error(
|
|
4370
|
+
payload.logger?.error(
|
|
4371
|
+
{ error: String(error) },
|
|
4372
|
+
`Failed to populate array ${arrayField.name} for block ${blockType}`
|
|
4373
|
+
);
|
|
4358
4374
|
}
|
|
4359
4375
|
}
|
|
4360
4376
|
}
|
|
@@ -4363,23 +4379,24 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4363
4379
|
}
|
|
4364
4380
|
}
|
|
4365
4381
|
}
|
|
4366
|
-
if (field.type === "richText" &&
|
|
4367
|
-
await populateRichTextUploads(
|
|
4382
|
+
if (field.type === "richText" && typedNode.fields[field.name]) {
|
|
4383
|
+
await populateRichTextUploads(typedNode.fields[field.name], payload);
|
|
4368
4384
|
payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
|
|
4369
4385
|
}
|
|
4370
4386
|
}
|
|
4371
4387
|
}
|
|
4372
4388
|
}
|
|
4373
|
-
if (
|
|
4374
|
-
for (const child of
|
|
4389
|
+
if (typedNode.children) {
|
|
4390
|
+
for (const child of typedNode.children) {
|
|
4375
4391
|
await populateBlockMediaFields(child, payload, config);
|
|
4376
4392
|
}
|
|
4377
4393
|
}
|
|
4378
4394
|
}
|
|
4379
4395
|
async function populateRichTextUploads(content, payload) {
|
|
4380
4396
|
if (!content || typeof content !== "object") return;
|
|
4381
|
-
|
|
4382
|
-
|
|
4397
|
+
const typedContent = content;
|
|
4398
|
+
if (typedContent.root?.children) {
|
|
4399
|
+
await processNodeArray(typedContent.root.children);
|
|
4383
4400
|
}
|
|
4384
4401
|
if (Array.isArray(content)) {
|
|
4385
4402
|
await processNodeArray(content);
|
|
@@ -4389,33 +4406,42 @@ async function populateRichTextUploads(content, payload) {
|
|
|
4389
4406
|
}
|
|
4390
4407
|
async function processNode(node) {
|
|
4391
4408
|
if (!node || typeof node !== "object") return;
|
|
4392
|
-
|
|
4409
|
+
const typedNode = node;
|
|
4410
|
+
if (typedNode.type === "upload" && typedNode.relationTo === "media" && typeof typedNode.value === "string" && typedNode.value.match(/^[a-f0-9]{24}$/i)) {
|
|
4393
4411
|
try {
|
|
4394
4412
|
const media = await payload.findByID({
|
|
4395
4413
|
collection: "media",
|
|
4396
|
-
id:
|
|
4414
|
+
id: typedNode.value,
|
|
4397
4415
|
depth: 0
|
|
4398
4416
|
});
|
|
4399
4417
|
if (media) {
|
|
4400
|
-
|
|
4401
|
-
payload.logger?.info(
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4418
|
+
typedNode.value = media;
|
|
4419
|
+
payload.logger?.info(
|
|
4420
|
+
{
|
|
4421
|
+
mediaId: typedNode.value,
|
|
4422
|
+
mediaUrl: media.url,
|
|
4423
|
+
filename: media.filename
|
|
4424
|
+
},
|
|
4425
|
+
"Populated rich text upload node"
|
|
4426
|
+
);
|
|
4406
4427
|
}
|
|
4407
4428
|
} catch (error) {
|
|
4408
|
-
payload.logger?.error(
|
|
4429
|
+
payload.logger?.error(
|
|
4430
|
+
{ error: String(error) },
|
|
4431
|
+
`Failed to populate rich text upload ${typedNode.value}`
|
|
4432
|
+
);
|
|
4409
4433
|
}
|
|
4410
4434
|
}
|
|
4411
|
-
if (
|
|
4412
|
-
await processNodeArray(
|
|
4435
|
+
if (typedNode.children && Array.isArray(typedNode.children)) {
|
|
4436
|
+
await processNodeArray(typedNode.children);
|
|
4413
4437
|
}
|
|
4414
|
-
if (
|
|
4415
|
-
await processNodeArray(
|
|
4438
|
+
if (typedNode.root?.children && Array.isArray(typedNode.root.children)) {
|
|
4439
|
+
await processNodeArray(typedNode.root.children);
|
|
4416
4440
|
}
|
|
4417
4441
|
}
|
|
4418
4442
|
}
|
|
4443
|
+
|
|
4444
|
+
// src/endpoints/broadcasts/preview.ts
|
|
4419
4445
|
var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
|
|
4420
4446
|
return {
|
|
4421
4447
|
path: "/preview",
|
|
@@ -5263,6 +5289,28 @@ var setPluginConfig = (config) => {
|
|
|
5263
5289
|
// src/utilities/session.ts
|
|
5264
5290
|
import jwt2 from "jsonwebtoken";
|
|
5265
5291
|
|
|
5292
|
+
// src/utils/preview.ts
|
|
5293
|
+
async function generateBroadcastPreviewHtml(content, payload, config, options) {
|
|
5294
|
+
const mediaUrl = options.mediaUrl ?? (payload.config.serverURL ? `${payload.config.serverURL}/api/media` : "/api/media");
|
|
5295
|
+
const emailPreviewConfig = config.customizations?.broadcasts?.emailPreview;
|
|
5296
|
+
payload.logger?.info("Populating media fields for preview generation...");
|
|
5297
|
+
const populatedContent = await populateMediaFields(content, payload, config);
|
|
5298
|
+
const html = await convertToEmailSafeHtml(populatedContent, {
|
|
5299
|
+
wrapInTemplate: options.wrapInTemplate ?? emailPreviewConfig?.wrapInTemplate ?? true,
|
|
5300
|
+
preheader: options.preheader,
|
|
5301
|
+
subject: options.subject,
|
|
5302
|
+
mediaUrl,
|
|
5303
|
+
documentData: options.documentData,
|
|
5304
|
+
customBlockConverter: config.customizations?.broadcasts?.customBlockConverter,
|
|
5305
|
+
customWrapper: emailPreviewConfig?.customWrapper
|
|
5306
|
+
});
|
|
5307
|
+
return {
|
|
5308
|
+
html,
|
|
5309
|
+
subject: options.subject,
|
|
5310
|
+
preheader: options.preheader ?? null
|
|
5311
|
+
};
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5266
5314
|
// src/index.ts
|
|
5267
5315
|
var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
|
|
5268
5316
|
const config = {
|
|
@@ -5417,6 +5465,7 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
|
|
|
5417
5465
|
export {
|
|
5418
5466
|
BroadcastProvider,
|
|
5419
5467
|
ResendProvider,
|
|
5468
|
+
convertToEmailSafeHtml,
|
|
5420
5469
|
createBroadcastsCollection,
|
|
5421
5470
|
createNewsletterSettingsGlobal,
|
|
5422
5471
|
createPreferencesEndpoint,
|
|
@@ -5424,5 +5473,7 @@ export {
|
|
|
5424
5473
|
createSubscribersCollection,
|
|
5425
5474
|
createUnsubscribeEndpoint,
|
|
5426
5475
|
createVerifyMagicLinkEndpoint,
|
|
5427
|
-
|
|
5476
|
+
generateBroadcastPreviewHtml,
|
|
5477
|
+
newsletterPlugin,
|
|
5478
|
+
populateMediaFields
|
|
5428
5479
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-plugin-newsletter",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.13",
|
|
4
4
|
"description": "Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|