ghost 5.14.1 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/{tryghost-adapter-manager-5.14.1.tgz → tryghost-adapter-manager-5.15.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.14.1.tgz → tryghost-api-framework-5.15.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.14.1.tgz → tryghost-api-version-compatibility-service-5.15.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.15.0.tgz +0 -0
- package/components/tryghost-constants-5.15.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.14.1.tgz → tryghost-custom-theme-settings-service-5.15.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.15.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.15.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.15.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.15.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.15.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.15.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.15.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.14.1.tgz → tryghost-job-manager-5.15.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.15.0.tgz +0 -0
- package/components/tryghost-link-replacement-5.15.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.15.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.14.1.tgz → tryghost-magic-link-5.15.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.15.0.tgz +0 -0
- package/components/{tryghost-member-analytics-service-5.14.1.tgz → tryghost-member-analytics-service-5.15.0.tgz} +0 -0
- package/components/tryghost-member-attribution-5.15.0.tgz +0 -0
- package/components/tryghost-member-events-5.15.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.15.0.tgz +0 -0
- package/components/tryghost-members-api-5.15.0.tgz +0 -0
- package/components/tryghost-members-csv-5.15.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.15.0.tgz +0 -0
- package/components/tryghost-members-importer-5.15.0.tgz +0 -0
- package/components/tryghost-members-offers-5.15.0.tgz +0 -0
- package/components/{tryghost-members-payments-5.14.1.tgz → tryghost-members-payments-5.15.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.14.1.tgz → tryghost-members-ssr-5.15.0.tgz} +0 -0
- package/components/tryghost-members-stripe-service-5.15.0.tgz +0 -0
- package/components/tryghost-minifier-5.15.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.15.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.15.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.14.1.tgz → tryghost-mw-error-handler-5.15.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.15.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.15.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.15.0.tgz +0 -0
- package/components/{tryghost-oembed-service-5.14.1.tgz → tryghost-oembed-service-5.15.0.tgz} +0 -0
- package/components/{tryghost-package-json-5.14.1.tgz → tryghost-package-json-5.15.0.tgz} +0 -0
- package/components/tryghost-security-5.15.0.tgz +0 -0
- package/components/{tryghost-session-service-5.14.1.tgz → tryghost-session-service-5.15.0.tgz} +0 -0
- package/components/{tryghost-settings-path-manager-5.14.1.tgz → tryghost-settings-path-manager-5.15.0.tgz} +0 -0
- package/components/tryghost-staff-service-5.15.0.tgz +0 -0
- package/components/{tryghost-update-check-service-5.14.1.tgz → tryghost-update-check-service-5.15.0.tgz} +0 -0
- package/components/{tryghost-verification-trigger-5.14.1.tgz → tryghost-verification-trigger-5.15.0.tgz} +0 -0
- package/components/tryghost-version-notifications-data-service-5.15.0.tgz +0 -0
- package/content/themes/casper/default.hbs +2 -2
- package/core/boot.js +12 -3
- package/core/built/admin/assets/{chunk.143.fa1d2a279fd46b1f8e63.js → chunk.143.558b9943af7b15f189ae.js} +20 -20
- package/core/built/admin/assets/{chunk.174.2edaa0869bfc2d88cf90.js → chunk.174.e1e89637eab79fdd5c5d.js} +68 -68
- package/core/built/admin/assets/{chunk.178.2292bd0899558aebead6.js → chunk.178.a9f6ddaea01e2bc76235.js} +4 -4
- package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js → chunk.579.dc11bf8dda5cf4406708.js} +5464 -4961
- package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt → chunk.579.dc11bf8dda5cf4406708.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/fonts/{Inter.ttf → Inter-e19174fb2c0e19b1fa67492a07886c75.ttf} +0 -0
- package/core/built/admin/assets/{ghost-8919656440ad4617a07bb31069b1f71b.js → ghost-4b1b550e34300f5f4774a261aac29557.js} +487 -470
- package/core/built/admin/assets/ghost-c933adafb359b75ea1577365ce252e76.css +1 -0
- package/core/built/admin/assets/ghost-dark-04981c84bf590e0fae0a8e83e018190f.css +1 -0
- package/core/built/admin/assets/img/{amp.svg → amp-d7b72aae3315fda95921fb575dfca100.svg} +0 -0
- package/core/built/admin/assets/img/{disqus.svg → disqus-43503a3fa4f38dc8c61c7358b811f343.svg} +0 -0
- package/core/built/admin/assets/img/{favicon.ico → favicon-a9c6dbdcdc3ae568f4e0dad92149a0e3.ico} +0 -0
- package/core/built/admin/assets/img/{github.svg → github-c3a739c59df26fed12c10ffb00b33bd4.svg} +0 -0
- package/core/built/admin/assets/img/{google-docs.svg → google-docs-1e42cc272fc088da49e4b0ddfb01b006.svg} +0 -0
- package/core/built/admin/assets/img/{mailchimp.svg → mailchimp-f22b1e130aac764965b9306d7265a6b2.svg} +0 -0
- package/core/built/admin/assets/img/{patreon.svg → patreon-b19a5e6418a72977a16b30039d374d04.svg} +0 -0
- package/core/built/admin/assets/img/{paypal.svg → paypal-38e9448ce7549ea4caf8e7753ae661d6.svg} +0 -0
- package/core/built/admin/assets/img/{twitter.svg → twitter-7a7a0ba12d9b5bfb8a2058764a827c31.svg} +0 -0
- package/core/built/admin/assets/img/{typeform.svg → typeform-9f23f8712d776a7515594676285266f5.svg} +0 -0
- package/core/built/admin/assets/img/{unsplash.svg → unsplash-5b329eef0b11447b4117eaf817ebad6f.svg} +0 -0
- package/core/built/admin/assets/img/{zapier.svg → zapier-bf93bc440a3fd43b73489a63c215cdc7.svg} +0 -0
- package/core/built/admin/assets/img/{zapier-logo.svg → zapier-logo-a125f24313dfe01ef49af01fc90061fb.svg} +0 -0
- package/core/built/admin/assets/{vendor-eb76d0236a09b8b6f44675dba45b1fc6.js → vendor-271c32988ab16ba175a9bfa2acb2887a.js} +45 -39
- package/core/built/admin/assets/videos/logo-loader.mp4 +0 -0
- package/core/built/admin/index.html +11 -8
- package/core/frontend/src/member-attribution/member-attribution.js +27 -0
- package/core/frontend/web/site.js +10 -7
- package/core/server/api/endpoints/redirects.js +6 -8
- package/core/server/api/endpoints/utils/permissions.js +2 -16
- package/core/server/api/endpoints/utils/serializers/input/pages.js +5 -5
- package/core/server/api/endpoints/utils/serializers/input/posts.js +7 -7
- package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
- package/core/server/api/endpoints/utils/validators/input/pages.js +24 -9
- package/core/server/api/endpoints/utils/validators/input/posts.js +24 -9
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/migrations/utils/settings.js +1 -3
- package/core/server/data/migrations/versions/5.15/2022-09-12-16-10-add-posts-lexical-column.js +8 -0
- package/core/server/data/migrations/versions/5.15/2022-09-14-12-46-add-email-track-clicks-setting.js +8 -0
- package/core/server/data/migrations/versions/5.15/2022-09-16-08-22-add-post-revisions-table.js +9 -0
- package/core/server/data/schema/default-settings/default-settings.json +8 -0
- package/core/server/data/schema/schema.js +8 -0
- package/core/server/lib/lexical.js +12 -0
- package/core/server/models/base/plugins/user-type.js +4 -6
- package/core/server/models/post-revision.js +35 -0
- package/core/server/models/post.js +72 -7
- package/core/server/services/bulk-email/bulk-email-processor.js +2 -5
- package/core/server/services/{redirects → custom-redirects}/api.js +0 -0
- package/core/server/services/{redirects → custom-redirects}/index.js +0 -0
- package/core/server/services/{redirects → custom-redirects}/utils.js +0 -0
- package/core/server/services/{redirects → custom-redirects}/validation.js +0 -0
- package/core/server/services/explore/service.js +5 -3
- package/core/server/services/link-click-tracking/index.js +25 -0
- package/core/server/services/link-redirection/index.js +33 -0
- package/core/server/services/link-replacement/index.js +24 -0
- package/core/server/services/mega/email-preview.js +7 -0
- package/core/server/services/mega/mega.js +1 -1
- package/core/server/services/mega/post-email-serializer.js +75 -27
- package/core/server/services/mega/template.js +1 -1
- package/core/server/services/members/api.js +0 -2
- package/core/server/services/permissions/index.js +1 -2
- package/core/server/services/posts/posts-service.js +7 -16
- package/core/server/services/posts/stats/post-stats.js +35 -0
- package/core/server/services/staff/index.js +10 -1
- package/core/server/services/url/config.js +2 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +3 -2
- package/core/shared/labs.js +4 -2
- package/package.json +97 -90
- package/yarn.lock +395 -198
- package/components/tryghost-bootstrap-socket-5.14.1.tgz +0 -0
- package/components/tryghost-constants-5.14.1.tgz +0 -0
- package/components/tryghost-domain-events-5.14.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.14.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.14.1.tgz +0 -0
- package/components/tryghost-email-content-generator-5.14.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.14.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.14.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.14.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.14.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.14.1.tgz +0 -0
- package/components/tryghost-member-events-5.14.1.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.14.1.tgz +0 -0
- package/components/tryghost-members-api-5.14.1.tgz +0 -0
- package/components/tryghost-members-csv-5.14.1.tgz +0 -0
- package/components/tryghost-members-events-service-5.14.1.tgz +0 -0
- package/components/tryghost-members-importer-5.14.1.tgz +0 -0
- package/components/tryghost-members-offers-5.14.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.14.1.tgz +0 -0
- package/components/tryghost-minifier-5.14.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.14.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.14.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.14.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.14.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.14.1.tgz +0 -0
- package/components/tryghost-security-5.14.1.tgz +0 -0
- package/components/tryghost-staff-service-5.14.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.14.1.tgz +0 -0
- package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +0 -1
- package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +0 -1
- package/core/server/services/permissions/public.js +0 -76
|
@@ -46,11 +46,13 @@ module.exports = class ExploreService {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
const mostRecentlyPublishedPost = await this.PostsService.
|
|
50
|
-
|
|
49
|
+
const mostRecentlyPublishedPost = await this.PostsService.stats.getMostRecentlyPublishedPostDate();
|
|
50
|
+
const totalPostsPublished = await this.PostsService.stats.getTotalPostsPublished();
|
|
51
|
+
exploreProperties.most_recently_published_at = mostRecentlyPublishedPost ?? null;
|
|
52
|
+
exploreProperties.total_posts_published = totalPostsPublished ?? null;
|
|
51
53
|
|
|
52
54
|
const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
|
|
53
|
-
exploreProperties.owner_email = owner?.get('email')
|
|
55
|
+
exploreProperties.owner_email = owner?.get('email') ?? null;
|
|
54
56
|
|
|
55
57
|
return exploreProperties;
|
|
56
58
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class LinkTrackingServiceWrapper {
|
|
2
|
+
async init() {
|
|
3
|
+
if (this.service) {
|
|
4
|
+
// Already done
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Wire up all the dependencies
|
|
9
|
+
const LinkTrackingService = require('@tryghost/link-tracking');
|
|
10
|
+
|
|
11
|
+
// Expose the service
|
|
12
|
+
this.service = new LinkTrackingService({
|
|
13
|
+
linkClickRepository: {
|
|
14
|
+
async save(linkClick) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.log('Saving link click', linkClick);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await this.service.init();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = new LinkTrackingServiceWrapper();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
2
|
+
|
|
3
|
+
class LinkRedirectsServiceWrapper {
|
|
4
|
+
async init() {
|
|
5
|
+
if (this.service) {
|
|
6
|
+
// Already done
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Wire up all the dependencies
|
|
11
|
+
const {LinkRedirectsService} = require('@tryghost/link-redirects');
|
|
12
|
+
|
|
13
|
+
const store = [];
|
|
14
|
+
// Expose the service
|
|
15
|
+
this.service = new LinkRedirectsService({
|
|
16
|
+
linkRedirectRepository: {
|
|
17
|
+
async save(linkRedirect) {
|
|
18
|
+
store.push(linkRedirect);
|
|
19
|
+
},
|
|
20
|
+
async getByURL(url) {
|
|
21
|
+
return store.find((link) => {
|
|
22
|
+
return link.from.pathname === url.pathname;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
config: {
|
|
27
|
+
baseURL: new URL(urlUtils.getSiteUrl())
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = new LinkRedirectsServiceWrapper();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class LinkReplacementServiceWrapper {
|
|
2
|
+
init() {
|
|
3
|
+
if (this.service) {
|
|
4
|
+
// Already done
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Wire up all the dependencies
|
|
9
|
+
const LinkReplacementService = require('@tryghost/link-replacement');
|
|
10
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
11
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
12
|
+
|
|
13
|
+
// Expose the service
|
|
14
|
+
this.service = new LinkReplacementService({
|
|
15
|
+
linkRedirectService: require('../link-redirection').service,
|
|
16
|
+
linkClickTrackingService: require('../link-click-tracking').service,
|
|
17
|
+
attributionService: require('../member-attribution').service,
|
|
18
|
+
urlUtils,
|
|
19
|
+
settingsCache
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = new LinkReplacementServiceWrapper();
|
|
@@ -27,6 +27,7 @@ class EmailPreview {
|
|
|
27
27
|
emailContent = postEmailSerializer.renderEmailForSegment(emailContent, memberSegment);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Do fake replacements, just like a normal email, but use fallbacks and empty values
|
|
30
31
|
const replacements = postEmailSerializer.parseReplacements(emailContent);
|
|
31
32
|
|
|
32
33
|
replacements.forEach((replacement) => {
|
|
@@ -36,6 +37,12 @@ class EmailPreview {
|
|
|
36
37
|
);
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
// Replace unsubscribe URL (%recipient.unsubscribe_url% replacement)
|
|
41
|
+
// We should do this only here because replacements should happen at the very end only, just like when an actual email would be send
|
|
42
|
+
const previewUnsubscribeUrl = postEmailSerializer.createUnsubscribeUrl(null);
|
|
43
|
+
emailContent.html = emailContent.html.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
|
|
44
|
+
emailContent.plaintext = emailContent.plaintext.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
|
|
45
|
+
|
|
39
46
|
return {
|
|
40
47
|
subject: emailContent.subject,
|
|
41
48
|
html: emailContent.html,
|
|
@@ -85,7 +85,7 @@ const getEmailData = async (postModel, options) => {
|
|
|
85
85
|
* @param {ValidMemberSegment} [memberSegment]
|
|
86
86
|
*/
|
|
87
87
|
const sendTestEmail = async (postModel, toEmails, memberSegment) => {
|
|
88
|
-
let emailData = await getEmailData(postModel);
|
|
88
|
+
let emailData = await getEmailData(postModel, {isTestEmail: true});
|
|
89
89
|
emailData.subject = `[Test] ${emailData.subject}`;
|
|
90
90
|
|
|
91
91
|
// fetch any matching members so that replacements use expected values
|
|
@@ -14,8 +14,9 @@ const {isUnsplashImage, isLocalContentImage} = require('@tryghost/kg-default-car
|
|
|
14
14
|
const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@tryghost/color-utils');
|
|
15
15
|
const logging = require('@tryghost/logging');
|
|
16
16
|
const urlService = require('../../services/url');
|
|
17
|
+
const linkReplacement = require('../link-replacement');
|
|
17
18
|
|
|
18
|
-
const ALLOWED_REPLACEMENTS = ['first_name'];
|
|
19
|
+
const ALLOWED_REPLACEMENTS = ['first_name', 'uuid'];
|
|
19
20
|
|
|
20
21
|
const PostEmailSerializer = {
|
|
21
22
|
|
|
@@ -243,7 +244,7 @@ const PostEmailSerializer = {
|
|
|
243
244
|
return templateSettings;
|
|
244
245
|
},
|
|
245
246
|
|
|
246
|
-
async serialize(postModel, newsletter, options = {isBrowserPreview: false}) {
|
|
247
|
+
async serialize(postModel, newsletter, options = {isBrowserPreview: false, isTestEmail: false}) {
|
|
247
248
|
const post = await this.serializePostModel(postModel);
|
|
248
249
|
|
|
249
250
|
const timezone = settingsCache.get('timezone');
|
|
@@ -276,7 +277,7 @@ const PostEmailSerializer = {
|
|
|
276
277
|
// perform any email specific adjustments to the mobiledoc->HTML render output
|
|
277
278
|
// body wrapper is required so we can get proper top-level selections
|
|
278
279
|
const cheerio = require('cheerio');
|
|
279
|
-
|
|
280
|
+
const _cheerio = cheerio.load(`<body>${post.html}</body>`);
|
|
280
281
|
// remove leading/trailing HRs
|
|
281
282
|
_cheerio(`
|
|
282
283
|
body > hr:first-child,
|
|
@@ -284,8 +285,10 @@ const PostEmailSerializer = {
|
|
|
284
285
|
body > div:first-child > hr:first-child,
|
|
285
286
|
body > div:last-child > hr:last-child
|
|
286
287
|
`).remove();
|
|
287
|
-
post.html = _cheerio('body').html();
|
|
288
|
+
post.html = _cheerio('body').html(); // () (added this comment because of a bug in the syntax highlighter in VSCode)
|
|
288
289
|
|
|
290
|
+
// Note: we don't need to do link replacements on the plaintext here
|
|
291
|
+
// because the plaintext will get recalculated on the updated post html (which already includes link replacements) in renderEmailForSegment
|
|
289
292
|
post.plaintext = htmlToPlaintext.email(post.html);
|
|
290
293
|
|
|
291
294
|
// Outlook will render feature images at full-size breaking the layout.
|
|
@@ -325,24 +328,49 @@ const PostEmailSerializer = {
|
|
|
325
328
|
|
|
326
329
|
let htmlTemplate = render({post, site: this.getSite(), templateSettings, newsletter: newsletter.toJSON()});
|
|
327
330
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
+
// The plaintext version that is returned here is actually never really used for sending because we'll use htmlToPlaintext again later
|
|
332
|
+
let result = {
|
|
333
|
+
html: this.formatHtmlForEmail(htmlTemplate),
|
|
334
|
+
plaintext: post.plaintext
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* If a part of the email is members-only and the post is paid-only, add a paywall:
|
|
339
|
+
* - Just before sending the email, we'll hide the paywall or paid content depending on the member segment it is sent to.
|
|
340
|
+
* - We already need to do URL-replacement on the HTML here
|
|
341
|
+
* - Link replacement cannot happen later because renderEmailForSegment is called multiple times for a single email (which would result in duplicate redirects)
|
|
342
|
+
*/
|
|
343
|
+
const isPaidPost = post.visibility === 'paid' || post.visibility === 'tiers';
|
|
344
|
+
|
|
345
|
+
const paywallIndex = (result.html || '').indexOf('<!--members-only-->');
|
|
346
|
+
if (paywallIndex !== -1 && isPaidPost) {
|
|
347
|
+
const postContentEndIdx = result.html.indexOf('<!-- POST CONTENT END -->');
|
|
348
|
+
|
|
349
|
+
if (postContentEndIdx !== -1) {
|
|
350
|
+
const paywallHTML = '<!-- PAYWALL -->' + this.renderPaywallCTA(post);
|
|
351
|
+
|
|
352
|
+
// Append it just before the end of the post content
|
|
353
|
+
result.html = result.html.slice(0, postContentEndIdx) + paywallHTML + result.html.slice(postContentEndIdx);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Now replace the links in the HTML version
|
|
358
|
+
if (labs.isSet('emailClicks')) {
|
|
359
|
+
if ((!options.isBrowserPreview && !options.isTestEmail) || process.env.NODE_ENV === 'development') {
|
|
360
|
+
result.html = await linkReplacement.service.replaceLinks(result.html, newsletter, postModel);
|
|
361
|
+
}
|
|
331
362
|
}
|
|
332
363
|
|
|
333
364
|
// Clean up any unknown replacements strings to get our final content
|
|
334
|
-
const {html, plaintext} = this.normalizeReplacementStrings(
|
|
335
|
-
html: this.formatHtmlForEmail(htmlTemplate),
|
|
336
|
-
plaintext: post.plaintext
|
|
337
|
-
});
|
|
365
|
+
const {html, plaintext} = this.normalizeReplacementStrings(result);
|
|
338
366
|
const data = {
|
|
339
367
|
subject: post.email_subject || post.title,
|
|
340
368
|
html,
|
|
341
369
|
plaintext
|
|
342
370
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
371
|
+
|
|
372
|
+
// Add post for checking access in renderEmailForSegment (only for previews)
|
|
373
|
+
data.post = post;
|
|
346
374
|
return data;
|
|
347
375
|
},
|
|
348
376
|
|
|
@@ -392,25 +420,45 @@ const PostEmailSerializer = {
|
|
|
392
420
|
|
|
393
421
|
const result = {...email};
|
|
394
422
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
423
|
+
// Note about link tracking:
|
|
424
|
+
// Don't add new HTML in here, but add it in the serialize method and surround it with the required HTML comments or attributes
|
|
425
|
+
// This is because we can't replace links at this point (this is executed multiple times, once per batch and we don't want to generate duplicate links for the same email)
|
|
426
|
+
|
|
427
|
+
// Remove the paywall or members-only content based on the current member segment
|
|
428
|
+
const startMembersOnlyContent = (result.html || '').indexOf('<!--members-only-->');
|
|
429
|
+
const startPaywall = result.html.indexOf('<!-- PAYWALL -->');
|
|
430
|
+
let endPost = result.html.indexOf('<!-- POST CONTENT END -->');
|
|
431
|
+
|
|
432
|
+
if (endPost === -1) {
|
|
433
|
+
// Default to the end of the HTML (shouldn't happen, but just in case if we have members-only content that should get removed)
|
|
434
|
+
endPost = result.html.length;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// We support the cases where there is no <!--members-only--> but there is a paywall (in case of bugs)
|
|
438
|
+
// We also support the case where there is no <!-- PAYWALL --> but there is a <!--members-only--> (in case of bugs)
|
|
439
|
+
if (startMembersOnlyContent !== -1 || startPaywall !== -1) {
|
|
440
|
+
// By default remove the paywall if no memberSegment is passed
|
|
441
|
+
let memberHasAccess = true;
|
|
442
|
+
|
|
443
|
+
if (memberSegment && result.post) {
|
|
402
444
|
let statusFilter = memberSegment === 'status:free' ? {status: 'free'} : {status: 'paid'};
|
|
403
445
|
const postVisiblity = result.post.visibility;
|
|
404
446
|
|
|
405
447
|
// For newsletter paywall, specific tiers visibility is considered on par to paid tiers
|
|
406
448
|
result.post.visibility = postVisiblity === 'tiers' ? 'paid' : postVisiblity;
|
|
407
449
|
|
|
408
|
-
|
|
450
|
+
memberHasAccess = membersService.contentGating.checkPostAccess(result.post, statusFilter);
|
|
451
|
+
}
|
|
409
452
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
result.
|
|
453
|
+
if (!memberHasAccess) {
|
|
454
|
+
if (startMembersOnlyContent !== -1) {
|
|
455
|
+
// Remove the members-only content, but keep the paywall (if there is a paywall)
|
|
456
|
+
result.html = result.html.slice(0, startMembersOnlyContent) + result.html.slice(startPaywall === -1 ? endPost : startPaywall);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
if (startPaywall !== -1) {
|
|
460
|
+
// Remove the paywall
|
|
461
|
+
result.html = result.html.slice(0, startPaywall) + result.html.slice(endPost);
|
|
414
462
|
}
|
|
415
463
|
}
|
|
416
464
|
}
|
|
@@ -427,7 +475,7 @@ const PostEmailSerializer = {
|
|
|
427
475
|
});
|
|
428
476
|
|
|
429
477
|
result.html = this.formatHtmlForEmail($.html());
|
|
430
|
-
result.plaintext = htmlToPlaintext.email(result.html);
|
|
478
|
+
result.plaintext = htmlToPlaintext.email(result.html);
|
|
431
479
|
delete result.post;
|
|
432
480
|
|
|
433
481
|
return result;
|
|
@@ -26,7 +26,7 @@ const sanitizeKeys = (obj, keys) => {
|
|
|
26
26
|
module.exports = ({post, site, newsletter, templateSettings}) => {
|
|
27
27
|
const date = new Date();
|
|
28
28
|
const hasFeatureImageCaption = templateSettings.showFeatureImage && post.feature_image && post.feature_image_caption;
|
|
29
|
-
const cleanPost = sanitizeKeys(post, ['url', 'published_at', 'title', 'excerpt', 'authors', 'feature_image', 'feature_image_alt'
|
|
29
|
+
const cleanPost = sanitizeKeys(post, ['url', 'published_at', 'title', 'excerpt', 'authors', 'feature_image', 'feature_image_alt']);
|
|
30
30
|
const cleanSite = sanitizeKeys(site, ['title']);
|
|
31
31
|
const cleanNewsletter = sanitizeKeys(newsletter, ['name']);
|
|
32
32
|
|
|
@@ -13,7 +13,6 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
|
|
13
13
|
const urlUtils = require('../../../shared/url-utils');
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
|
-
const staffService = require('../staff');
|
|
17
16
|
const newslettersService = require('../newsletters');
|
|
18
17
|
const memberAttributionService = require('../member-attribution');
|
|
19
18
|
|
|
@@ -198,7 +197,6 @@ function createApiInstance(config) {
|
|
|
198
197
|
},
|
|
199
198
|
stripeAPIService: stripeService.api,
|
|
200
199
|
offersAPI: offersService.api,
|
|
201
|
-
staffService: staffService.api,
|
|
202
200
|
labsService: labsService,
|
|
203
201
|
newslettersService: newslettersService,
|
|
204
202
|
memberAttributionService: memberAttributionService.service
|
|
@@ -8,11 +8,12 @@ const messages = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
class PostsService {
|
|
11
|
-
constructor({mega, urlUtils, models, isSet}) {
|
|
11
|
+
constructor({mega, urlUtils, models, isSet, stats}) {
|
|
12
12
|
this.mega = mega;
|
|
13
13
|
this.urlUtils = urlUtils;
|
|
14
14
|
this.models = models;
|
|
15
15
|
this.isSet = isSet;
|
|
16
|
+
this.stats = stats;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async editPost(frame) {
|
|
@@ -76,20 +77,6 @@ class PostsService {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
/**
|
|
80
|
-
* Returns the most recently `published_at` post that was published or sent
|
|
81
|
-
* via email
|
|
82
|
-
*/
|
|
83
|
-
async getMostRecentlyPublishedPost() {
|
|
84
|
-
const recentlyPublishedPost = await this.models.Post.findPage({
|
|
85
|
-
status: ['published', 'sent'],
|
|
86
|
-
order: 'published_at DESC',
|
|
87
|
-
limit: 1
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return recentlyPublishedPost?.data[0];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
80
|
/**
|
|
94
81
|
* Calculates if the email should be tried to be sent out
|
|
95
82
|
* @private
|
|
@@ -135,12 +122,16 @@ const getPostServiceInstance = () => {
|
|
|
135
122
|
const {mega} = require('../mega');
|
|
136
123
|
const labs = require('../../../shared/labs');
|
|
137
124
|
const models = require('../../models');
|
|
125
|
+
const PostStats = require('./stats/post-stats');
|
|
126
|
+
|
|
127
|
+
const postStats = new PostStats();
|
|
138
128
|
|
|
139
129
|
return new PostsService({
|
|
140
130
|
mega: mega,
|
|
141
131
|
urlUtils: urlUtils,
|
|
142
132
|
models: models,
|
|
143
|
-
isSet: labs.isSet.bind(labs)
|
|
133
|
+
isSet: labs.isSet.bind(labs),
|
|
134
|
+
stats: postStats
|
|
144
135
|
});
|
|
145
136
|
};
|
|
146
137
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const db = require('../../../data/db');
|
|
2
|
+
class PostStats {
|
|
3
|
+
#db;
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.#db = db;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the most recently `published_at` post that was published or sent
|
|
11
|
+
* via email
|
|
12
|
+
*/
|
|
13
|
+
async getMostRecentlyPublishedPostDate() {
|
|
14
|
+
const result = await this.#db.knex.select('published_at')
|
|
15
|
+
.from('posts')
|
|
16
|
+
.whereIn('status', ['sent', 'published'])
|
|
17
|
+
.orderBy('published_at', 'desc')
|
|
18
|
+
.limit(1);
|
|
19
|
+
|
|
20
|
+
return result?.[0]?.published_at ? new Date(result?.[0]?.published_at) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetches count of all published posts
|
|
25
|
+
*/
|
|
26
|
+
async getTotalPostsPublished() {
|
|
27
|
+
const [result] = await this.#db.knex('posts')
|
|
28
|
+
.whereIn('status', ['sent', 'published'])
|
|
29
|
+
.count('id', {as: 'total'});
|
|
30
|
+
|
|
31
|
+
return result.total;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = PostStats;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
1
2
|
class StaffServiceWrapper {
|
|
2
3
|
init() {
|
|
4
|
+
if (this.api) {
|
|
5
|
+
// Prevent creating duplicate DomainEvents subscribers
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
const StaffService = require('@tryghost/staff-service');
|
|
4
10
|
|
|
5
11
|
const logging = require('@tryghost/logging');
|
|
@@ -16,8 +22,11 @@ class StaffServiceWrapper {
|
|
|
16
22
|
mailer,
|
|
17
23
|
settingsHelpers,
|
|
18
24
|
settingsCache,
|
|
19
|
-
urlUtils
|
|
25
|
+
urlUtils,
|
|
26
|
+
DomainEvents
|
|
20
27
|
});
|
|
28
|
+
|
|
29
|
+
this.api.subscribeEvents();
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -12,6 +12,7 @@ module.exports = [
|
|
|
12
12
|
exclude: [
|
|
13
13
|
'title',
|
|
14
14
|
'mobiledoc',
|
|
15
|
+
'lexical',
|
|
15
16
|
'html',
|
|
16
17
|
'plaintext',
|
|
17
18
|
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
|
|
@@ -55,6 +56,7 @@ module.exports = [
|
|
|
55
56
|
exclude: [
|
|
56
57
|
'title',
|
|
57
58
|
'mobiledoc',
|
|
59
|
+
'lexical',
|
|
58
60
|
'html',
|
|
59
61
|
'plaintext',
|
|
60
62
|
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
|
|
@@ -153,8 +153,8 @@
|
|
|
153
153
|
"version": "0.10.1"
|
|
154
154
|
},
|
|
155
155
|
"editor": {
|
|
156
|
-
"url": "https://unpkg.com/@tryghost/koenig-
|
|
157
|
-
"
|
|
156
|
+
"url": "https://unpkg.com/@tryghost/koenig-lexical@~{version}/dist/koenig-lexical.umd.js",
|
|
157
|
+
"version": "0.0"
|
|
158
158
|
},
|
|
159
159
|
"tenor": {
|
|
160
160
|
"googleApiKey": null,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/webp"]
|
|
31
31
|
},
|
|
32
32
|
"media": {
|
|
33
|
-
"extensions": [".mp4",".webm", ".ogv", ".mp3", ".wav", ".ogg"],
|
|
33
|
+
"extensions": [".mp4",".webm", ".ogv", ".mp3", ".wav", ".ogg", ".m4a"],
|
|
34
34
|
"contentTypes": [
|
|
35
35
|
"video/mp4",
|
|
36
36
|
"video/webm",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"audio/wave",
|
|
41
41
|
"audio/wav",
|
|
42
42
|
"audio/x-wav",
|
|
43
|
-
"audio/ogg"
|
|
43
|
+
"audio/ogg",
|
|
44
|
+
"audio/x-m4a"
|
|
44
45
|
]
|
|
45
46
|
},
|
|
46
47
|
"thumbnails": {
|
package/core/shared/labs.js
CHANGED
|
@@ -15,7 +15,6 @@ const messages = {
|
|
|
15
15
|
|
|
16
16
|
// flags in this list always return `true`, allows quick global enable prior to full flag removal
|
|
17
17
|
const GA_FEATURES = [
|
|
18
|
-
'auditLog',
|
|
19
18
|
'newsletterPaywall',
|
|
20
19
|
'freeTrial',
|
|
21
20
|
'compExpiring',
|
|
@@ -32,7 +31,10 @@ const BETA_FEATURES = [
|
|
|
32
31
|
|
|
33
32
|
const ALPHA_FEATURES = [
|
|
34
33
|
'urlCache',
|
|
35
|
-
'beforeAfterCard'
|
|
34
|
+
'beforeAfterCard',
|
|
35
|
+
'emailClicks',
|
|
36
|
+
'sourceAttribution',
|
|
37
|
+
'lexicalEditor'
|
|
36
38
|
];
|
|
37
39
|
|
|
38
40
|
module.exports.GA_KEYS = [...GA_FEATURES];
|