Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td>
<tr class="post-content-row">
<td class="post-content">
{{{content}}}
</td>
</tr>
Expand All @@ -13,11 +13,21 @@
<tr>
<td class="wrapper" align="center">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-top: 40px; padding-bottom: 30px;">
{{#if footerContent }}
<tr>
<td class="footer">{{{footerContent}}}</td>
</tr>
{{/if}}
<tr>
<td class="footer">
{{siteTitle}} &copy; {{year}} &mdash; <a href="{{managePreferencesUrl}}">{{t "Manage your preferences"}}</a>
</td>
</tr>
{{#if showBadge }}
<tr>
<td class="footer-powered"><a href="https://ghost.org/?via=pbg-newsletter"><img src="https://static.ghost.org/v4.0.0/images/powered.png" border="0" width="142" height="30" class="gh-powered" alt="Powered by Ghost"></a></td>
</tr>
{{/if}}
</table>
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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',
'<style>\n{{>baseStyles}}\n{{>contentStyles}}\n{{>cardStyles}}\n</style>'
);
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',
'<style>\n{{>baseStyles}}\n{{>contentStyles}}\n{{>cardStyles}}\n</style>'
);
}
const emailWrapperSource = fs.readFileSync(
path.join(__dirname, '../email-rendering/partials/email-wrapper.hbs'),
'utf8'
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand Down
41 changes: 31 additions & 10 deletions ghost/core/core/server/services/member-welcome-emails/service.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -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()
});
Expand All @@ -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});
}
}

Expand Down
Loading
Loading