diff --git a/ghost/email-service/lib/EmailRenderer.js b/ghost/email-service/lib/EmailRenderer.js index 681fc86224..dce7aa35a0 100644 --- a/ghost/email-service/lib/EmailRenderer.js +++ b/ghost/email-service/lib/EmailRenderer.js @@ -8,6 +8,7 @@ const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@trygh const {DateTime} = require('luxon'); const htmlToPlaintext = require('@tryghost/html-to-plaintext'); const tpl = require('@tryghost/tpl'); +const cheerio = require('cheerio'); const messages = { subscriptionStatus: { @@ -218,7 +219,6 @@ class EmailRenderer { return allowedSegments; } - const cheerio = require('cheerio'); const $ = cheerio.load(html); let allSegments = $('[data-gh-segment]') @@ -281,11 +281,30 @@ class EmailRenderer { } } + let $ = cheerio.load(html); + + // Remove parts of the HTML not applicable to the current segment - We do this + // before rendering the template as the preheader for the email may be generated + // using the HTML and we don't want to include content that should not be + // visible depending on the segment + $('[data-gh-segment]').get().forEach((node) => { + // TODO: replace with NQL interpretation + if (node.attribs['data-gh-segment'] !== segment) { + $(node).remove(); + } else { + // Getting rid of the attribute for a cleaner html output + $(node).removeAttr('data-gh-segment'); + } + }); + + html = $.html(); + const templateData = await this.getTemplateData({ post, newsletter, html, - addPaywall + addPaywall, + segment }); html = await this.renderTemplate(templateData); @@ -326,8 +345,7 @@ class EmailRenderer { html = juice(html, {inlinePseudoElements: true, removeStyleTags: true}); // happens after inlining of CSS so we can change element types without worrying about styling - const cheerio = require('cheerio'); - const $ = cheerio.load(html); + $ = cheerio.load(html); // force all links to open in new tab $('a').attr('target', '_blank'); @@ -335,17 +353,6 @@ class EmailRenderer { // convert figure and figcaption to div so that Outlook applies margins $('figure, figcaption').each((i, elem) => !!(elem.tagName = 'div')); - // Remove/hide parts of the email based on segment data attributes - $('[data-gh-segment]').get().forEach((node) => { - // TODO: replace with NQL interpretation - if (node.attribs['data-gh-segment'] !== segment) { - $(node).remove(); - } else { - // Getting rid of the attribute for a cleaner html output - $(node).removeAttr('data-gh-segment'); - } - }); - // Remove duplicate black/white images (CSS based solution not working in Outlook) if (templateData.backgroundIsDark) { $('img.is-light-background').each((i, elem) => { @@ -715,13 +722,19 @@ class EmailRenderer { * @param {object} postModel * @returns */ - #getEmailPreheader(postModel) { + #getEmailPreheader(postModel, segment, html) { let plaintext = postModel.get('plaintext'); let customExcerpt = postModel.get('custom_excerpt'); if (customExcerpt) { return customExcerpt; } else { if (plaintext) { + // The plaintext field on the model may contain paid only content + // so we use the provided HTML to generate the plaintext as this + // should have already had the paid content removed + if (segment === 'status:free') { + plaintext = htmlToPlaintext.email(html); + } return plaintext.substring(0, 500); } else { return `${postModel.get('title')} – `; @@ -821,7 +834,7 @@ class EmailRenderer { /** * @private */ - async getTemplateData({post, newsletter, html, addPaywall}) { + async getTemplateData({post, newsletter, html, addPaywall, segment}) { let accentColor = this.#settingsCache.get('accent_color') || '#15212A'; let adjustedAccentColor; let adjustedAccentContrastColor; @@ -931,7 +944,7 @@ class EmailRenderer { image: this.#settingsCache.get('icon') }, true) : null }, - preheader: this.#getEmailPreheader(post), + preheader: this.#getEmailPreheader(post, segment, html), html, post: { diff --git a/ghost/email-service/test/email-renderer.test.js b/ghost/email-service/test/email-renderer.test.js index e1e102b585..9c5788e438 100644 --- a/ghost/email-service/test/email-renderer.test.js +++ b/ghost/email-service/test/email-renderer.test.js @@ -855,7 +855,7 @@ describe('Email renderer', function () { }); describe('renderBody', function () { - let renderedPost = '

Lexical Test

'; + let renderedPost; let postUrl = 'http://example.com'; let customSettings = {}; let emailRenderer; @@ -864,6 +864,7 @@ describe('Email renderer', function () { let labsEnabled; beforeEach(function () { + renderedPost = '

Lexical Test

'; labsEnabled = true; basePost = { lexical: '{}', @@ -1057,6 +1058,74 @@ describe('Email renderer', function () { should($('.preheader').text()).eql('Custom excerpt'); }); + it('does not include members-only content in preheader for non-members', async function () { + renderedPost = '
Lexical Test
some text for both finishing part only for members'; + let post = { + related: sinon.stub(), + get: (key) => { + if (key === 'lexical') { + return '{}'; + } + + if (key === 'visibility') { + return 'paid'; + } + + if (key === 'plaintext') { + return 'foobarbaz'; + } + }, + getLazyRelation: sinon.stub() + }; + let newsletter = { + get: sinon.stub() + }; + + let response = await emailRenderer.renderBody( + post, + newsletter, + 'status:free', + {} + ); + + const $ = cheerio.load(response.html); + should($('.preheader').text()).eql('Lexical Test some text for both'); + }); + + it('does not include paid segmented content in preheader for non-paying members', async function () { + renderedPost = '
Lexical Test
members only section
some text for both'; + let post = { + related: sinon.stub(), + get: (key) => { + if (key === 'lexical') { + return '{}'; + } + + if (key === 'visibility') { + return 'public'; + } + + if (key === 'plaintext') { + return 'foobarbaz'; + } + }, + getLazyRelation: sinon.stub() + }; + let newsletter = { + get: sinon.stub() + }; + + let response = await emailRenderer.renderBody( + post, + newsletter, + 'status:free', + {} + ); + + const $ = cheerio.load(response.html); + should($('.preheader').text()).eql('Lexical Test some text for both'); + }); + it('only includes first author if more than 2', async function () { const post = createModel({...basePost, authors: [ createModel({