diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap index 45469deaaa..e3ac021810 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap @@ -169,6 +169,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -305,12 +313,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -701,7 +711,7 @@ exports[`Email Preview API Read can read post email preview with email card and Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "21891", + "content-length": "22062", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -784,6 +794,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -920,12 +938,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -1336,7 +1356,7 @@ exports[`Email Preview API Read can read post email preview with fields 4: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "26714", + "content-length": "26885", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1450,6 +1470,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -1586,12 +1614,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -1989,7 +2019,7 @@ exports[`Email Preview API Read has custom content transformations for email com Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "21657", + "content-length": "21828", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2432,6 +2462,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -2568,12 +2606,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -2981,7 +3021,7 @@ exports[`Email Preview API Read uses the newsletter provided through ?newsletter Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "22149", + "content-length": "22320", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3450,6 +3490,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -3586,12 +3634,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -3999,7 +4049,7 @@ exports[`Email Preview API Read uses the posts newsletter by default 4: [headers Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "22149", + "content-length": "22320", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap b/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap index a2c608b885..c6661e18c4 100644 --- a/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap +++ b/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap @@ -62,6 +62,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -198,12 +206,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -657,6 +667,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -793,12 +811,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -1238,6 +1258,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -1374,12 +1402,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -1819,6 +1849,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -1955,12 +1993,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -2400,6 +2440,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -2536,12 +2584,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -2939,6 +2989,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -3075,12 +3133,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -4594,6 +4654,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -4730,12 +4798,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -5275,6 +5345,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -5411,12 +5489,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -5912,6 +5992,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -6048,12 +6136,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -7797,6 +7887,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -7933,12 +8031,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -8433,6 +8533,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -8569,12 +8677,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -9069,6 +9179,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -9205,12 +9323,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -9705,6 +9825,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -9841,12 +9969,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -10341,6 +10471,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -10477,12 +10615,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -11613,6 +11753,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -11749,12 +11897,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } @@ -12196,6 +12346,14 @@ Object { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -12332,12 +12490,14 @@ table.body .footer a { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, +table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, +table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } diff --git a/ghost/email-service/lib/email-renderer.js b/ghost/email-service/lib/email-renderer.js index 3c2493b7c2..fde383874b 100644 --- a/ghost/email-service/lib/email-renderer.js +++ b/ghost/email-service/lib/email-renderer.js @@ -21,6 +21,15 @@ const messages = { } }; +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function formatDateLong(date, timezone) { return DateTime.fromJSDate(date).setZone(timezone).setLocale('en-gb').toLocaleString({ year: 'numeric', @@ -710,6 +719,27 @@ class EmailRenderer { } } + /** + * + * @param {*} text + * @param {number} maxLength + * @param {number} maxLengthMobile should be larger than maxLength + * @returns + */ + truncateHtml(text, maxLength, maxLengthMobile) { + if (!maxLengthMobile || maxLength >= maxLengthMobile) { + return escapeHtml(this.truncateText(text, maxLength)); + } + if (text && text.length > maxLength) { + if (text.length <= maxLengthMobile) { + return escapeHtml(text.substring(0, maxLength - 1)) + '' + escapeHtml(text.substring(maxLength - 1, maxLengthMobile - 1)) + '' + ''; + } + return escapeHtml(text.substring(0, maxLength - 1)) + '' + escapeHtml(text.substring(maxLength - 1, maxLengthMobile - 1)) + '' + '…'; + } else { + return escapeHtml(text ?? ''); + } + } + /** * @private */ @@ -784,7 +814,7 @@ class EmailRenderer { const {href: featureImageMobile, width: featureImageMobileWidth, height: featureImageMobileHeight} = await this.limitImageWidth(latestPost.get('feature_image'), 600, 480); latestPosts.push({ - title: this.truncateText(latestPost.get('title'), 85), + title: this.truncateHtml(latestPost.get('title'), featureImage ? 85 : 105, 105), url: this.#getPostUrl(latestPost), featureImage: featureImage ? { src: featureImage, @@ -796,7 +826,7 @@ class EmailRenderer { width: featureImageMobileWidth, height: featureImageMobileHeight } : null, - excerpt: this.truncateText(latestPost.get('custom_excerpt') || latestPost.get('plaintext'), 60) + excerpt: this.truncateHtml(latestPost.get('custom_excerpt') || latestPost.get('plaintext'), featureImage ? 60 : 70, 105) }); if (featureImage) { diff --git a/ghost/email-service/lib/email-templates/partials/latest-posts.hbs b/ghost/email-service/lib/email-templates/partials/latest-posts.hbs index 113903bf80..cff32deb18 100644 --- a/ghost/email-service/lib/email-templates/partials/latest-posts.hbs +++ b/ghost/email-service/lib/email-templates/partials/latest-posts.hbs @@ -21,9 +21,9 @@ {{/if}} {{/if}} -

{{title}}

+

{{{title}}}

{{#if excerpt}} -

{{excerpt}}

+

{{{excerpt}}}

{{/if}} {{#if ../latestPostsHasImages}} diff --git a/ghost/email-service/lib/email-templates/partials/styles.hbs b/ghost/email-service/lib/email-templates/partials/styles.hbs index 8172edf807..6adfd6823b 100644 --- a/ghost/email-service/lib/email-templates/partials/styles.hbs +++ b/ghost/email-service/lib/email-templates/partials/styles.hbs @@ -1071,6 +1071,10 @@ a[data-flickr-embed] img { width: 0; } +.mobile-only { + display: none; +} + /* ------------------------------------- RESPONSIVE AND MOBILE FRIENDLY STYLES ------------------------------------- */ @@ -1081,6 +1085,14 @@ a[data-flickr-embed] img { min-width: 100%; } + .hide-mobile { + display: none; + } + + .mobile-only { + display: initial !important; + } + table.body p, table.body ul, table.body ol, @@ -1218,12 +1230,12 @@ a[data-flickr-embed] img { padding-bottom: 8px !important; } - table.body .latest-post h4 { + table.body .latest-post h4, table.body .latest-post h4 span { padding: 4px 0 !important; font-size: 18px !important; } - table.body .latest-post p { + table.body .latest-post p, table.body .latest-post p span { font-size: 13px !important; line-height: 1.25em; } diff --git a/ghost/email-service/test/email-renderer.test.js b/ghost/email-service/test/email-renderer.test.js index 8df0c149df..0269b52cea 100644 --- a/ghost/email-service/test/email-renderer.test.js +++ b/ghost/email-service/test/email-renderer.test.js @@ -1659,7 +1659,7 @@ describe('Email renderer', function () { assert.deepEqual(data.latestPosts, [ { - excerpt: 'Super long custom excerpt. Super long custom excerpt. Super…', + excerpt: 'Super long custom excerpt. Super long custom excerpt. Super long custom excerpt. Super long custom excer…', title: 'Test Post 1', url: 'http://example.com', featureImage: { @@ -1740,6 +1740,18 @@ describe('Email renderer', function () { }); }); + describe('truncateHTML', function () { + it('works correctly', async function () { + const emailRenderer = new EmailRenderer({}); + assert.equal(emailRenderer.truncateHtml('This is a short one', 5, 10), 'This is a…'); + assert.equal(emailRenderer.truncateHtml('This is a', 5, 10), 'This is a'); + assert.equal(emailRenderer.truncateHtml('This', 5, 10), 'This'); + assert.equal(emailRenderer.truncateHtml('This is a long text', 5, 5), 'This…'); + assert.equal(emailRenderer.truncateHtml('This is a long text', 5), 'This…'); + assert.equal(emailRenderer.truncateHtml(null, 5, 10), ''); + }); + }); + describe('limitImageWidth', function () { it('Limits width of local images', async function () { const emailRenderer = new EmailRenderer({