Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,52 @@
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;

// TODO: remove this constant after removing the labs flag, as we won't need these defaults anymore

Check warning on line 16 in ghost/core/core/server/services/member-welcome-emails/member-welcome-email-renderer.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ1pJvVzCMRywbZ289xQ&open=AZ1pJvVzCMRywbZ289xQ&pullRequest=27191
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 +62,17 @@
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 +143,32 @@
* @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');

if (!useDesignCustomization) {
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 +200,39 @@

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