diff --git a/ghost/core/core/server/services/member-welcome-emails/email-templates/wrapper.hbs b/ghost/core/core/server/services/member-welcome-emails/email-templates/wrapper.hbs
index d9b86760f66..18de7061f65 100644
--- a/ghost/core/core/server/services/member-welcome-emails/email-templates/wrapper.hbs
+++ b/ghost/core/core/server/services/member-welcome-emails/email-templates/wrapper.hbs
@@ -2,8 +2,8 @@
-
-
+
+
{{{content}}}
@@ -13,11 +13,21 @@
+ {{#if footerContent }}
+
+
+
+ {{/if}}
+ {{#if showBadge }}
+
+
+
+ {{/if}}
diff --git a/ghost/core/core/server/services/member-welcome-emails/member-welcome-email-renderer.js b/ghost/core/core/server/services/member-welcome-emails/member-welcome-email-renderer.js
index 34251d03364..7cf8bdff9c1 100644
--- a/ghost/core/core/server/services/member-welcome-emails/member-welcome-email-renderer.js
+++ b/ghost/core/core/server/services/member-welcome-emails/member-welcome-email-renderer.js
@@ -1,25 +1,50 @@
const fs = require('fs');
const path = require('path');
const lexicalLib = require('../../lib/lexical');
+const labs = require('../../../shared/labs');
const {finalize} = require('../email-rendering/finalize');
const errors = require('@tryghost/errors');
const {MESSAGES} = require('./constants');
const {wrapReplacementStrings} = require('../koenig/render-utils/replacement-strings');
const linkReplacer = require('../lib/link-replacer');
const {getEmailDesign} = require('../email-rendering/email-design');
+const {registerHelpers} = require('../email-service/helpers/register-helpers');
const REPLACEMENT_REGEX = /%%\{(\w+?)(?:,? *"(.*?)")?\}%%/g;
const UNMATCHED_TOKEN_REGEX = /%%\{.*?\}%%/g;
+const DEFAULT_DESIGN_SETTINGS = {
+ background_color: '#ffffff',
+ button_color: 'accent',
+ button_corners: null,
+ button_style: null,
+ divider_color: null,
+ footer_content: null,
+ header_background_color: null,
+ header_image: null,
+ image_corners: null,
+ link_color: 'accent',
+ link_style: null,
+ section_title_color: null,
+ show_badge: true,
+ show_header_title: true,
+ title_font_weight: 'bold'
+};
class MemberWelcomeEmailRenderer {
#wrapperTemplate;
constructor({t}) {
this.Handlebars = require('handlebars').create();
- this.Handlebars.registerHelper('t', function (key, options) {
- let hash = options?.hash;
- return t(key, hash || options || {});
- });
+ const useDesignCustomization = labs.isSet('welcomeEmailsDesignCustomization');
+
+ if (useDesignCustomization) {
+ registerHelpers(this.Handlebars, labs, t);
+ } else {
+ this.Handlebars.registerHelper('t', function (key, options) {
+ let hash = options?.hash;
+ return t(key, hash || options || {});
+ });
+ }
const baseStylesSource = fs.readFileSync(
path.join(__dirname, '../email-rendering/partials/base-styles.hbs'),
'utf8'
@@ -35,9 +60,17 @@ class MemberWelcomeEmailRenderer {
this.Handlebars.registerPartial('baseStyles', baseStylesSource);
this.Handlebars.registerPartial('contentStyles', contentStylesSource);
this.Handlebars.registerPartial('cardStyles', cardStylesSource);
- this.Handlebars.registerPartial('styles',
- ''
- );
+ if (useDesignCustomization) {
+ const emailStylesSource = fs.readFileSync(
+ path.join(__dirname, '../email-service/email-templates/partials/styles.hbs'),
+ 'utf8'
+ );
+ this.Handlebars.registerPartial('styles', emailStylesSource);
+ } else {
+ this.Handlebars.registerPartial('styles',
+ ''
+ );
+ }
const emailWrapperSource = fs.readFileSync(
path.join(__dirname, '../email-rendering/partials/email-wrapper.hbs'),
'utf8'
@@ -108,25 +141,33 @@ class MemberWelcomeEmailRenderer {
* @param {Object} options
* @param {string} options.lexical - Lexical JSON string to render
* @param {string} options.subject - Email subject (may contain template variables)
+ * @param {Object} [options.designSettings] - Email design settings loaded from the database
* @param {Object} options.member - Member data (name, email)
* @param {Object} options.siteSettings - Site settings (title, url, accentColor)
* @returns {Promise<{html: string, text: string, subject: string}>}
*/
- async render({lexical, subject, member, siteSettings}) {
+ async render({lexical, subject, designSettings = {}, member, siteSettings}) {
+ const useDesignCustomization = labs.isSet('welcomeEmailsDesignCustomization');
+
+ designSettings = useDesignCustomization ? {
+ ...DEFAULT_DESIGN_SETTINGS,
+ ...designSettings
+ } : DEFAULT_DESIGN_SETTINGS;
+
const design = getEmailDesign({
accentColor: siteSettings.accentColor,
- backgroundColor: '#ffffff',
- buttonColor: 'accent',
- buttonCorners: null,
- buttonStyle: null,
- dividerColor: null,
- headerBackgroundColor: null,
- imageCorners: null,
- linkColor: 'accent',
- linkStyle: null,
+ backgroundColor: designSettings.background_color,
+ buttonColor: designSettings.button_color,
+ buttonCorners: designSettings.button_corners,
+ buttonStyle: designSettings.button_style,
+ dividerColor: designSettings.divider_color,
+ headerBackgroundColor: designSettings.header_background_color,
+ imageCorners: designSettings.image_corners,
+ linkColor: designSettings.link_color,
+ linkStyle: designSettings.link_style,
postTitleColor: null,
- sectionTitleColor: null,
- titleFontWeight: 'bold'
+ sectionTitleColor: designSettings.section_title_color,
+ titleFontWeight: designSettings.title_font_weight
});
let content;
@@ -158,15 +199,39 @@ class MemberWelcomeEmailRenderer {
const managePreferencesUrl = new URL('#/portal/account/newsletters', siteSettings.url).href;
const year = new Date().getFullYear();
+ const headerImage = useDesignCustomization ? (designSettings.header_image || null) : null;
+ const showHeaderTitle = useDesignCustomization ? designSettings.show_header_title !== false : false;
+ const showBadge = useDesignCustomization ? designSettings.show_badge !== false : false;
const html = this.#wrapperTemplate({
content: contentWithAbsoluteLinks,
emailTitle: subjectWithReplacements,
subject: subjectWithReplacements,
+ footerContent: useDesignCustomization ? designSettings.footer_content : null,
+ hasHeaderContent: Boolean(headerImage || showHeaderTitle),
+ headerImage,
+ showBadge,
+ showHeaderIcon: false,
+ showHeaderName: false,
+ showHeaderTitle,
+ site: {
+ title: siteSettings.title,
+ url: siteSettings.url
+ },
siteTitle: siteSettings.title,
siteUrl: siteSettings.url,
managePreferencesUrl,
year,
+ ctaBgColors: [
+ 'grey',
+ 'blue',
+ 'green',
+ 'yellow',
+ 'red',
+ 'pink',
+ 'purple',
+ 'white'
+ ],
...design,
classes: {
container: 'container'
diff --git a/ghost/core/core/server/services/member-welcome-emails/service.js b/ghost/core/core/server/services/member-welcome-emails/service.js
index 85a78d3ca91..2c0db536095 100644
--- a/ghost/core/core/server/services/member-welcome-emails/service.js
+++ b/ghost/core/core/server/services/member-welcome-emails/service.js
@@ -1,5 +1,6 @@
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
+const labs = require('../../../shared/labs');
const urlUtils = require('../../../shared/url-utils');
const settingsCache = require('../../../shared/settings-cache');
const emailAddressService = require('../email-address');
@@ -93,21 +94,30 @@ class MemberWelcomeEmailService {
return this.#defaultNewsletterSenderOptions;
}
+ #useDesignCustomization() {
+ return labs.isSet('welcomeEmailsDesignCustomization');
+ }
+
async loadMemberWelcomeEmails() {
this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions();
for (const [memberStatus, slug] of Object.entries(MEMBER_WELCOME_EMAIL_SLUGS)) {
- const row = await AutomatedEmail.findOne({slug});
+ const row = this.#useDesignCustomization()
+ ? await AutomatedEmail.findOne({slug}, {withRelated: ['emailDesignSetting']})
+ : await AutomatedEmail.findOne({slug});
if (!row || !row.get('lexical')) {
this.#memberWelcomeEmails[memberStatus] = null;
continue;
}
+ const designSettings = this.#useDesignCustomization() ? row.related('emailDesignSetting') : null;
+
this.#memberWelcomeEmails[memberStatus] = {
lexical: row.get('lexical'),
subject: row.get('subject'),
status: row.get('status'),
+ designSettings: designSettings?.id ? designSettings.toJSON() : null,
senderName: row.get('sender_name'),
senderEmail: row.get('sender_email'),
senderReplyTo: row.get('sender_reply_to')
@@ -147,6 +157,7 @@ class MemberWelcomeEmailService {
const {html, text, subject} = await this.#renderer.render({
lexical: memberWelcomeEmail.lexical,
subject: memberWelcomeEmail.subject,
+ designSettings: memberWelcomeEmail.designSettings,
member: {
name: member.name,
email: member.email,
@@ -181,7 +192,9 @@ class MemberWelcomeEmailService {
async sendTestEmail({email, subject, lexical, automatedEmailId}) {
// Still validate the automated email exists (for permission purposes)
- const automatedEmail = await AutomatedEmail.findOne({id: automatedEmailId});
+ const automatedEmail = this.#useDesignCustomization()
+ ? await AutomatedEmail.findOne({id: automatedEmailId}, {withRelated: ['emailDesignSetting']})
+ : await AutomatedEmail.findOne({id: automatedEmailId});
if (!automatedEmail) {
throw new errors.NotFoundError({
@@ -207,9 +220,12 @@ class MemberWelcomeEmailService {
uuid: '00000000-0000-4000-8000-000000000000'
};
+ const designSettings = this.#useDesignCustomization() ? automatedEmail.related('emailDesignSetting') : null;
+
const {html, text, subject: renderedSubject} = await this.#renderer.render({
lexical,
subject,
+ designSettings: designSettings?.id ? designSettings.toJSON() : null,
member: testMember,
siteSettings: this.#getSiteSettings()
});
@@ -230,20 +246,25 @@ class MemberWelcomeEmailService {
class MemberWelcomeEmailServiceWrapper {
init() {
- if (this.api) {
+ const useDesignCustomization = labs.isSet('welcomeEmailsDesignCustomization');
+
+ if (this.api && this.useDesignCustomization === useDesignCustomization) {
return;
}
- const i18nLib = require('@tryghost/i18n');
- const events = require('../../lib/common/events');
+ if (!this.i18n) {
+ const i18nLib = require('@tryghost/i18n');
+ const events = require('../../lib/common/events');
- const i18n = i18nLib(settingsCache.get('locale') || 'en', 'ghost');
+ this.i18n = i18nLib(settingsCache.get('locale') || 'en', 'ghost');
- events.on('settings.locale.edited', (model) => {
- i18n.changeLanguage(model.get('value'));
- });
+ events.on('settings.locale.edited', (model) => {
+ this.i18n.changeLanguage(model.get('value'));
+ });
+ }
- this.api = new MemberWelcomeEmailService({t: i18n.t});
+ this.useDesignCustomization = useDesignCustomization;
+ this.api = new MemberWelcomeEmailService({t: this.i18n.t});
}
}
diff --git a/ghost/core/test/integration/services/__snapshots__/member-welcome-emails-snapshot.test.js.snap b/ghost/core/test/integration/services/__snapshots__/member-welcome-emails-snapshot.test.js.snap
index 6b06864395f..d11f0afee04 100644
--- a/ghost/core/test/integration/services/__snapshots__/member-welcome-emails-snapshot.test.js.snap
+++ b/ghost/core/test/integration/services/__snapshots__/member-welcome-emails-snapshot.test.js.snap
@@ -1,5 +1,3082 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Member Welcome Email Renderer Snapshots labs flag on renders a simple paragraph welcome email 1 1`] = `
+Object {
+ "html": "
+
+
+
+
+
+ Welcome to Test Site
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Welcome to our site!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+ "plaintext": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site [http://example.com]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Welcome to our site!
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site © 2020 — Manage your preferences [http://example.com/#/portal/account/newsletters]
+
+
+
+https://ghost.org/?via=pbg-newsletter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+}
+`;
+
+exports[`Member Welcome Email Renderer Snapshots labs flag on renders fallback values when member has no name 1 1`] = `
+Object {
+ "html": "
+
+
+
+
+
+ Welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello friend, welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+ "plaintext": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site [http://example.com]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Hello friend, welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site © 2020 — Manage your preferences [http://example.com/#/portal/account/newsletters]
+
+
+
+https://ghost.org/?via=pbg-newsletter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+}
+`;
+
+exports[`Member Welcome Email Renderer Snapshots labs flag on renders template variable replacements 1 1`] = `
+Object {
+ "html": "
+
+
+
+
+
+ Welcome to Test Site, Jamie Larson
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello Jamie, welcome to Test Site! Your email is jamie@example.com.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+ "plaintext": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site [http://example.com]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Hello Jamie, welcome to Test Site! Your email is jamie@example.com.
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site © 2020 — Manage your preferences [http://example.com/#/portal/account/newsletters]
+
+
+
+https://ghost.org/?via=pbg-newsletter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+ "subject": "Welcome to Test Site, Jamie Larson",
+}
+`;
+
+exports[`Member Welcome Email Renderer Snapshots labs flag on renders with a custom accent color 1 1`] = `
+Object {
+ "html": "
+
+
+
+
+
+ Welcome
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+ "plaintext": "
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site [http://example.com]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Welcome!
+
+
+
+
+
+
+
+
+
+
+
+
+Test Site © 2020 — Manage your preferences [http://example.com/#/portal/account/newsletters]
+
+
+
+https://ghost.org/?via=pbg-newsletter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+",
+}
+`;
+
exports[`Member Welcome Email Renderer Snapshots renders a simple paragraph welcome email 1 1`] = `
Object {
"html": "
diff --git a/ghost/core/test/integration/services/member-welcome-emails-snapshot.test.js b/ghost/core/test/integration/services/member-welcome-emails-snapshot.test.js
index 0369d7ad1b1..d2c094b338e 100644
--- a/ghost/core/test/integration/services/member-welcome-emails-snapshot.test.js
+++ b/ghost/core/test/integration/services/member-welcome-emails-snapshot.test.js
@@ -132,4 +132,88 @@ describe('Member Welcome Email Renderer Snapshots', function () {
plaintext: result.text
});
});
+
+ describe('labs flag on', function () {
+ beforeEach(function () {
+ labs.isSet.restore();
+ sinon.stub(labs, 'isSet').callsFake((flag) => {
+ if (flag === 'welcomeEmailsDesignCustomization') {
+ return true;
+ }
+
+ return originalLabsIsSet(flag);
+ });
+
+ const i18n = i18nLib('en', 'ghost');
+ renderer = new MemberWelcomeEmailRenderer({t: i18n.t});
+ });
+
+ it('renders a simple paragraph welcome email', async function () {
+ const result = await renderer.render({
+ lexical: makeLexical([makeParagraph('Welcome to our site!')]),
+ subject: 'Welcome to Test Site',
+ member: defaultMember,
+ siteSettings: defaultSiteSettings
+ });
+
+ assertMatchSnapshot({
+ html: result.html,
+ plaintext: result.text
+ });
+ });
+
+ it('renders template variable replacements', async function () {
+ const result = await renderer.render({
+ lexical: makeLexical([
+ makeParagraph('Hello {first_name}, welcome to {site_title}! Your email is {email}.')
+ ]),
+ subject: 'Welcome to {site_title}, {name}',
+ member: defaultMember,
+ siteSettings: defaultSiteSettings
+ });
+
+ assertMatchSnapshot({
+ html: result.html,
+ plaintext: result.text,
+ subject: result.subject
+ });
+ });
+
+ it('renders fallback values when member has no name', async function () {
+ const result = await renderer.render({
+ lexical: makeLexical([
+ makeParagraph('Hello {first_name, "friend"}, welcome!')
+ ]),
+ subject: 'Welcome!',
+ member: {
+ name: '',
+ email: 'anonymous@example.com',
+ uuid: '00000000-0000-4000-8000-000000000001'
+ },
+ siteSettings: defaultSiteSettings
+ });
+
+ assertMatchSnapshot({
+ html: result.html,
+ plaintext: result.text
+ });
+ });
+
+ it('renders with a custom accent color', async function () {
+ const result = await renderer.render({
+ lexical: makeLexical([makeParagraph('Welcome!')]),
+ subject: 'Welcome',
+ member: defaultMember,
+ siteSettings: {
+ ...defaultSiteSettings,
+ accentColor: '#FF0000'
+ }
+ });
+
+ assertMatchSnapshot({
+ html: result.html,
+ plaintext: result.text
+ });
+ });
+ });
});
diff --git a/ghost/core/test/integration/services/member-welcome-emails.test.js b/ghost/core/test/integration/services/member-welcome-emails.test.js
index a86dbb1a045..4c74323b9da 100644
--- a/ghost/core/test/integration/services/member-welcome-emails.test.js
+++ b/ghost/core/test/integration/services/member-welcome-emails.test.js
@@ -7,6 +7,7 @@ const {OUTBOX_STATUSES} = require('../../../core/server/models/outbox');
const db = require('../../../core/server/data/db');
const mailService = require('../../../core/server/services/mail');
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../../../core/server/services/member-welcome-emails/constants');
+const memberWelcomeEmailService = require('../../../core/server/services/member-welcome-emails/service');
const processOutbox = require('../../../core/server/services/outbox/jobs/lib/process-outbox');
const labs = require('../../../core/shared/labs');
@@ -410,9 +411,6 @@ describe('Member Welcome Emails Integration', function () {
});
it('uses mock member UUID when sending test welcome emails', async function () {
- const memberWelcomeEmailService = require('../../../core/server/services/member-welcome-emails/service');
- memberWelcomeEmailService.init();
-
const automatedEmail = await db.knex('automated_emails')
.where('slug', MEMBER_WELCOME_EMAIL_SLUGS.free)
.first();
@@ -431,6 +429,9 @@ describe('Member Welcome Emails Integration', function () {
}
});
+ memberWelcomeEmailService.api = null;
+ memberWelcomeEmailService.init();
+
await memberWelcomeEmailService.api.sendTestEmail({
email: 'test-member@example.com',
subject: 'Welcome test',
@@ -445,4 +446,74 @@ describe('Member Welcome Emails Integration', function () {
assert(!sendCall.args[0].html.includes('%7Buuid%7D'));
});
});
+
+ describe('labs flag on', function () {
+ beforeEach(function () {
+ labs.isSet.restore();
+ sinon.stub(labs, 'isSet').callsFake((flag) => {
+ if (flag === 'welcomeEmailsDesignCustomization') {
+ return true;
+ }
+
+ return originalLabsIsSet(flag);
+ });
+ memberWelcomeEmailService.api = null;
+ memberWelcomeEmailService.init();
+ sinon.stub(mailService.GhostMailer.prototype, 'send').resolves('Mail sent');
+ });
+
+ it('reinitializes the service when the labs mode changes', function () {
+ labs.isSet.restore();
+ sinon.stub(labs, 'isSet').callsFake((flag) => {
+ if (flag === 'welcomeEmailsDesignCustomization') {
+ return false;
+ }
+
+ return originalLabsIsSet(flag);
+ });
+
+ memberWelcomeEmailService.api = null;
+ memberWelcomeEmailService.useDesignCustomization = undefined;
+ memberWelcomeEmailService.init();
+ const labsOffApi = memberWelcomeEmailService.api;
+
+ labs.isSet.restore();
+ sinon.stub(labs, 'isSet').callsFake((flag) => {
+ if (flag === 'welcomeEmailsDesignCustomization') {
+ return true;
+ }
+
+ return originalLabsIsSet(flag);
+ });
+
+ memberWelcomeEmailService.init();
+
+ assert.notEqual(memberWelcomeEmailService.api, labsOffApi);
+ });
+
+ it('uses cached design settings after welcome emails are loaded', async function () {
+ await memberWelcomeEmailService.api.loadMemberWelcomeEmails();
+
+ await db.knex('email_design_settings')
+ .where('id', defaultEmailDesignSettingId)
+ .update({
+ footer_content: 'Fresh footer content
',
+ show_badge: false
+ });
+
+ await memberWelcomeEmailService.api.send({
+ member: {
+ email: 'fresh-design@example.com',
+ name: 'Fresh Design',
+ uuid: '77777777-7777-4777-8777-777777777777'
+ },
+ memberStatus: 'free'
+ });
+
+ sinon.assert.calledOnce(mailService.GhostMailer.prototype.send);
+ const sendCall = mailService.GhostMailer.prototype.send.firstCall;
+ assert.equal(sendCall.args[0].html.includes('Fresh footer content'), false);
+ assert.equal(sendCall.args[0].html.includes('https://ghost.org/?via=pbg-newsletter'), true);
+ });
+ });
});
diff --git a/ghost/core/test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js b/ghost/core/test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
index 054a438b5d8..b6208bdf548 100644
--- a/ghost/core/test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
+++ b/ghost/core/test/unit/server/services/member-welcome-emails/member-welcome-email-renderer.test.js
@@ -77,6 +77,46 @@ describe('MemberWelcomeEmailRenderer', function () {
}});
});
+ it('falls back to the legacy welcome email design when the labs flag is off', async function () {
+ const getEmailDesignStub = sinon.stub().returns({accentColor: '#123456'});
+ MemberWelcomeEmailRenderer.__set__('getEmailDesign', getEmailDesignStub);
+
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Welcome!',
+ designSettings: {
+ background_color: '#111111',
+ header_image: 'https://example.com/header.png',
+ show_badge: true,
+ show_header_title: true
+ },
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
+
+ sinon.assert.calledOnceWithExactly(getEmailDesignStub, {
+ accentColor: '#ff0000',
+ backgroundColor: '#ffffff',
+ buttonColor: 'accent',
+ buttonCorners: null,
+ buttonStyle: null,
+ dividerColor: null,
+ headerBackgroundColor: null,
+ imageCorners: null,
+ linkColor: 'accent',
+ linkStyle: null,
+ postTitleColor: null,
+ sectionTitleColor: null,
+ titleFontWeight: 'bold'
+ });
+
+ assert(!result.html.includes('https://example.com/header.png'));
+ assert(!result.html.includes('https://ghost.org/?via=pbg-newsletter'));
+ assert(!result.html.includes('class="header"'));
+ });
+
it('substitutes member template variables', async function () {
lexicalRenderStub.resolves('Hello {name}, or {first_name}! Contact: {email}
');
const renderer = new MemberWelcomeEmailRenderer({t: key => key});
@@ -457,338 +497,471 @@ describe('MemberWelcomeEmailRenderer', function () {
assert(!result.html.includes('Manage your preferences'));
});
- it('applies shared Koenig card styles used by newsletters', async function () {
- lexicalRenderStub.resolves(`
-
-
- `);
- const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+ describe('labs flag on', function () {
+ beforeEach(function () {
+ labs.isSet.restore();
+ sinon.stub(labs, 'isSet').callsFake((flag) => {
+ if (flag === 'welcomeEmailsDesignCustomization') {
+ return true;
+ }
+
+ return originalLabsIsSet(flag);
+ });
+ });
+
+ it('builds the email design from database-backed design settings', async function () {
+ const getEmailDesignStub = sinon.stub().returns({accentColor: '#123456'});
+ MemberWelcomeEmailRenderer.__set__('getEmailDesign', getEmailDesignStub);
+
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+ const designSettings = {
+ background_color: '#111111',
+ button_color: '#222222',
+ button_corners: 'pill',
+ button_style: 'outline',
+ divider_color: '#333333',
+ header_background_color: '#444444',
+ image_corners: 'rounded',
+ link_color: '#555555',
+ link_style: 'bold',
+ section_title_color: '#666666',
+ title_font_weight: 'medium'
+ };
+
+ await renderer.render({
+ lexical: '{}',
+ subject: 'Welcome!',
+ designSettings,
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
+
+ sinon.assert.calledOnceWithExactly(getEmailDesignStub, {
+ accentColor: '#ff0000',
+ backgroundColor: '#111111',
+ buttonColor: '#222222',
+ buttonCorners: 'pill',
+ buttonStyle: 'outline',
+ dividerColor: '#333333',
+ headerBackgroundColor: '#444444',
+ imageCorners: 'rounded',
+ linkColor: '#555555',
+ linkStyle: 'bold',
+ postTitleColor: null,
+ sectionTitleColor: '#666666',
+ titleFontWeight: 'medium'
+ });
+
+ sinon.assert.calledWith(lexicalRenderStub, '{}', {target: 'email', design: {accentColor: '#123456'}});
+ });
+
+ it('passes header and footer settings through to the wrapper template', async function () {
+ lexicalRenderStub.resolves('Content
');
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Test Subject',
+ designSettings: {
+ footer_content: 'Custom footer
',
+ header_image: 'https://example.com/header.png',
+ show_badge: true,
+ show_header_title: false
+ },
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
- const result = await renderer.render({
- lexical: '{}',
- subject: 'Welcome!',
- member: {name: 'John', email: 'john@example.com'},
- siteSettings: defaultSiteSettings
+ assert(result.html.includes('src="https://example.com/header.png"'));
+ assert(result.html.includes('Custom footer'));
+ assert(result.html.includes('https://ghost.org/?via=pbg-newsletter'));
});
- assert(result.html.includes('kg-callout-card'));
- assert(result.html.includes('padding: 24px'));
- assert(result.html.includes('table class="btn"'));
- assert(result.html.includes('background-color: #ff0000'));
- });
+ it('uses the newsletter content-shell class when design customization is enabled', async function () {
+ lexicalRenderStub.resolves('Content
');
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
- it('applies transistor card styles in welcome emails', async function () {
- lexicalRenderStub.resolves(`
-
- `);
- const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Test Subject',
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
- const result = await renderer.render({
- lexical: '{}',
- subject: 'Welcome!',
- member: {name: 'John', email: 'john@example.com', uuid: 'abc-123-def'},
- siteSettings: defaultSiteSettings
+ assert.match(result.html, //);
+ assert(result.html.includes('class="post-content"'));
});
- assert.match(result.html, /class="kg-card kg-transistor-card"[^>]*style="[^"]*border-radius: 10px/);
- assert.match(result.html, /class="kg-card kg-transistor-card"[^>]*style="[^"]*border: 1px solid rgba\(0, 0, 0, 0.12\)/);
- assert.match(result.html, /class="kg-transistor-title"[^>]*style="[^"]*display: block/);
- assert.match(result.html, /class="kg-transistor-title"[^>]*style="[^"]*text-decoration: none/);
- assert.match(result.html, /class="kg-transistor-description"[^>]*style="[^"]*max-width: 400px/);
- assert.match(result.html, /href="https:\/\/partner\.transistor\.fm\/ghost\/abc-123-def"/);
- assert(!result.html.includes('%%abc-123-def%%'));
- });
+ it('applies custom link colors when design customization is enabled', async function () {
+ lexicalRenderStub.resolves('Custom link
');
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
- it('applies bookmark and YouTube embed card styles', async function () {
- lexicalRenderStub.resolves(`
-
-
-
-
Example title
-
Example description
-
- Example author
-
-
-
-
-
-
-
-
-
-
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Test Subject',
+ designSettings: {
+ link_color: '#000000'
+ },
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
+
+ assert(result.html.includes('class="post-content"'));
+ assert(result.html.includes('color: #000000'));
+ });
+
+ it('applies header image styles and preserves header background color', async function () {
+ lexicalRenderStub.resolves('Content
');
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Test Subject',
+ designSettings: {
+ header_background_color: '#123456',
+ header_image: 'https://example.com/header.png',
+ show_badge: false,
+ show_header_title: false
+ },
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
+
+ assert.match(result.html, /class="header"[^>]*background-color:\s*#123456/i);
+ assert.match(result.html, /class="header-main"[^>]*background-color:\s*#123456/i);
+ assert.match(result.html, /class="header-image"/i);
+ assert.match(result.html, /src="https:\/\/example\.com\/header\.png"/);
+ });
+
+ it('applies shared Koenig card styles used by newsletters', async function () {
+ lexicalRenderStub.resolves(`
+
-
- Embed note
-
- `);
- const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+
+
+ `);
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Welcome!',
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
- const result = await renderer.render({
- lexical: '{}',
- subject: 'Welcome!',
- member: {name: 'John', email: 'john@example.com'},
- siteSettings: defaultSiteSettings
+ assert(result.html.includes('kg-callout-card'));
+ assert(result.html.includes('padding: 24px'));
+ assert(result.html.includes('table class="btn"'));
+ assert(result.html.includes('background-color: #ff0000'));
});
- assert.match(result.html, /class="kg-bookmark-container"[^>]*style="[^"]*display: flex/);
- assert.match(result.html, /class="kg-video-preview"[^>]*style="[^"]*background-color: #1d1f21/);
- assert(result.html.includes('Embed note'));
- });
+ it('applies transistor card styles in welcome emails', async function () {
+ lexicalRenderStub.resolves(`
+
+ `);
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
- it('applies call-to-action and product card styles', async function () {
- lexicalRenderStub.resolves(`
-
-
-
-
-
-
-
-
- CTA body with link
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Product title
-
-
-
- Product description
-
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Welcome!',
+ member: {name: 'John', email: 'john@example.com', uuid: 'abc-123-def'},
+ siteSettings: defaultSiteSettings
+ });
+
+ assert.match(result.html, /class="kg-card kg-transistor-card"[^>]*style="[^"]*border-radius: 10px/);
+ assert.match(result.html, /class="kg-card kg-transistor-card"[^>]*style="[^"]*border: 1px solid rgba\(0, 0, 0, 0.12\)/);
+ assert.match(result.html, /class="kg-transistor-title"[^>]*style="[^"]*display: block/);
+ assert.match(result.html, /class="kg-transistor-title"[^>]*style="[^"]*text-decoration: none/);
+ assert.match(result.html, /class="kg-transistor-description"[^>]*style="[^"]*max-width: 400px/);
+ assert.match(result.html, /href="https:\/\/partner\.transistor\.fm\/ghost\/abc-123-def"/);
+ assert(!result.html.includes('%%abc-123-def%%'));
+ });
+
+ it('applies bookmark and YouTube embed card styles', async function () {
+ lexicalRenderStub.resolves(`
+
+
+
+
Example title
+
Example description
+
+ Example author
+
+
+
+
+
+
+
+
+
+
-
-
-
- `);
- const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ Embed note
+
+ `);
+ const renderer = new MemberWelcomeEmailRenderer({t: key => key});
+
+ const result = await renderer.render({
+ lexical: '{}',
+ subject: 'Welcome!',
+ member: {name: 'John', email: 'john@example.com'},
+ siteSettings: defaultSiteSettings
+ });
- const result = await renderer.render({
- lexical: '{}',
- subject: 'Welcome!',
- member: {name: 'John', email: 'john@example.com'},
- siteSettings: defaultSiteSettings
+ assert.match(result.html, /class="kg-bookmark-container"[^>]*style="[^"]*display: flex/);
+ assert.match(result.html, /class="kg-video-preview"[^>]*style="[^"]*background-color: #1d1f21/);
+ assert(result.html.includes('Embed note'));
});
- assert.match(result.html, /class="kg-card kg-cta-card kg-cta-bg-none kg-cta-immersive kg-cta-link-accent"[^>]*style="[^"]*border-bottom: 1px solid #e0e7eb/);
- assert.match(result.html, /class="kg-cta-sponsor-label"[^>]*style="[^"]*border-bottom: 1px solid #e0e7eb/);
- assert.match(result.html, /class="kg-product-card"[^>]*style="[^"]*background-color: rgba\(255, 255, 255, 0.25\)/);
-
- const productButtonTableMatch = result.html.match(/class="kg-product-button-wrapper"[\s\S]*?]*class="btn"[^>]*style="([^"]*)"/);
- assert(productButtonTableMatch, 'product button table should have inline styles');
- assert(productButtonTableMatch[1].includes('width: 100%'), 'product button table should have width: 100%');
- });
-
- it('does not inline margin 0 auto on button tables that would override alignment', async function () {
- lexicalRenderStub.resolves(`
-