ghost 5.129.2 → 5.130.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.
Files changed (35) hide show
  1. package/components/tryghost-i18n-5.130.0.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-B12913rO.mjs → index-BhgdXgH_.mjs} +2 -2
  4. package/core/built/admin/assets/admin-x-activitypub/{index-B7EmcyVj.mjs → index-rDFm98Ub.mjs} +15498 -15418
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-l2Ex2555.mjs → CodeEditorView-bO8i1M7l.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-C6P_16OJ.mjs → index-BeD9DTp3.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/index-DIak5kz8.mjs +30462 -0
  9. package/core/built/admin/assets/admin-x-settings/{modals-CY1xx4Em.mjs → modals-DLPpqlUq.mjs} +7537 -7543
  10. package/core/built/admin/assets/{chunk.524.c8313bccd308920abf9c.js → chunk.524.2443bfd380e6da0cbabd.js} +8 -8
  11. package/core/built/admin/assets/{chunk.582.e4feab981886cfc91835.js → chunk.582.434476dff5ddc79ed054.js} +10 -10
  12. package/core/built/admin/assets/{ghost-db9fcb8c1f65776f3ee11c39f19a660b.js → ghost-5d9c65b5c4ef960a664cd664b2616dea.js} +4 -4
  13. package/core/built/admin/assets/posts/posts.js +6641 -6621
  14. package/core/built/admin/assets/stats/stats.js +12770 -12724
  15. package/core/built/admin/index.html +3 -3
  16. package/core/frontend/helpers/match.js +3 -0
  17. package/core/frontend/meta/schema.js +19 -0
  18. package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
  19. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +3 -1
  20. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +3 -1
  21. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  22. package/core/server/data/migrations/versions/5.130/2025-07-11-14-14-54-add-explore-settings.js +16 -0
  23. package/core/server/data/schema/default-settings/default-settings.json +18 -0
  24. package/core/server/services/activitypub/ActivityPubService.js +22 -2
  25. package/core/server/services/activitypub/ActivityPubService.ts +26 -6
  26. package/core/server/services/email-service/email-templates/partials/styles.hbs +0 -14
  27. package/core/server/services/explore-ping/ExplorePingService.js +44 -33
  28. package/core/server/services/public-config/config.js +4 -0
  29. package/core/server/services/themes/installer.js +17 -3
  30. package/core/shared/config/env/config.production.json +4 -0
  31. package/package.json +6 -6
  32. package/tsconfig.tsbuildinfo +1 -1
  33. package/yarn.lock +185 -154
  34. package/components/tryghost-i18n-5.129.2.tgz +0 -0
  35. package/core/built/admin/assets/admin-x-settings/index-DoLRADbr.mjs +0 -30308
@@ -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%2262b55e2aae%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%226d96d71fe8%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%223ad9cd1892%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%22701e8fc366%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" />
@@ -49,7 +49,7 @@
49
49
 
50
50
  <script src="assets/vendor-c89102f24c3d9502e9db741509767580.js"></script>
51
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>
52
+ <script src="assets/chunk.524.2443bfd380e6da0cbabd.js"></script>
53
+ <script src="assets/ghost-5d9c65b5c4ef960a664cd664b2616dea.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,
@@ -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
  }
@@ -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,6 +25,10 @@ module.exports = function getConfigProperties() {
25
25
  security: config.get('security')
26
26
  };
27
27
 
28
+ if (config.get('explore') && config.get('explore:testimonials_url')) {
29
+ configProperties.exploreTestimonialsUrl = config.get('explore:testimonials_url');
30
+ }
31
+
28
32
  // WIP tinybird stats feature - it's entirely config driven instead of using an alpha flag for now
29
33
  if (config.get('tinybird') && config.get('tinybird:stats')) {
30
34
  const statsConfig = config.get('tinybird:stats');
@@ -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
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "5.129.2",
3
+ "version": "5.130.0",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -86,7 +86,7 @@
86
86
  "@tryghost/helpers": "1.1.97",
87
87
  "@tryghost/html-to-plaintext": "1.0.4",
88
88
  "@tryghost/http-cache-utils": "0.1.20",
89
- "@tryghost/i18n": "file:components/tryghost-i18n-5.129.2.tgz",
89
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.130.0.tgz",
90
90
  "@tryghost/image-transform": "1.4.6",
91
91
  "@tryghost/job-manager": "1.0.3",
92
92
  "@tryghost/kg-card-factory": "5.1.2",
@@ -226,21 +226,21 @@
226
226
  },
227
227
  "devDependencies": {
228
228
  "@actions/core": "1.11.1",
229
- "@playwright/test": "1.53.2",
229
+ "@playwright/test": "1.54.1",
230
230
  "@prettier/sync": "0.6.1",
231
231
  "@tryghost/express-test": "0.15.0",
232
232
  "@tryghost/webhook-mock-receiver": "0.2.17",
233
233
  "@types/bookshelf": "1.2.9",
234
234
  "@types/common-tags": "1.8.4",
235
235
  "@types/jsonwebtoken": "9.0.10",
236
- "@types/node": "22.16.3",
236
+ "@types/node": "22.16.4",
237
237
  "@types/node-jose": "1.1.13",
238
238
  "@types/nodemailer": "6.4.17",
239
239
  "@types/sinon": "17.0.4",
240
240
  "@types/supertest": "6.0.3",
241
241
  "c8": "10.1.3",
242
242
  "cli-progress": "3.12.0",
243
- "cssnano": "7.0.7",
243
+ "cssnano": "7.1.0",
244
244
  "detect-indent": "6.1.0",
245
245
  "detect-newline": "3.1.0",
246
246
  "expect": "29.7.0",
@@ -274,7 +274,7 @@
274
274
  "jackspeak": "2.3.6",
275
275
  "moment": "2.24.0",
276
276
  "moment-timezone": "0.5.45",
277
- "@tryghost/i18n": "file:components/tryghost-i18n-5.129.2.tgz"
277
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.130.0.tgz"
278
278
  },
279
279
  "nx": {
280
280
  "targets": {