Skip to content
Closed
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,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);
Comment on lines +64 to +68
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure this is the right approach here. Basically this uses the styles.hbs from newsletters directly, which includes some cruft that we don't need in welcome emails. It works, but I'm not sure how I feel about this architecturally - maybe we don't want to couple welcome emails and newsletters in this way?

} 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