ghost 5.121.0 → 5.123.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 (49) hide show
  1. package/components/tryghost-i18n-5.123.0.tgz +0 -0
  2. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +14017 -14035
  3. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-03f7aa4d.mjs → CodeEditorView-aba69ca9.mjs} +2 -2
  4. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  5. package/core/built/admin/assets/admin-x-settings/{index-eea9098a.mjs → index-87586072.mjs} +4 -4
  6. package/core/built/admin/assets/admin-x-settings/{index-b0484a21.mjs → index-b3cfde97.mjs} +2 -2
  7. package/core/built/admin/assets/admin-x-settings/{modals-85a92c66.mjs → modals-890d62ee.mjs} +1669 -1618
  8. package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js → chunk.383.3dcd1102188f9d5e0467.js} +7 -7
  9. package/core/built/admin/assets/{chunk.524.22c1abc09947848be03d.js → chunk.524.f1c003b0bae2aa6a9805.js} +7 -7
  10. package/core/built/admin/assets/{chunk.582.a7cbc2ce76922c8870bc.js → chunk.582.4d677cde396354a18509.js} +9 -9
  11. package/core/built/admin/assets/{ghost-1c91950af9d6f9cee826820f056e0025.js → ghost-4dac0876cd7426ba504e4143b9772689.js} +141 -132
  12. package/core/built/admin/assets/posts/posts.js +71680 -41896
  13. package/core/built/admin/assets/stats/stats.js +35194 -35499
  14. package/core/built/admin/index.html +4 -4
  15. package/core/frontend/public/ghost-stats.min.js +3 -3
  16. package/core/frontend/src/ghost-stats/ghost-stats.js +0 -12
  17. package/core/server/api/endpoints/previews.js +25 -4
  18. package/core/server/api/endpoints/stats.js +50 -0
  19. package/core/server/data/migrations/versions/5.122/2025-06-03-19-32-57-change-default-for-newsletters-button-color.js +37 -0
  20. package/core/server/data/schema/schema.js +1 -1
  21. package/core/server/data/tinybird/endpoints/api_top_pages.pipe +1 -0
  22. package/core/server/data/tinybird/fixtures/analytics_events.ndjson +31 -31
  23. package/core/server/data/tinybird/pipes/mv_hits.pipe +51 -11
  24. package/core/server/data/tinybird/tests/api_top_pages.yaml +7 -0
  25. package/core/server/models/newsletter.js +1 -0
  26. package/core/server/services/adapter-manager/AdapterManager.js +5 -1
  27. package/core/server/services/comments/CommentsServiceEmails.js +8 -6
  28. package/core/server/services/email-service/EmailRenderer.js +80 -23
  29. package/core/server/services/email-service/email-templates/partials/paywall.hbs +56 -33
  30. package/core/server/services/email-service/email-templates/partials/styles.hbs +129 -4
  31. package/core/server/services/email-service/email-templates/template.hbs +1 -1
  32. package/core/server/services/koenig/node-renderers/button-renderer.js +1 -21
  33. package/core/server/services/koenig/node-renderers/call-to-action-renderer.js +10 -44
  34. package/core/server/services/koenig/node-renderers/header-v2-renderer.js +15 -31
  35. package/core/server/services/koenig/node-renderers/horizontalrule-renderer.js +43 -2
  36. package/core/server/services/koenig/node-renderers/product-renderer.js +0 -1
  37. package/core/server/services/koenig/render-partials/email-button.js +127 -21
  38. package/core/server/services/koenig/render-utils/stylex.js +104 -0
  39. package/core/server/services/stats/MembersStatsService.js +129 -5
  40. package/core/server/services/stats/PostsStatsService.js +281 -1
  41. package/core/server/services/stats/StatsService.js +23 -1
  42. package/core/server/services/stats/utils/tinybird.js +25 -5
  43. package/core/server/web/api/endpoints/admin/routes.js +2 -0
  44. package/package.json +13 -13
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/yarn.lock +851 -310
  47. package/components/tryghost-i18n-5.121.0.tgz +0 -0
  48. package/core/frontend/src/utils/session-storage.js +0 -68
  49. /package/core/built/admin/assets/{chunk.383.77dd3c8cbac5f464c6da.js.LICENSE.txt → chunk.383.3dcd1102188f9d5e0467.js.LICENSE.txt} +0 -0
@@ -1,5 +1,6 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const errors = require('@tryghost/errors');
3
+ const urlUtils = require('../../../shared/url-utils');
3
4
 
4
5
  /**
5
6
  * @typedef {Object} StatsServiceOptions
@@ -49,9 +50,11 @@ class PostsStatsService {
49
50
  /**
50
51
  * @param {object} deps
51
52
  * @param {import('knex').Knex} deps.knex - Database client
53
+ * @param {object} [deps.tinybirdClient] - Tinybird client for analytics
52
54
  */
53
55
  constructor(deps) {
54
56
  this.knex = deps.knex;
57
+ this.tinybirdClient = deps.tinybirdClient;
55
58
  }
56
59
 
57
60
  /**
@@ -60,7 +63,7 @@ class PostsStatsService {
60
63
  * @param {StatsServiceOptions} options
61
64
  * @returns {Promise<{data: TopPostResult[]}>} The top posts based on the requested attribution metric
62
65
  */
63
- async getTopPosts(options = {}) {
66
+ async getTopPosts(options) {
64
67
  try {
65
68
  const order = options.order || 'free_members desc';
66
69
  const limitRaw = Number.parseInt(String(options.limit ?? 20), 10); // Ensure options.limit is a string for parseInt
@@ -594,6 +597,283 @@ class PostsStatsService {
594
597
  };
595
598
  }
596
599
  }
600
+
601
+ /**
602
+ * Get stats for the latest published post including open rate, member attribution counts, and visitor count
603
+ * @returns {Promise<{data: Array<{id: string, title: string, slug: string, feature_image: string|null, published_at: string, recipient_count: number|null, opened_count: number|null, open_rate: number|null, member_delta: number, free_members: number, paid_members: number, visitors: number}>}>}
604
+ */
605
+ async getLatestPostStats() {
606
+ try {
607
+ // Get the latest published post
608
+ const latestPost = await this.knex('posts as p')
609
+ .select(
610
+ 'p.id',
611
+ 'p.uuid',
612
+ 'p.title',
613
+ 'p.slug',
614
+ 'p.feature_image',
615
+ 'p.published_at',
616
+ 'e.email_count',
617
+ 'e.opened_count'
618
+ )
619
+ .leftJoin('emails as e', 'p.id', 'e.post_id')
620
+ .where('p.status', 'published')
621
+ .whereNotNull('p.published_at')
622
+ .orderBy('p.published_at', 'desc')
623
+ .first();
624
+
625
+ if (!latestPost) {
626
+ return {data: []};
627
+ }
628
+
629
+ // Get member attribution counts using the same logic as other methods
630
+ const memberAttributionCounts = await this._getMemberAttributionCounts([latestPost.id]);
631
+ const attributionCount = memberAttributionCounts.find(ac => ac.post_id === latestPost.id);
632
+
633
+ const freeMembers = attributionCount ? attributionCount.free_members : 0;
634
+ const paidMembers = attributionCount ? attributionCount.paid_members : 0;
635
+ const totalMembers = freeMembers + paidMembers;
636
+
637
+ // Calculate open rate
638
+ const openRate = latestPost.email_count ?
639
+ (latestPost.opened_count / latestPost.email_count) * 100 :
640
+ null;
641
+
642
+ // Get visitor count from Tinybird
643
+ let visitors = 0;
644
+ if (this.tinybirdClient) {
645
+ try {
646
+ const dateFrom = new Date(latestPost.published_at).toISOString().split('T')[0];
647
+ const visitorData = await this.tinybirdClient.fetch('api_top_pages', {
648
+ post_uuid: latestPost.uuid,
649
+ dateFrom: dateFrom
650
+ });
651
+
652
+ visitors = visitorData?.[0]?.visits || 0;
653
+ } catch (error) {
654
+ logging.error('Error fetching visitor data from Tinybird:', error);
655
+ }
656
+ }
657
+
658
+ return {
659
+ data: [{
660
+ id: latestPost.id,
661
+ title: latestPost.title,
662
+ slug: latestPost.slug,
663
+ feature_image: latestPost.feature_image ? urlUtils.transformReadyToAbsolute(latestPost.feature_image) : latestPost.feature_image,
664
+ published_at: latestPost.published_at,
665
+ recipient_count: latestPost.email_count,
666
+ opened_count: latestPost.opened_count,
667
+ open_rate: openRate,
668
+ member_delta: totalMembers,
669
+ free_members: freeMembers,
670
+ paid_members: paidMembers,
671
+ visitors: visitors
672
+ }]
673
+ };
674
+ } catch (error) {
675
+ // Log the error but return a valid response
676
+ logging.error('Error fetching latest post stats:', error);
677
+ return {data: []};
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Get top posts by views for a given date range
683
+ * @param {Object} options
684
+ * @param {string} options.date_from - Start date in YYYY-MM-DD format
685
+ * @param {string} options.date_to - End date in YYYY-MM-DD format
686
+ * @param {string} options.timezone - Timezone to use for date interpretation
687
+ * @param {number} [options.limit] - Maximum number of posts to return (default: 5)
688
+ * @returns {Promise<Object>} Top posts with view counts and additional Ghost data
689
+ */
690
+ async getTopPostsViews(options) {
691
+ try {
692
+ const limit = options.limit || 5;
693
+ let viewsData = [];
694
+
695
+ if (this.tinybirdClient) {
696
+ const tinybirdOptions = {
697
+ dateFrom: options.date_from,
698
+ dateTo: options.date_to,
699
+ timezone: options.timezone,
700
+ post_type: 'post',
701
+ limit: limit
702
+ };
703
+
704
+ viewsData = await this.tinybirdClient.fetch('api_top_pages', tinybirdOptions) || [];
705
+ }
706
+
707
+ // Filter out any rows without post_uuid and get unique UUIDs
708
+ const postUuids = [...new Set(viewsData.filter(row => row.post_uuid).map(row => row.post_uuid))];
709
+
710
+ // Get posts data from Ghost DB for the posts we have views for
711
+ const posts = await this.knex('posts as p')
712
+ .select(
713
+ 'p.id as post_id',
714
+ 'p.uuid as post_uuid',
715
+ 'p.title',
716
+ 'p.published_at',
717
+ 'p.feature_image',
718
+ 'emails.email_count',
719
+ 'emails.opened_count'
720
+ )
721
+ .leftJoin('emails', 'emails.post_id', 'p.id')
722
+ .whereIn('p.uuid', postUuids);
723
+
724
+ // Get member attribution counts for these posts (model after GrowthStats logic)
725
+ const memberAttributionCounts = await this._getMemberAttributionCounts(posts.map(p => p.post_id), options);
726
+
727
+ // Process posts with views
728
+ const postsWithViews = viewsData.map((row) => {
729
+ const post = posts.find(p => p.post_uuid === row.post_uuid);
730
+
731
+ if (!post) {
732
+ return null;
733
+ }
734
+
735
+ // Find the member attribution count for this post
736
+ const attributionCount = memberAttributionCounts.find(ac => ac.post_id === post.post_id);
737
+ const memberCount = attributionCount ? (attributionCount.free_members + attributionCount.paid_members) : 0;
738
+
739
+ return {
740
+ post_id: post.post_id,
741
+ title: post.title,
742
+ published_at: post.published_at,
743
+ feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
744
+ views: row.visits,
745
+ open_rate: post.email_count > 0 ? (post.opened_count / post.email_count) * 100 : null,
746
+ members: memberCount
747
+ };
748
+ }).filter(Boolean);
749
+
750
+ // Calculate how many more posts we need - we want to always return 5 posts
751
+ const remainingCount = limit - postsWithViews.length;
752
+
753
+ // If we need more posts, get the latest ones excluding the ones we already have
754
+ let additionalPosts = [];
755
+ let additionalMemberAttributionCounts = [];
756
+ if (remainingCount > 0) {
757
+ additionalPosts = await this.knex('posts as p')
758
+ .select(
759
+ 'p.id as post_id',
760
+ 'p.uuid as post_uuid',
761
+ 'p.title',
762
+ 'p.published_at',
763
+ 'p.feature_image',
764
+ 'emails.email_count',
765
+ 'emails.opened_count'
766
+ )
767
+ .leftJoin('emails', 'emails.post_id', 'p.id')
768
+ .whereNotIn('p.uuid', postUuids)
769
+ .where('p.status', 'published')
770
+ .whereNotNull('p.published_at')
771
+ .orderBy('p.published_at', 'desc')
772
+ .limit(remainingCount);
773
+
774
+ // Get member attribution counts for additional posts
775
+ if (additionalPosts.length > 0) {
776
+ additionalMemberAttributionCounts = await this._getMemberAttributionCounts(additionalPosts.map(p => p.post_id), options);
777
+ }
778
+ }
779
+
780
+ // Process additional posts with 0 views
781
+ const additionalPostsWithZeroViews = additionalPosts.map((post) => {
782
+ // Find the member attribution count for this post
783
+ const attributionCount = additionalMemberAttributionCounts.find(ac => ac.post_id === post.post_id);
784
+ const memberCount = attributionCount ? (attributionCount.free_members + attributionCount.paid_members) : 0;
785
+
786
+ return {
787
+ post_id: post.post_id,
788
+ title: post.title,
789
+ published_at: post.published_at,
790
+ feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
791
+ views: 0,
792
+ open_rate: post.email_count > 0 ? (post.opened_count / post.email_count) * 100 : null,
793
+ members: memberCount
794
+ };
795
+ });
796
+
797
+ // Combine both sets of posts
798
+ return [...postsWithViews, ...additionalPostsWithZeroViews];
799
+ } catch (error) {
800
+ logging.error('Error fetching top posts views:', error);
801
+ return [];
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Get member attribution counts for a set of post IDs, modeling after GrowthStats logic
807
+ * Properly handles both free and paid members with deduplication
808
+ * @private
809
+ * @param {string[]} postIds - Array of post IDs to get attribution counts for
810
+ * @param {Object} options - Date filter options
811
+ * @returns {Promise<Array<{post_id: string, free_members: number, paid_members: number}>>}
812
+ */
813
+ async _getMemberAttributionCounts(postIds, options = {}) {
814
+ if (!postIds.length) {
815
+ return [];
816
+ }
817
+
818
+ try {
819
+ // Build free members query (modeled after _buildFreeMembersSubquery)
820
+ // Members who signed up on post but paid elsewhere/never
821
+ let freeMembersQuery = this.knex('members_created_events as mce')
822
+ .select('mce.attribution_id as post_id')
823
+ .countDistinct('mce.member_id as free_members')
824
+ .leftJoin('members_subscription_created_events as msce', function () {
825
+ this.on('mce.member_id', '=', 'msce.member_id')
826
+ .andOn('mce.attribution_id', '=', 'msce.attribution_id')
827
+ .andOnVal('msce.attribution_type', '=', 'post');
828
+ })
829
+ .where('mce.attribution_type', 'post')
830
+ .whereIn('mce.attribution_id', postIds)
831
+ .whereNull('msce.id')
832
+ .groupBy('mce.attribution_id');
833
+
834
+ // Apply date filter to free members query
835
+ this._applyDateFilter(freeMembersQuery, options, 'mce.created_at');
836
+
837
+ // Build paid members query (modeled after _buildPaidMembersSubquery)
838
+ // Members whose paid conversion was attributed to this post
839
+ let paidMembersQuery = this.knex('members_subscription_created_events as msce')
840
+ .select('msce.attribution_id as post_id')
841
+ .countDistinct('msce.member_id as paid_members')
842
+ .where('msce.attribution_type', 'post')
843
+ .whereIn('msce.attribution_id', postIds)
844
+ .groupBy('msce.attribution_id');
845
+
846
+ // Apply date filter to paid members query
847
+ this._applyDateFilter(paidMembersQuery, options, 'msce.created_at');
848
+
849
+ // Execute both queries
850
+ const [freeResults, paidResults] = await Promise.all([
851
+ freeMembersQuery,
852
+ paidMembersQuery
853
+ ]);
854
+
855
+ // Combine results for each post
856
+ const combinedResults = postIds.map((postId) => {
857
+ const freeResult = freeResults.find(r => r.post_id === postId);
858
+ const paidResult = paidResults.find(r => r.post_id === postId);
859
+
860
+ return {
861
+ post_id: postId,
862
+ free_members: freeResult ? freeResult.free_members : 0,
863
+ paid_members: paidResult ? paidResult.paid_members : 0
864
+ };
865
+ });
866
+
867
+ return combinedResults;
868
+ } catch (error) {
869
+ logging.error('Error fetching member attribution counts:', error);
870
+ return postIds.map(postId => ({
871
+ post_id: postId,
872
+ free_members: 0,
873
+ paid_members: 0
874
+ }));
875
+ }
876
+ }
597
877
  }
598
878
 
599
879
  module.exports = PostsStatsService;
@@ -89,6 +89,20 @@ class StatsService {
89
89
  return result;
90
90
  }
91
91
 
92
+ /**
93
+ * Get top posts by views
94
+ * @param {Object} options
95
+ * @param {string} options.date_from - Start date in YYYY-MM-DD format
96
+ * @param {string} options.date_to - End date in YYYY-MM-DD format
97
+ * @param {string} options.timezone - Timezone to use for date interpretation
98
+ * @param {number} [options.limit=5] - Maximum number of posts to return
99
+ * @returns {Promise<{data: import('./PostsStatsService').TopPostResult[]}>}
100
+ */
101
+ async getTopPostsViews(options) {
102
+ const result = await this.posts.getTopPostsViews(options);
103
+ return result;
104
+ }
105
+
92
106
  /**
93
107
  * @param {string} postId
94
108
  */
@@ -142,6 +156,14 @@ class StatsService {
142
156
  return result;
143
157
  }
144
158
 
159
+ /**
160
+ * Get stats for the latest published post
161
+ * @returns {Promise<{data: Object}>}
162
+ */
163
+ async getLatestPostStats() {
164
+ return await this.posts.getLatestPostStats();
165
+ }
166
+
145
167
  /**
146
168
  * @param {object} deps
147
169
  *
@@ -172,7 +194,7 @@ class StatsService {
172
194
  members: new MembersService(deps),
173
195
  subscriptions: new SubscriptionStatsService(deps),
174
196
  referrers: new ReferrersStatsService(deps),
175
- posts: new PostsStatsService(deps),
197
+ posts: new PostsStatsService(depsWithTinybird),
176
198
  content: new ContentStatsService(depsWithTinybird)
177
199
  });
178
200
  }
@@ -16,6 +16,7 @@ const create = ({config, request}) => {
16
16
  * @param {string} [options.dateTo] - End date in YYYY-MM-DD format
17
17
  * @param {string} [options.timezone] - Timezone for the query
18
18
  * @param {string} [options.memberStatus] - Member status filter (defaults to 'all')
19
+ * @param {string} [options.postType] - Post type filter
19
20
  * @param {string} [options.tbVersion] - Tinybird version for API URL
20
21
  * @returns {Object} Object with URL and request options
21
22
  */
@@ -34,12 +35,31 @@ const create = ({config, request}) => {
34
35
 
35
36
  // Use snake_case for query parameters as expected by Tinybird API
36
37
  const searchParams = {
37
- site_uuid: statsConfig.id,
38
- date_from: options.dateFrom,
39
- date_to: options.dateTo,
40
- timezone: options.timezone || config.get('timezone'),
41
- member_status: options.memberStatus || 'all'
38
+ site_uuid: statsConfig.id
42
39
  };
40
+
41
+ // todo: refactor all uses to simply pass options through
42
+ if (options.dateFrom) {
43
+ searchParams.date_from = options.dateFrom;
44
+ }
45
+ if (options.dateTo) {
46
+ searchParams.date_to = options.dateTo;
47
+ }
48
+ if (options.timezone) {
49
+ searchParams.timezone = options.timezone;
50
+ }
51
+ if (options.memberStatus) {
52
+ searchParams.member_status = options.memberStatus;
53
+ }
54
+ if (options.postType) {
55
+ searchParams.post_type = options.postType;
56
+ }
57
+ // Add any other options that might be needed
58
+ Object.entries(options).forEach(([key, value]) => {
59
+ if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus'].includes(key)) {
60
+ searchParams[key] = value;
61
+ }
62
+ });
43
63
 
44
64
  // Convert searchParams to query string and append to URL
45
65
  const queryString = new URLSearchParams(searchParams).toString();
@@ -156,7 +156,9 @@ module.exports = function apiRoutes() {
156
156
  router.get('/stats/referrers/posts/:id', mw.authAdminApi, http(api.stats.postReferrers));
157
157
  router.get('/stats/referrers', mw.authAdminApi, http(api.stats.referrersHistory));
158
158
  if (labs.isSet('trafficAnalytics')) {
159
+ router.get('/stats/latest-post', mw.authAdminApi, http(api.stats.latestPost));
159
160
  router.get('/stats/top-posts', mw.authAdminApi, http(api.stats.topPosts));
161
+ router.get('/stats/top-posts-views', mw.authAdminApi, http(api.stats.topPostsViews));
160
162
  router.get('/stats/top-content', mw.authAdminApi, http(api.stats.topContent));
161
163
  router.get('/stats/newsletter-stats', mw.authAdminApi, http(api.stats.newsletterStats));
162
164
  router.get('/stats/subscriber-count', mw.authAdminApi, http(api.stats.subscriberCount));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "5.121.0",
3
+ "version": "5.123.0",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -36,18 +36,18 @@
36
36
  "test:unit:base": "yarn test:base './test/unit' --timeout=2000",
37
37
  "test:integration": "yarn test:base './test/integration' --timeout=10000",
38
38
  "test:e2e": "yarn test:base ./test/e2e-* --timeout=15000",
39
- "test:regression": "yarn test:base './test/regression' --timeout=60000",
39
+ "test:legacy": "yarn test:base './test/legacy' --timeout=60000",
40
40
  "test:browser": "NODE_ENV=testing-browser playwright test",
41
41
  "test:browser:admin": "NODE_ENV=testing-browser playwright test test/e2e-browser --project=admin",
42
42
  "test:browser:portal": "NODE_ENV=testing-browser playwright test test/e2e-browser --project=portal",
43
43
  "test:browser:setup": "npx playwright install",
44
44
  "test:ci:e2e": "c8 -c ./.c8rc.e2e.json -o coverage-e2e yarn test:e2e -b",
45
- "test:ci:regression": "yarn test:regression -b",
45
+ "test:ci:legacy": "yarn test:legacy -b",
46
46
  "test:ci:integration": "c8 -c ./.c8rc.e2e.json -o coverage-integration --lines 52 --functions 47 --branches 73 --statements 52 yarn test:integration -b",
47
47
  "test:unit:slow": "yarn test:unit --reporter=mocha-slow-test-reporter",
48
48
  "test:int:slow": "yarn test:integration --reporter=mocha-slow-test-reporter",
49
49
  "test:e2e:slow": "yarn test:e2e --reporter=mocha-slow-test-reporter",
50
- "test:reg:slow": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js './test/regression' --timeout=60000 --reporter=mocha-slow-test-reporter",
50
+ "test:leg:slow": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js './test/legacy' --timeout=60000 --reporter=mocha-slow-test-reporter",
51
51
  "lint:server": "eslint --ignore-path .eslintignore 'core/server/**/*.js' 'core/*.js' '*.js' --cache",
52
52
  "lint:shared": "eslint --ignore-path .eslintignore 'core/shared/**/*.js' --cache",
53
53
  "lint:frontend": "eslint --ignore-path .eslintignore 'core/frontend/**/*.js' --cache",
@@ -79,11 +79,11 @@
79
79
  "@tryghost/domain-events": "1.0.1",
80
80
  "@tryghost/email-mock-receiver": "0.3.11",
81
81
  "@tryghost/errors": "1.3.8",
82
- "@tryghost/helpers": "1.1.94",
82
+ "@tryghost/helpers": "1.1.95",
83
83
  "@tryghost/html-to-plaintext": "1.0.4",
84
84
  "@tryghost/http-cache-utils": "0.1.20",
85
- "@tryghost/i18n": "file:components/tryghost-i18n-5.121.0.tgz",
86
- "@tryghost/image-transform": "1.4.4",
85
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.123.0.tgz",
86
+ "@tryghost/image-transform": "1.4.5",
87
87
  "@tryghost/job-manager": "1.0.1",
88
88
  "@tryghost/kg-card-factory": "5.1.1",
89
89
  "@tryghost/kg-clean-basic-html": "4.2.5",
@@ -107,7 +107,7 @@
107
107
  "@tryghost/pretty-cli": "1.2.47",
108
108
  "@tryghost/prometheus-metrics": "1.0.1",
109
109
  "@tryghost/promise": "0.3.15",
110
- "@tryghost/referrer-parser": "0.1.5",
110
+ "@tryghost/referrer-parser": "0.1.7",
111
111
  "@tryghost/request": "1.0.11",
112
112
  "@tryghost/root-utils": "0.3.33",
113
113
  "@tryghost/security": "1.0.1",
@@ -158,7 +158,7 @@
158
158
  "ghost-storage-base": "1.0.0",
159
159
  "glob": "8.1.0",
160
160
  "got": "11.8.6",
161
- "gscan": "4.48.1",
161
+ "gscan": "4.49.1",
162
162
  "handlebars": "4.7.8",
163
163
  "html-to-text": "5.1.1",
164
164
  "html5parser": "2.0.2",
@@ -192,7 +192,7 @@
192
192
  "mime-types": "2.1.35",
193
193
  "moment": "2.24.0",
194
194
  "moment-timezone": "0.5.45",
195
- "multer": "2.0.0",
195
+ "multer": "2.0.1",
196
196
  "mysql2": "3.14.1",
197
197
  "nconf": "0.13.0",
198
198
  "node-fetch": "2.7.0",
@@ -211,7 +211,7 @@
211
211
  "stripe": "8.222.0",
212
212
  "superagent": "5.3.1",
213
213
  "superagent-throttle": "1.0.1",
214
- "terser": "5.40.0",
214
+ "terser": "5.41.0",
215
215
  "tiny-glob": "0.2.9",
216
216
  "tough-cookie": "4.1.4",
217
217
  "ua-parser-js": "1.0.40",
@@ -231,7 +231,7 @@
231
231
  "@types/bookshelf": "1.2.9",
232
232
  "@types/common-tags": "1.8.4",
233
233
  "@types/jsonwebtoken": "9.0.9",
234
- "@types/node": "22.15.27",
234
+ "@types/node": "22.15.30",
235
235
  "@types/node-jose": "1.1.13",
236
236
  "@types/nodemailer": "6.4.17",
237
237
  "@types/sinon": "17.0.4",
@@ -272,7 +272,7 @@
272
272
  "jackspeak": "2.3.6",
273
273
  "moment": "2.24.0",
274
274
  "moment-timezone": "0.5.45",
275
- "@tryghost/i18n": "file:components/tryghost-i18n-5.121.0.tgz"
275
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.123.0.tgz"
276
276
  },
277
277
  "nx": {
278
278
  "targets": {