ghost 5.129.2 → 5.130.1

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.
Files changed (45) hide show
  1. package/components/tryghost-i18n-5.130.1.tgz +0 -0
  2. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
  3. package/core/built/admin/assets/admin-x-activitypub/{index-B7EmcyVj.mjs → index-B8te98RZ.mjs} +26908 -20750
  4. package/core/built/admin/assets/admin-x-activitypub/{index-B12913rO.mjs → index-C8qwgKWF.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-l2Ex2555.mjs → CodeEditorView-DaQbEVUf.mjs} +2 -2
  6. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  7. package/core/built/admin/assets/admin-x-settings/index-Cmiy56KV.mjs +30462 -0
  8. package/core/built/admin/assets/admin-x-settings/{index-C6P_16OJ.mjs → index-zBk55R7z.mjs} +2 -2
  9. package/core/built/admin/assets/admin-x-settings/{modals-CY1xx4Em.mjs → modals-tqVrPtPy.mjs} +2418 -2416
  10. package/core/built/admin/assets/{chunk.524.c8313bccd308920abf9c.js → chunk.524.8a4cbb5b8ae5cf01697e.js} +7 -7
  11. package/core/built/admin/assets/{chunk.582.e4feab981886cfc91835.js → chunk.582.7b14e9ac2e84d285035e.js} +8 -8
  12. package/core/built/admin/assets/{chunk.728.214803966b81ffdb1acd.js → chunk.728.077782a432061228b91e.js} +141 -141
  13. package/core/built/admin/assets/{ghost-db9fcb8c1f65776f3ee11c39f19a660b.js → ghost-280b83af263b51bc4d6ce5bd8f536096.js} +48 -55
  14. package/core/built/admin/assets/posts/posts.js +21698 -21753
  15. package/core/built/admin/assets/stats/stats.js +21710 -21735
  16. package/core/built/admin/assets/{vendor-c89102f24c3d9502e9db741509767580.js → vendor-aed0068cf9b67d042dd23a6343545b7b.js} +1 -1
  17. package/core/built/admin/index.html +5 -5
  18. package/core/frontend/helpers/match.js +3 -0
  19. package/core/frontend/meta/schema.js +19 -0
  20. package/core/frontend/web/middleware/frontend-caching.js +6 -1
  21. package/core/frontend/web/middleware/static-theme.js +3 -5
  22. package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
  23. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +3 -1
  24. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +3 -1
  25. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  26. package/core/server/data/migrations/versions/5.130/2025-07-11-14-14-54-add-explore-settings.js +16 -0
  27. package/core/server/data/schema/default-settings/default-settings.json +18 -0
  28. package/core/server/data/tinybird/README.md +9 -14
  29. package/core/server/services/activitypub/ActivityPubService.js +22 -2
  30. package/core/server/services/activitypub/ActivityPubService.ts +26 -6
  31. package/core/server/services/email-service/email-templates/partials/styles.hbs +0 -14
  32. package/core/server/services/explore-ping/ExplorePingService.js +44 -33
  33. package/core/server/services/public-config/config.js +4 -1
  34. package/core/server/services/themes/installer.js +17 -3
  35. package/core/server/web/admin/app.js +5 -6
  36. package/core/server/web/shared/middleware/cache-control.js +2 -1
  37. package/core/shared/config/defaults.json +6 -0
  38. package/core/shared/config/env/config.production.json +4 -0
  39. package/package.json +12 -12
  40. package/tsconfig.tsbuildinfo +1 -1
  41. package/yarn.lock +325 -249
  42. package/components/tryghost-i18n-5.129.2.tgz +0 -0
  43. package/core/built/admin/assets/admin-x-settings/index-DoLRADbr.mjs +0 -30308
  44. package/core/built/admin/assets/img/twitter-7a7a0ba12d9b5bfb8a2058764a827c31.svg +0 -4
  45. /package/core/built/admin/assets/{chunk.728.214803966b81ffdb1acd.js.LICENSE.txt → chunk.728.077782a432061228b91e.js.LICENSE.txt} +0 -0
@@ -9553,4 +9553,4 @@ e.default=class{constructor(e){if(this._data=new t.default,e)for(let t=0;t<e.len
9553
9553
  return this}get(e){let t=this._data[e]
9554
9554
  return t===r.UNDEFINED_KEY?void 0:t}set(e,t){return this._data[e]=t,this}delete(e){return this._data[e]=r.UNDEFINED_KEY,!0}}})
9555
9555
 
9556
- //# sourceMappingURL=vendor-1552b6fd843e2f460183ad9424be2a2d.map
9556
+ //# sourceMappingURL=vendor-326b46cbc2845d47f1e0af43ba21caec.map
@@ -6,7 +6,7 @@
6
6
  <title>Ghost</title>
7
7
 
8
8
 
9
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.129%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22657b852ebf%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%2240f7252084%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22be74778cd8%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%222dcb5663ba%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
9
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.130%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2237bd1e3e4d%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22575f27df11%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22b38d27bb5e%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%229a7e1b885b%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%225445ed6ca9%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
10
10
 
11
11
  <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover" />
12
12
  <meta name="pinterest" content="nopin" />
@@ -47,9 +47,9 @@
47
47
 
48
48
  <div id="ember-basic-dropdown-wormhole"></div>
49
49
 
50
- <script src="assets/vendor-c89102f24c3d9502e9db741509767580.js"></script>
51
- <script src="assets/chunk.728.214803966b81ffdb1acd.js"></script>
52
- <script src="assets/chunk.524.c8313bccd308920abf9c.js"></script>
53
- <script src="assets/ghost-db9fcb8c1f65776f3ee11c39f19a660b.js"></script>
50
+ <script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
51
+ <script src="assets/chunk.728.077782a432061228b91e.js"></script>
52
+ <script src="assets/chunk.524.8a4cbb5b8ae5cf01697e.js"></script>
53
+ <script src="assets/ghost-280b83af263b51bc4d6ce5bd8f536096.js"></script>
54
54
  </body>
55
55
  </html>
@@ -57,6 +57,9 @@ const handleMatch = (data, operator, value) => {
57
57
  case '<=':
58
58
  result = data <= value;
59
59
  break;
60
+ case '~':
61
+ result = _.isString(data) && _.isString(value) && data.includes(value);
62
+ break;
60
63
  case '~^':
61
64
  result = _.isString(data) && _.isString(value) && data.startsWith(value);
62
65
  break;
@@ -78,6 +78,24 @@ function trimSameAs(author) {
78
78
  return sameAs;
79
79
  }
80
80
 
81
+ /**
82
+ * Build contributor objects for schema.org Article schema.
83
+ *
84
+ * @param {Object[]} authors - Array of author objects (excluding primary author)
85
+ */
86
+ function buildContributorObjects(authors) {
87
+ return authors.map(author => trimSchema({
88
+ '@type': 'Person',
89
+ name: escapeExpression(author.name),
90
+ image: author.profile_image ? schemaImageObject({url: author.profile_image}) : null,
91
+ url: author.url || null,
92
+ sameAs: trimSameAs(author),
93
+ description: author.meta_description ?
94
+ escapeExpression(author.meta_description) :
95
+ null
96
+ }));
97
+ }
98
+
81
99
  function getPostSchema(metaData, data) {
82
100
  // CASE: metaData.excerpt for post context is populated by either the custom excerpt, the meta description,
83
101
  // or the automated excerpt of 50 words. It is empty for any other context.
@@ -101,6 +119,7 @@ function getPostSchema(metaData, data) {
101
119
  escapeExpression(data[context].primary_author.metaDescription) :
102
120
  null
103
121
  },
122
+ contributor: data[context].authors && data[context].authors.length > 1 ? buildContributorObjects(data[context].authors.slice(1)) : null,
104
123
  headline: escapeExpression(metaData.metaTitle),
105
124
  url: metaData.url,
106
125
  datePublished: metaData.publishedDate,
@@ -51,6 +51,11 @@ const getMiddleware = async (getFreeTier = async () => {
51
51
  return shared.middleware.cacheControl('private')(req, res, next);
52
52
  }
53
53
 
54
+ // CASE: Never cache preview routes
55
+ if (req.path?.startsWith('/p/')) {
56
+ return shared.middleware.cacheControl('noCache')(req, res, next);
57
+ }
58
+
54
59
  // CASE: Cache member's content if this feature is enabled
55
60
  if (req.member && shouldCacheMembersContent) {
56
61
  // Set the 'cache-control' header to 'public'
@@ -74,4 +79,4 @@ const getMiddleware = async (getFreeTier = async () => {
74
79
  module.exports = {
75
80
  getMiddleware,
76
81
  calculateMemberTier // exported for testing
77
- };
82
+ };
@@ -59,12 +59,10 @@ function forwardToExpressStatic(req, res, next) {
59
59
  return next();
60
60
  }
61
61
 
62
- const configMaxAge = config.get('caching:theme:maxAge');
63
-
64
- // @NOTE: the maxAge config passed below are in milliseconds and the config
65
- // is specified in seconds. See https://github.com/expressjs/serve-static/issues/150 for more context
66
62
  express.static(themeEngine.getActive().path, {
67
- maxAge: (configMaxAge || configMaxAge === 0) ? configMaxAge : (365 * 24 * 60 * 60 * 1000) // Default to 1 year in ms
63
+ // @NOTE: the maxAge config passed below are in milliseconds and the config
64
+ // is specified in seconds. See https://github.com/expressjs/serve-static/issues/150 for more context
65
+ maxAge: config.get('caching:theme:maxAge') * 1000
68
66
  }
69
67
  )(req, res, next);
70
68
  }
@@ -78,7 +78,9 @@ const EDITABLE_SETTINGS = [
78
78
  'heading_font',
79
79
  'blocked_email_domains',
80
80
  'require_email_mfa',
81
- 'social_web'
81
+ 'social_web',
82
+ 'explore_ping',
83
+ 'explore_ping_growth'
82
84
  ];
83
85
 
84
86
  module.exports = {
@@ -48,7 +48,9 @@ const keyGroupMapping = {
48
48
  portal_name: 'portal',
49
49
  portal_button: 'portal',
50
50
  portal_plans: 'portal',
51
- require_email_mfa: 'security'
51
+ require_email_mfa: 'security',
52
+ explore_ping: 'explore',
53
+ explore_ping_growth: 'explore'
52
54
  };
53
55
 
54
56
  const mapKeyToGroup = (key) => {
@@ -55,7 +55,9 @@ const keyTypeMapping = {
55
55
  labs: 'object',
56
56
  unsplash: 'object',
57
57
  bulk_email_settings: 'object',
58
- require_email_mfa: 'boolean'
58
+ require_email_mfa: 'boolean',
59
+ explore_ping: 'boolean',
60
+ explore_ping_growth: 'boolean'
59
61
  };
60
62
 
61
63
  const mapKeyToType = (key) => {
@@ -22,7 +22,8 @@ module.exports = {
22
22
  'pintura',
23
23
  'signupForm',
24
24
  'stats',
25
- 'security'
25
+ 'security',
26
+ 'exploreTestimonialsUrl'
26
27
  ];
27
28
 
28
29
  frame.response = {
@@ -0,0 +1,16 @@
1
+ const {combineTransactionalMigrations, addSetting} = require('../../utils');
2
+
3
+ module.exports = combineTransactionalMigrations(
4
+ addSetting({
5
+ key: 'explore_ping',
6
+ value: 'true',
7
+ type: 'boolean',
8
+ group: 'explore'
9
+ }),
10
+ addSetting({
11
+ key: 'explore_ping_growth',
12
+ value: 'false',
13
+ type: 'boolean',
14
+ group: 'explore'
15
+ })
16
+ );
@@ -628,5 +628,23 @@
628
628
  },
629
629
  "type": "boolean"
630
630
  }
631
+ },
632
+ "explore": {
633
+ "explore_ping": {
634
+ "defaultValue": "true",
635
+ "validations": {
636
+ "isEmpty": false,
637
+ "isIn": [["true", "false"]]
638
+ },
639
+ "type": "boolean"
640
+ },
641
+ "explore_ping_growth": {
642
+ "defaultValue": "false",
643
+ "validations": {
644
+ "isEmpty": false,
645
+ "isIn": [["true", "false"]]
646
+ },
647
+ "type": "boolean"
648
+ }
631
649
  }
632
650
  }
@@ -31,7 +31,7 @@ with the following information.
31
31
 
32
32
  ### Config
33
33
  Sample config:
34
- ```json
34
+ ```jsonc
35
35
  {
36
36
  "someOtherConfigurationForEmail": {
37
37
  "transport": "SMTP",
@@ -40,25 +40,20 @@ Sample config:
40
40
  }
41
41
  },
42
42
  "tinybird": {
43
+ "workspaceId": "workspace-id-from-tinybird",
44
+ "adminToken": "admin-token-from-tinybird",
43
45
  "tracker": {
44
- "endpoint": "https://e.ghost.org/tb/web_analytics",
45
- "token": "xxxxx",
46
- "datasource": "analytics_events",
47
- "local": {
48
- "enabled": true,
49
- "token": "xxxxx",
50
- "endpoint": "http://localhost:7181/v0/events",
51
- "datasource": "analytics_events"
52
- }
46
+ // -- needs to be present, and required Traffic Analytics service running with correct setup
47
+ "endpoint": "http://localhost:3000/tb/web_analytics"
53
48
  },
54
49
  "stats": {
50
+ // -- optional override for site uuid
51
+ // "id": "106a623d-9792-4b63-acde-4a0c28ead3dc",
55
52
  "endpoint": "https://api.tinybird.co",
56
- "token": "xxxxx",
53
+ // -- tinybird local configuration (optional)
57
54
  "local": {
58
55
  "enabled": true,
59
- "token": "xxxxx",
60
- "endpoint": "http://localhost:7181",
61
- "datasource": "analytics_events"
56
+ "token": "local-stats-or-admin-token",
62
57
  }
63
58
  }
64
59
  }
@@ -18,12 +18,32 @@ class ActivityPubService {
18
18
  this.identityTokenService = identityTokenService;
19
19
  }
20
20
  getExpectedWebhooks(secret) {
21
- return [{
21
+ return [
22
+ {
22
23
  event: 'post.published',
23
24
  target_url: new URL('.ghost/activitypub/v1/webhooks/post/published', this.siteUrl),
24
25
  api_version: 'v5.100.0',
25
26
  secret
26
- }];
27
+ },
28
+ {
29
+ event: 'post.deleted',
30
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/deleted', this.siteUrl),
31
+ api_version: 'v5.100.0',
32
+ secret
33
+ },
34
+ {
35
+ event: 'post.unpublished',
36
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/unpublished', this.siteUrl),
37
+ api_version: 'v5.100.0',
38
+ secret
39
+ },
40
+ {
41
+ event: 'post.published.edited',
42
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/updated', this.siteUrl),
43
+ api_version: 'v5.100.0',
44
+ secret
45
+ }
46
+ ];
27
47
  }
28
48
  async checkWebhookState(expectedWebhooks, integration) {
29
49
  this.logging.info(`Checking ActivityPub Webhook state`);
@@ -25,12 +25,32 @@ export class ActivityPubService {
25
25
  ) {}
26
26
 
27
27
  getExpectedWebhooks(secret: string): ExpectedWebhook[] {
28
- return [{
29
- event: 'post.published',
30
- target_url: new URL('.ghost/activitypub/v1/webhooks/post/published', this.siteUrl),
31
- api_version: 'v5.100.0',
32
- secret
33
- }];
28
+ return [
29
+ {
30
+ event: 'post.published',
31
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/published', this.siteUrl),
32
+ api_version: 'v5.100.0',
33
+ secret
34
+ },
35
+ {
36
+ event: 'post.deleted',
37
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/deleted', this.siteUrl),
38
+ api_version: 'v5.100.0',
39
+ secret
40
+ },
41
+ {
42
+ event: 'post.unpublished',
43
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/unpublished', this.siteUrl),
44
+ api_version: 'v5.100.0',
45
+ secret
46
+ },
47
+ {
48
+ event: 'post.published.edited',
49
+ target_url: new URL('.ghost/activitypub/v1/webhooks/post/updated', this.siteUrl),
50
+ api_version: 'v5.100.0',
51
+ secret
52
+ }
53
+ ];
34
54
  }
35
55
 
36
56
  async checkWebhookState(expectedWebhooks: ExpectedWebhook[], integration: {id: string}) {
@@ -381,7 +381,6 @@ h6 + .kg-paywall .kg-paywall-hr td {
381
381
  }
382
382
 
383
383
  /* Exclude CTA cards with colored backgrounds from custom text color, but allow transparent ones */
384
- {{#hasFeature "emailCustomization"}}
385
384
  {{#each ctaBgColors}}
386
385
  .post-content-row .kg-cta-bg-{{this}} .kg-cta-text p,
387
386
  .post-content-row .kg-cta-bg-{{this}} .kg-cta-text ul,
@@ -391,17 +390,6 @@ h6 + .kg-paywall .kg-paywall-hr td {
391
390
  color: inherit !important;
392
391
  }
393
392
  {{/each}}
394
- {{else}}
395
- .post-content-row .kg-cta-bg-grey .kg-cta-text p,
396
- .post-content-row .kg-cta-bg-blue .kg-cta-text p,
397
- .post-content-row .kg-cta-bg-green .kg-cta-text p,
398
- .post-content-row .kg-cta-bg-yellow .kg-cta-text p,
399
- .post-content-row .kg-cta-bg-red .kg-cta-text p,
400
- .post-content-row .kg-cta-bg-pink .kg-cta-text p,
401
- .post-content-row .kg-cta-bg-purple .kg-cta-text p {
402
- color: inherit !important;
403
- }
404
- {{/hasFeature}}
405
393
 
406
394
  .kg-cta-bg-none .kg-cta-sponsor-label span,
407
395
  .kg-cta-bg-white .kg-cta-sponsor-label span {
@@ -2542,10 +2530,8 @@ table.btn-accent a {
2542
2530
  {{/if}}
2543
2531
 
2544
2532
  </style>
2545
- {{#hasFeature "emailCustomization"}}
2546
2533
  <!--[if mso]>
2547
2534
  <style type="text/css">
2548
2535
  ul, ol { margin-left: 1.5em !important; } {{!-- fix bullets/numbers not appearing for lists in older Outlook versions --}}
2549
2536
  </style>
2550
2537
  <![endif]-->
2551
- {{/hasFeature}}
@@ -1,7 +1,7 @@
1
1
  module.exports = class ExplorePingService {
2
2
  /**
3
3
  * @param {object} deps
4
- * @param {{getPublic: () => import('../../../shared/settings-cache/CacheManager').PublicSettingsCache}} deps.settingsCache
4
+ * @param {{get: (string) => string}} deps.settingsCache
5
5
  * @param {object} deps.config
6
6
  * @param {object} deps.labs
7
7
  * @param {object} deps.logging
@@ -14,6 +14,7 @@ module.exports = class ExplorePingService {
14
14
  * }}} deps.posts
15
15
  * @param {{stats: {
16
16
  * getTotalMembers: () => Promise<number>
17
+ * getMRRHistory: () => Promise<number>
17
18
  * }}} deps.members
18
19
  */
19
20
  constructor({settingsCache, config, labs, logging, ghostVersion, request, posts, members}) {
@@ -28,45 +29,50 @@ module.exports = class ExplorePingService {
28
29
  }
29
30
 
30
31
  async constructPayload() {
31
- /* eslint-disable camelcase */
32
- const {title, description, icon, locale, accent_color, twitter, facebook, site_uuid} = this.settingsCache.getPublic();
33
-
34
- // Get post statistics
35
- const [totalPosts, lastPublishedAt, firstPublishedAt] = await Promise.all([
36
- this.posts.stats.getTotalPostsPublished(),
37
- this.posts.stats.getMostRecentlyPublishedPostDate(),
38
- this.posts.stats.getFirstPublishedPostDate()
39
- ]);
32
+ const payload = {
33
+ ghost: this.ghostVersion.full,
34
+ site_uuid: this.settingsCache.get('site_uuid'),
35
+ url: this.config.get('url'),
36
+ theme: this.settingsCache.get('active_theme')
37
+ };
40
38
 
41
- // Get member statistics with error handling
42
- let totalMembers = null;
43
39
  try {
44
- totalMembers = await this.members.stats.getTotalMembers();
40
+ const [totalPosts, lastPublishedAt, firstPublishedAt] = await Promise.all([
41
+ this.posts.stats.getTotalPostsPublished(),
42
+ this.posts.stats.getMostRecentlyPublishedPostDate(),
43
+ this.posts.stats.getFirstPublishedPostDate()
44
+ ]);
45
+
46
+ payload.posts_total = totalPosts;
47
+ payload.posts_last = lastPublishedAt ? lastPublishedAt.toISOString() : null;
48
+ payload.posts_first = firstPublishedAt ? firstPublishedAt.toISOString() : null;
45
49
  } catch (err) {
46
- this.logging.warn('Failed to fetch member statistics', {
50
+ this.logging.warn('Failed to fetch post statistics', {
47
51
  error: err.message,
48
52
  context: 'explore-ping-service'
49
53
  });
50
- // Continue without member statistics
54
+ payload.posts_total = null;
55
+ payload.posts_last = null;
56
+ payload.posts_first = null;
51
57
  }
52
58
 
53
- return {
54
- ghost: this.ghostVersion.full,
55
- site_uuid,
56
- url: this.config.get('url'),
57
- title,
58
- description,
59
- icon,
60
- locale,
61
- accent_color,
62
- twitter,
63
- facebook,
64
- posts_first: firstPublishedAt ? firstPublishedAt.toISOString() : null,
65
- posts_last: lastPublishedAt ? lastPublishedAt.toISOString() : null,
66
- posts_total: totalPosts,
67
- members_total: totalMembers
68
- };
69
- /* eslint-enable camelcase */
59
+ if (this.settingsCache.get('explore_ping_growth')) {
60
+ try {
61
+ const totalMembers = await this.members.stats.getTotalMembers();
62
+ const mrr = await this.members.stats.getMRRHistory();
63
+ payload.members_total = totalMembers;
64
+ payload.mrr = mrr;
65
+ } catch (err) {
66
+ this.logging.warn('Failed to fetch member statistics', {
67
+ error: err.message,
68
+ context: 'explore-ping-service'
69
+ });
70
+ payload.members_total = null;
71
+ payload.mrr = null;
72
+ }
73
+ }
74
+
75
+ return payload;
70
76
  }
71
77
 
72
78
  async makeRequest(exploreUrl, payload) {
@@ -95,12 +101,17 @@ module.exports = class ExplorePingService {
95
101
  return;
96
102
  }
97
103
 
98
- const exploreUrl = this.config.get('explore:url');
104
+ const exploreUrl = this.config.get('explore:update_url');
99
105
  if (!exploreUrl) {
100
106
  this.logging.warn('Explore URL not set');
101
107
  return;
102
108
  }
103
109
 
110
+ if (!this.settingsCache.get('explore_ping')) {
111
+ this.logging.info('Explore ping disabled');
112
+ return;
113
+ }
114
+
104
115
  const payload = await this.constructPayload();
105
116
  await this.makeRequest(exploreUrl, payload);
106
117
  }
@@ -25,7 +25,10 @@ module.exports = function getConfigProperties() {
25
25
  security: config.get('security')
26
26
  };
27
27
 
28
- // WIP tinybird stats feature - it's entirely config driven instead of using an alpha flag for now
28
+ if (config.get('explore') && config.get('explore:testimonials_url')) {
29
+ configProperties.exploreTestimonialsUrl = config.get('explore:testimonials_url');
30
+ }
31
+
29
32
  if (config.get('tinybird') && config.get('tinybird:stats')) {
30
33
  const statsConfig = config.get('tinybird:stats');
31
34
  const siteUuid = statsConfig.id || settingsCache.get('site_uuid');
@@ -19,9 +19,18 @@ const messages = {
19
19
  const installFromGithub = async (ref) => {
20
20
  const [org, repo] = ref.toLowerCase().split('/');
21
21
 
22
- //TODO: move the organization check to config
23
- if (limitService.isLimited('customThemes') && org.toLowerCase() !== 'tryghost') {
24
- await limitService.errorIfWouldGoOverLimit('customThemes', {value: repo.toLowerCase()});
22
+ if (limitService.isLimited('customThemes')) {
23
+ // The custom theme limit might consist of only one single theme, so we can't rely on
24
+ // the org alone to determine if the request is allowed or not.
25
+ const noOtherThemesAllowed = limitService.limits.customThemes?.allowlist?.length === 1;
26
+ //TODO: move the organization check to config
27
+ const isNotOfficialThemeRequest = org.toLowerCase() !== 'tryghost';
28
+
29
+ const checkThemeLimit = noOtherThemesAllowed || isNotOfficialThemeRequest;
30
+
31
+ if (checkThemeLimit) {
32
+ await limitService.errorIfWouldGoOverLimit('customThemes', {value: repo.toLowerCase()});
33
+ }
25
34
  }
26
35
 
27
36
  // omit /:ref so we fetch the default branch
@@ -62,6 +71,11 @@ const installFromGithub = async (ref) => {
62
71
  }));
63
72
  }
64
73
 
74
+ if (e instanceof errors.HostLimitError) {
75
+ // If the error is a HostLimitError, we can assume that the theme name is not allowed
76
+ return Promise.reject(e);
77
+ }
78
+
65
79
  throw e;
66
80
  } finally {
67
81
  // clean up tmp dir with downloaded file
@@ -19,16 +19,15 @@ module.exports = function setupAdminApp() {
19
19
  const adminApp = express('admin');
20
20
 
21
21
  // Admin assets
22
- // @TODO ensure this gets a local 404 error handler
23
- const configMaxAge = config.get('caching:admin:maxAge');
24
22
  // @NOTE: when we start working on HTTP/3 optimizations the immutable headers
25
23
  // produced below should be split into separate 'Cache-Control' entry.
26
24
  // For reference see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#validation_2
27
- // @NOTE: the maxAge config passed below are in milliseconds and the config
28
- // is specified in seconds. See https://github.com/expressjs/serve-static/issues/150 for more context
25
+
29
26
  adminApp.use('/assets', serveStatic(
30
27
  path.join(config.get('paths').adminAssets, 'assets'), {
31
- maxAge: (configMaxAge || configMaxAge === 0) ? configMaxAge : (365 * 24 * 60 * 60 * 1000), // Default to 1 year in ms
28
+ // @NOTE: the maxAge config passed below are in milliseconds and the config
29
+ // is specified in seconds. See https://github.com/expressjs/serve-static/issues/150 for more context
30
+ maxAge: config.get('caching:admin:maxAge') * 1000,
32
31
  immutable: true,
33
32
  fallthrough: false
34
33
  }
@@ -94,4 +93,4 @@ module.exports = function setupAdminApp() {
94
93
  debug('Admin setup end');
95
94
 
96
95
  return adminApp;
97
- };
96
+ };
@@ -9,7 +9,7 @@
9
9
  const isString = require('lodash/isString');
10
10
 
11
11
  /**
12
- * @param {'public'|'private'} profile Use "private" if you do not want caching
12
+ * @param {'public'|'private'|'noCache'} profile Use "private" if you do not want caching
13
13
  * @param {object} [options]
14
14
  * @param {number} [options.maxAge] The max-age in seconds to use when profile is "public"
15
15
  * @param {number} [options.staleWhileRevalidate] The stale-while-revalidate in seconds to use when profile is "public"
@@ -24,6 +24,7 @@ const cacheControl = (profile, options = {maxAge: 0}) => {
24
24
 
25
25
  const profiles = {
26
26
  public: publicOptions.filter(option => option).join(', '),
27
+ noCache: 'no-cache, max-age=0, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0',
27
28
  private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
28
29
  };
29
30
 
@@ -168,6 +168,12 @@
168
168
  },
169
169
  "commentsCountAPI": {
170
170
  "maxAge": 0
171
+ },
172
+ "admin": {
173
+ "maxAge": 31536000
174
+ },
175
+ "theme": {
176
+ "maxAge": 31536000
171
177
  }
172
178
  },
173
179
  "optimization": {
@@ -17,5 +17,9 @@
17
17
  "enabled": true
18
18
  },
19
19
  "transports": ["file"]
20
+ },
21
+ "explore": {
22
+ "update_url": "https://explore.ghost.org/api/update",
23
+ "testimonials_url": "https://explore.ghost.org/api/testimonials"
20
24
  }
21
25
  }