ghost 6.55.0 → 6.56.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 (280) hide show
  1. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  2. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  3. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  4. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  8. package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
  9. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  10. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  11. package/core/built/admin/assets/{account-migration-sjHLx5dy.js → account-migration-D3O_yVck.js} +1 -1
  12. package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.js → account-migration-D8FbP-Z-.js} +7 -7
  13. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  14. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
  15. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{bluesky-sharing-Bxeh6MKW.js → bluesky-sharing-f1jTQe2P.js} +16 -16
  17. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{dist-D8sFfdpC.js → dist-BEfX-0aW.js} +12 -12
  20. package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-AaDlZlrA.js} +9 -9
  21. package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-DPi8Tl5M.js} +945 -945
  22. package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CxTzjILQ.js} +8 -8
  23. package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-DPv5x2MX.js} +33 -33
  24. package/core/built/admin/assets/activitypub/{field-BV-CIT3d.js → field-DbgA8lat.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
  26. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{inbox-yC0nfvzl.js → inbox-DSrF_i1O.js} +85 -85
  28. package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-CPjFBbo7.js} +3 -3
  29. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
  30. package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-BkDC1XVZ.js} +11 -11
  31. package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-xEyH4xT-.js} +7 -7
  32. package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-DClaL3ui.js} +9 -9
  33. package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-DpnOa-4Z.js} +20 -20
  34. package/core/built/admin/assets/activitypub/{profile-BAsaJekx.js → profile-Dvi9mz-6.js} +237 -237
  35. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
  36. package/core/built/admin/assets/activitypub/{routes-CLH3OVTP.js → routes-DFBeTnfn.js} +1139 -1150
  37. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
  38. package/core/built/admin/assets/activitypub/{settings-DA6pNJ-Q.js → settings-CV2fFu6D.js} +13 -13
  39. package/core/built/admin/assets/activitypub/{site-CQD42Pw5.js → site-_TgDd_EM.js} +81 -81
  40. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
  41. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
  42. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
  43. package/core/built/admin/assets/activitypub/{tabs-Cg0saow1.js → tabs-BUbz7tea.js} +317 -315
  44. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
  45. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
  46. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
  47. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Br4aOWOh.js → use-reply-chain-data-CyFcGfan.js} +5 -5
  48. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
  49. package/core/built/admin/assets/{audience-DjEWNhp3.js → audience-CkBb2LlO.js} +1 -1
  50. package/core/built/admin/assets/{automated-emails-DRy7qY-J.js → automated-emails-6t2bIRWf.js} +1 -1
  51. package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-cMllU545.js} +1 -1
  52. package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.js → bluesky-sharing-DPGpyD-G.js} +1 -1
  53. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  54. package/core/built/admin/assets/{breadcrumb-BD0U2ygg.js → breadcrumb-DIfcF5ow.js} +1 -1
  55. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  56. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  57. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  58. package/core/built/admin/assets/{chart-helpers-fjQKRVyY.js → chart-helpers-DV0FkdcD.js} +1 -1
  59. package/core/built/admin/assets/{checkbox-CWa28lgO.js → checkbox-BIl077a0.js} +1 -1
  60. package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.332.05991a101d67f732dec3.js} +43 -42
  61. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
  62. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
  63. package/core/built/admin/assets/{code-editor-view-CCr0rBWS.js → code-editor-view-C0aWNwdl.js} +1 -1
  64. package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
  65. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  66. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  67. package/core/built/admin/assets/{date-range-select-BFp58EMk.js → date-range-select-dXc-GsVe.js} +1 -1
  68. package/core/built/admin/assets/{disabled-sources-indicator-CUxMfiPa.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
  69. package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
  70. package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
  71. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  72. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
  73. package/core/built/admin/assets/domain-B5ogXUBa.js +1 -0
  74. package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-BnFG7dDP.js} +1 -1
  75. package/core/built/admin/assets/{edit-profile-BKT-REJw.js → edit-profile-B07jCZy9.js} +1 -1
  76. package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
  77. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  78. package/core/built/admin/assets/{explore-ClUpJZOp.js → explore-Ca1y8HW_.js} +1 -1
  79. package/core/built/admin/assets/{feed-DF92dUMP.js → feed-x4iFMBP4.js} +1 -1
  80. package/core/built/admin/assets/{field-Do7lieBA.js → field-ChCAabZg.js} +1 -1
  81. package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
  82. package/core/built/admin/assets/filters-CLehPo_D.js +1 -0
  83. package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
  84. package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-7f01131115186d1af84ead76e79646dd.js} +149 -129
  85. package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  86. package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  87. package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-Buqh2vXh.js} +1 -1
  88. package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-DajOBdN0.js} +1 -1
  89. package/core/built/admin/assets/{growth-CVw1My4S.js → growth-Dy2UOSay.js} +2 -2
  90. package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BUOLDoji.js} +12 -12
  91. package/core/built/admin/assets/index-08n05-rc.js +67 -0
  92. package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
  93. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  94. package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
  95. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  96. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9535 -9527
  97. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  98. package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-BFc4yFed.js} +86 -86
  99. package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-BN-E6WYX.js} +1 -1
  100. package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-CCt-qeWD.js} +1 -1
  101. package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-DlxTbTs4.js} +1 -1
  102. package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-BlcUttpn.js} +1 -1
  103. package/core/built/admin/assets/{label-DS8Mi6G5.js → label-AVrxwmsz.js} +1 -1
  104. package/core/built/admin/assets/{label-picker-1DvSuLA1.js → label-picker-DVnpu_IF.js} +1 -1
  105. package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
  106. package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
  107. package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
  108. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  109. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
  110. package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
  111. package/core/built/admin/assets/{members-CQpVFJu4.js → members-oLPiSkXD.js} +2 -2
  112. package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
  113. package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-CQ3urTFE.js} +1 -1
  114. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
  115. package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
  116. package/core/built/admin/assets/{note-C8kxrz6w.js → note-Dk877BTv.js} +1 -1
  117. package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-CPlLjPxO.js} +1 -1
  118. package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DdStOtjy.js} +1 -1
  119. package/core/built/admin/assets/{overview-Bf2sbznq.js → overview--4V-kx9c.js} +1 -1
  120. package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
  121. package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-pp4dWmOS.js} +1 -1
  122. package/core/built/admin/assets/{post-analytics-We-ko5Cb.js → post-analytics-BMJrI81R.js} +1 -1
  123. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
  124. package/core/built/admin/assets/{post-analytics-provider-B6UynTGb.js → post-analytics-provider-Btml5vJI.js} +1 -1
  125. package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-AcUZkbfL.js} +1 -1
  126. package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
  127. package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
  128. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
  129. package/core/built/admin/assets/select-B0HteHrp.js +1 -0
  130. package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
  131. package/core/built/admin/assets/{settings-WXNeWyU1.js → settings-BGgk7oCI.js} +1 -1
  132. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  133. package/core/built/admin/assets/{share-modal-DsgW3wbg.js → share-modal-BeuydaBf.js} +1 -1
  134. package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
  135. package/core/built/admin/assets/{sort-button-Bb_FCl7e.js → sort-button-Df4t2AL8.js} +1 -1
  136. package/core/built/admin/assets/{source-icon-BeaRxUn9.js → source-icon-ajoaKb41.js} +1 -1
  137. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  138. package/core/built/admin/assets/{stats-Bjj_kmJ1.js → stats-C_lfxZAw.js} +1 -1
  139. package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-Cxot1iD8.js} +1 -1
  140. package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-ogAA4lDr.js} +1 -1
  141. package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-C5Xini-7.js} +1 -1
  142. package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-CZv1FV81.js} +2 -2
  143. package/core/built/admin/assets/{switch-z5Y6Uzt6.js → switch-CyQcwMAv.js} +1 -1
  144. package/core/built/admin/assets/{table-CB-4_HBJ.js → table-DFj1wiYv.js} +1 -1
  145. package/core/built/admin/assets/{tabs-CCEgKYzq.js → tabs-D4y1gqUd.js} +1 -1
  146. package/core/built/admin/assets/{tags-Cg2VJGmG.js → tags-Dmft-Y4O.js} +1 -1
  147. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  148. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  149. package/core/built/admin/assets/{toggle-group-b3IlxvhV.js → toggle-group-R2QMFvXW.js} +1 -1
  150. package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-DGDxDalo.js} +1 -1
  151. package/core/built/admin/assets/{use-analytics-data-S0FTd43s.js → use-analytics-data-C76v4LIp.js} +1 -1
  152. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
  153. package/core/built/admin/assets/{use-growth-stats-CIaiHOVb.js → use-growth-stats-aNXJOmQr.js} +1 -1
  154. package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-CG0SZioj.js} +1 -1
  155. package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
  156. package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-DgcF69KI.js} +1 -1
  157. package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
  158. package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-C9fPjVM_.js} +1 -1
  159. package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-BmTUzeoF.js} +1 -1
  160. package/core/built/admin/assets/{web-B_5GZRvH.js → web-BqW0b0_C.js} +1 -1
  161. package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
  162. package/core/built/admin/index.html +16 -16
  163. package/core/frontend/services/data/fetch-data.js +10 -5
  164. package/core/frontend/services/routing/api-adapter.js +115 -0
  165. package/core/frontend/services/routing/api-adapter.ts +157 -0
  166. package/core/frontend/services/routing/collection-router.js +5 -3
  167. package/core/frontend/services/routing/controllers/index.js +1 -1
  168. package/core/frontend/services/routing/controllers/rss.js +26 -31
  169. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  170. package/core/frontend/services/routing/controllers/static.js +8 -7
  171. package/core/frontend/services/routing/parent-router.js +17 -4
  172. package/core/frontend/services/routing/permalink-adapter.js +26 -0
  173. package/core/frontend/services/routing/permalink-adapter.ts +23 -0
  174. package/core/frontend/services/routing/registry.js +59 -33
  175. package/core/frontend/services/routing/router-manager.js +9 -13
  176. package/core/frontend/services/routing/static-routes-router.js +4 -4
  177. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  178. package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
  179. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  180. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  181. package/core/server/data/schema/schema.js +1 -0
  182. package/core/server/models/post.js +10 -1
  183. package/core/server/services/automations/automations-repository.ts +3 -1
  184. package/core/server/services/automations/database-automations-repository.js +33 -14
  185. package/core/server/services/automations/database-automations-repository.ts +41 -15
  186. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  187. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  188. package/core/server/services/automations/poll.js +3 -1
  189. package/core/server/services/automations/poll.ts +4 -1
  190. package/core/server/services/automations/service.js +5 -1
  191. package/core/server/services/automations/service.ts +5 -1
  192. package/core/server/services/gift-links/CONTEXT.md +9 -0
  193. package/core/server/services/gifts/CONTEXT.md +55 -0
  194. package/core/server/services/gifts/README.md +28 -0
  195. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  196. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  197. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  198. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  199. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  200. package/core/server/services/gifts/gift-codec.js +63 -0
  201. package/core/server/services/gifts/gift-codec.ts +62 -0
  202. package/core/server/services/gifts/gift-controller.js +6 -31
  203. package/core/server/services/gifts/gift-controller.ts +6 -69
  204. package/core/server/services/gifts/gift-email-service.ts +3 -2
  205. package/core/server/services/gifts/gift-schema.js +34 -0
  206. package/core/server/services/gifts/gift-schema.ts +39 -0
  207. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  208. package/core/server/services/gifts/gift-service.js +265 -52
  209. package/core/server/services/gifts/gift-service.ts +431 -97
  210. package/core/server/services/gifts/gift.ts +16 -39
  211. package/core/server/services/link-tracking/link-click-repository.js +6 -1
  212. package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
  213. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
  214. package/core/server/services/member-welcome-emails/service.js +7 -3
  215. package/core/server/services/members/api.js +1 -2
  216. package/core/server/services/members/import-export/import/importer.ts +3 -2
  217. package/core/server/services/members/import-export/index.js +10 -2
  218. package/core/server/services/members/import-export/index.ts +14 -4
  219. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  220. package/core/server/services/members/members-api/members-api.js +13 -7
  221. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  222. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  223. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  224. package/core/server/services/members-custom-fields/schema.js +8 -1
  225. package/core/server/services/members-custom-fields/schema.ts +8 -0
  226. package/core/server/services/members-custom-fields/values-service.js +118 -2
  227. package/core/server/services/members-custom-fields/values-service.ts +134 -4
  228. package/core/server/services/oembed/oembed-service.js +44 -3
  229. package/core/server/services/posts/posts-service.js +11 -0
  230. package/core/server/services/route-settings/dynamic-routing-service.js +1 -3
  231. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  232. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  233. package/core/server/services/route-settings/validation-errors.js +68 -4
  234. package/core/server/services/route-settings/validation-errors.ts +79 -4
  235. package/core/server/services/stats/posts-stats-service.js +27 -23
  236. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  237. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  238. package/core/server/services/stripe/stripe-api.js +10 -0
  239. package/core/server/web/gift-preview/controller.js +10 -25
  240. package/core/shared/labs.js +2 -2
  241. package/package.json +10 -9
  242. package/pnpm-lock.yaml +28 -18
  243. package/pnpm-workspace.yaml +5 -4
  244. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  245. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
  246. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
  247. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
  248. package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
  249. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  250. package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
  251. package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
  252. package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
  253. package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
  254. package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
  255. package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
  256. package/core/built/admin/assets/index-BGG7NckO.js +0 -67
  257. package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
  258. package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
  259. package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
  260. package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
  261. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
  262. package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
  263. package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
  264. package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
  265. package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
  266. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  267. package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
  268. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
  269. package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
  270. package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
  271. package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
  272. package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
  273. package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
  274. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  275. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +0 -1
  276. package/core/built/admin/assets/web-jFYWCKDn.js +0 -2
  277. package/core/server/services/gifts/gift-repository.js +0 -2
  278. package/core/server/services/gifts/gift-repository.ts +0 -29
  279. package/core/server/services/route-settings/activation-bridge.js +0 -174
  280. package/core/server/services/route-settings/activation-bridge.ts +0 -250
@@ -2,8 +2,8 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <link rel="stylesheet" href="./assets/vendor-3bf602dfa6463c3400ffd81697767274.css">
5
- <link rel="stylesheet" href="./assets/ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css">
6
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%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%226.55%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%22e35f1aa34a%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%22d7e993cdd1%22%2C%22activitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D">
5
+ <link rel="stylesheet" href="./assets/ghost-d38c30b866e59f2e06a357b5eb844fe5.css">
6
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%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%226.56%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%22571a1e1dc6%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%2228a9fedb77%22%2C%22activitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D">
7
7
 
8
8
  <meta charset="UTF-8" />
9
9
 
@@ -20,28 +20,28 @@
20
20
 
21
21
  <link rel="shortcut icon" href="./assets/favicon-C_q1KHzv.ico" />
22
22
  <link rel="apple-touch-icon" href="./assets/apple-touch-icon-DhO18_4B.png" />
23
- <script type="module" crossorigin src="./assets/index-BGG7NckO.js"></script>
23
+ <script type="module" crossorigin src="./assets/index-08n05-rc.js"></script>
24
24
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-DAXXjFlN.js">
25
25
  <link rel="modulepreload" crossorigin href="./assets/react-Bb3IsNbi.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/jsx-runtime-DX3WBWVO.js">
27
27
  <link rel="modulepreload" crossorigin href="./assets/framework-provider-CmtnPXIT.js">
28
28
  <link rel="modulepreload" crossorigin href="./assets/dist-Ndy-PuGN.js">
29
29
  <link rel="modulepreload" crossorigin href="./assets/hooks-CixO01lL.js">
30
- <link rel="modulepreload" crossorigin href="./assets/dist-DGzPJ_bO.js">
30
+ <link rel="modulepreload" crossorigin href="./assets/dist-Cb5RSHSg.js">
31
31
  <link rel="modulepreload" crossorigin href="./assets/dist-CQJY6SVY.js">
32
- <link rel="modulepreload" crossorigin href="./assets/dist-P_ohUlTi.js">
33
- <link rel="modulepreload" crossorigin href="./assets/Combination-CJG7Flp-.js">
32
+ <link rel="modulepreload" crossorigin href="./assets/dist-CqO2lWS0.js">
33
+ <link rel="modulepreload" crossorigin href="./assets/Combination-BCm9FL4o.js">
34
34
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-DYl5dUZ5.js">
35
35
  <link rel="modulepreload" crossorigin href="./assets/chunk-KS7C4IRE-DG9HTHQ3.js">
36
- <link rel="modulepreload" crossorigin href="./assets/router-provider-C1R4AZ2o.js">
37
- <link rel="modulepreload" crossorigin href="./assets/text-DwtjwGkh.js">
38
- <link rel="modulepreload" crossorigin href="./assets/shade-app-B3heOAfz-CGXQwWeO.js">
39
- <link rel="modulepreload" crossorigin href="./assets/dist-CjY-f5Z1.js">
40
- <link rel="modulepreload" crossorigin href="./assets/dist-2Tnz2Yfq.js">
41
- <link rel="modulepreload" crossorigin href="./assets/input-surface-BpAjZXY5.js">
36
+ <link rel="modulepreload" crossorigin href="./assets/router-provider-BmEZf6c6.js">
37
+ <link rel="modulepreload" crossorigin href="./assets/text-DU9Hv33_.js">
38
+ <link rel="modulepreload" crossorigin href="./assets/shade-app-B3heOAfz-BhQXxNkT.js">
39
+ <link rel="modulepreload" crossorigin href="./assets/dist-BUKmD51u.js">
40
+ <link rel="modulepreload" crossorigin href="./assets/dist-CNlMj7ML.js">
41
+ <link rel="modulepreload" crossorigin href="./assets/input-surface-BdOe4KzO.js">
42
42
  <link rel="modulepreload" crossorigin href="./assets/update-queries-C4CnvJaW.js">
43
43
  <link rel="modulepreload" crossorigin href="./assets/users-Dkv8NQZe.js">
44
- <link rel="stylesheet" crossorigin href="./assets/index-9Cw8z3rA.css">
44
+ <link rel="stylesheet" crossorigin href="./assets/index-DgUzgFvS.css">
45
45
  </head>
46
46
  <body class="react-admin">
47
47
  <div id="ember-alerts-wormhole"></div>
@@ -62,8 +62,8 @@
62
62
  <div id="ember-notifications-wormhole"></div>
63
63
  <script src="./assets/vendor-25f0bb2b2de85265afdf33da79f14a44.js"></script>
64
64
  <script src="./assets/chunk.211.6f7328d63c41536451b3.js"></script>
65
- <script src="./assets/chunk.481.6c7395a49484bb4583aa.js"></script>
66
- <script src="./assets/chunk.524.cffd399783a519e404a9.js"></script>
67
- <script src="./assets/ghost-c4057474943effc568c5659ccc4909d7.js"></script>
65
+ <script src="./assets/chunk.332.05991a101d67f732dec3.js"></script>
66
+ <script src="./assets/chunk.524.87b52f4fada529df2c4c.js"></script>
67
+ <script src="./assets/ghost-7f01131115186d1af84ead76e79646dd.js"></script>
68
68
  </body>
69
69
  </html>
@@ -3,6 +3,7 @@
3
3
  * Dynamically build and execute queries on the API
4
4
  */
5
5
  const _ = require('lodash');
6
+ const {resolveRouteData} = require('../routing/api-adapter');
6
7
 
7
8
  // The default settings for a default post query
8
9
  const queryDefaults = {
@@ -93,9 +94,13 @@ async function fetchData(pathOptions, routerOptions, locals) {
93
94
  // The filter can in theory contain a "%s" e.g. filter="primary_tag:%s"
94
95
  promises.push(processQuery(postQuery, pathOptions.slug, locals));
95
96
 
97
+ const apiCalls = resolveRouteData(routerOptions.data);
98
+
96
99
  // CASE: fetch more data defined by the router e.g. tags, authors - see TaxonomyRouter
97
- _.each(routerOptions.data, function (query, name) {
98
- const dataQueryOptions = _.merge(query, defaultDataQueryOptions[name]);
100
+ _.each(apiCalls, function (apiCall, name) {
101
+ // Merge into a fresh object: the resolved spec is read again below to
102
+ // shape the response, so it must stay as the adapter resolved it.
103
+ const dataQueryOptions = _.merge({}, apiCall, defaultDataQueryOptions[name]);
99
104
  promises.push(processQuery(dataQueryOptions, pathOptions.slug, locals));
100
105
  });
101
106
 
@@ -107,11 +112,11 @@ async function fetchData(pathOptions, routerOptions, locals) {
107
112
 
108
113
  let resultIndex = 1;
109
114
 
110
- _.each(routerOptions.data, function (config, name) {
115
+ _.each(apiCalls, function (apiCall, name) {
111
116
  if (results[resultIndex]) {
112
- response.data[name] = results[resultIndex][config.resource];
117
+ response.data[name] = results[resultIndex][apiCall.resource];
113
118
 
114
- if (config.type === 'browse') {
119
+ if (apiCall.type === 'browse') {
115
120
  response.data[name].meta = results[resultIndex].meta;
116
121
  }
117
122
 
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveApiCall = resolveApiCall;
7
+ exports.resolveRouteData = resolveRouteData;
8
+ exports.resolveResourceRead = resolveResourceRead;
9
+ const lodash_1 = __importDefault(require("lodash"));
10
+ const errors_1 = __importDefault(require("@tryghost/errors"));
11
+ const config_1 = require("./config");
12
+ /**
13
+ * Not every entry in QUERY is reachable from route data — `previews` and `email`
14
+ * are internal routers with no `type`, so they resolve to no API call.
15
+ */
16
+ const RESOURCE_CONFIGS = config_1.QUERY;
17
+ /**
18
+ * The entry fields the Content API accepts as query options. Everything else on
19
+ * a data entry is routing metadata (`redirect`) or unsupported (`fields`), and
20
+ * is deliberately not forwarded.
21
+ */
22
+ const ALLOWED_QUERY_OPTIONS = ['limit', 'order', 'filter', 'include', 'slug', 'visibility', 'status', 'page'];
23
+ function unknownResource(resource) {
24
+ return new errors_1.default.IncorrectUsageError({ message: `Unknown route data resource: ${resource}` });
25
+ }
26
+ // Shorthand names a resource the short way (`tag`), long form the way the API
27
+ // does (`tags`). Both land on the same config.
28
+ function splitShortForm(shortForm) {
29
+ const [key, slug] = shortForm.split('.');
30
+ return [key, slug];
31
+ }
32
+ function shortFormConfig(key) {
33
+ return RESOURCE_CONFIGS[key];
34
+ }
35
+ function longFormConfig(resource) {
36
+ return Object.values(RESOURCE_CONFIGS).find(config => config?.resource === resource);
37
+ }
38
+ function resolveShortForm(shortForm) {
39
+ const [key, slug] = splitShortForm(shortForm);
40
+ const config = shortFormConfig(key);
41
+ if (!config?.type) {
42
+ throw unknownResource(key);
43
+ }
44
+ return {
45
+ controller: config.controller,
46
+ type: config.type,
47
+ resource: config.resource,
48
+ options: { ...config.options, slug }
49
+ };
50
+ }
51
+ function resolveLongForm(entry) {
52
+ const config = longFormConfig(entry.resource);
53
+ if (!config) {
54
+ throw unknownResource(entry.resource);
55
+ }
56
+ const options = lodash_1.default.pick(entry, ALLOWED_QUERY_OPTIONS);
57
+ return {
58
+ controller: config.controller,
59
+ type: entry.type,
60
+ resource: config.resource,
61
+ // A `read` targets one known resource, so the resource defaults (e.g.
62
+ // `visibility: public` for tags) apply. A `browse` is author-driven and
63
+ // takes only what the entry asked for.
64
+ options: entry.type === 'read' ? lodash_1.default.defaults(options, config.options) : options
65
+ };
66
+ }
67
+ /**
68
+ * Resolve a single route data entry into a Content API call.
69
+ *
70
+ * This is the one place that knows how a domain `resource` maps onto a Ghost API
71
+ * controller — routes.yaml talks about `tags`, the API is reached via
72
+ * `tagsPublic`. Callers work with the domain model and never name a controller.
73
+ *
74
+ * @example resolveApiCall('tag.food')
75
+ * // => {controller: 'tagsPublic', type: 'read', resource: 'tags', options: {slug: 'food', visibility: 'public'}}
76
+ */
77
+ function resolveApiCall(entry) {
78
+ return typeof entry === 'string' ? resolveShortForm(entry) : resolveLongForm(entry);
79
+ }
80
+ /**
81
+ * Resolve a route's whole `data` block, keyed the way the theme sees it.
82
+ *
83
+ * Top-level shorthand (`data: tag.food`) has no author-given name, so it is
84
+ * keyed by its resource shorthand — `tag` — matching what themes already expect.
85
+ */
86
+ function resolveRouteData(routeData) {
87
+ if (!routeData) {
88
+ return {};
89
+ }
90
+ if (typeof routeData === 'string') {
91
+ const [key] = splitShortForm(routeData);
92
+ return { [key]: resolveApiCall(routeData) };
93
+ }
94
+ return lodash_1.default.mapValues(routeData, entry => resolveApiCall(entry));
95
+ }
96
+ /**
97
+ * The resource read a data entry claims, or `null` if it claims none.
98
+ *
99
+ * Only a `read` names a single resource; a `browse` pulls a set and claims
100
+ * nothing. Unlike {@link resolveApiCall} this answers a routing question, so an
101
+ * entry it cannot make sense of is simply not a claim rather than an error.
102
+ *
103
+ * @example resolveResourceRead('tag.food') // => {resource: 'tags', slug: 'food'}
104
+ */
105
+ function resolveResourceRead(entry) {
106
+ if (typeof entry === 'string') {
107
+ const [key, slug] = splitShortForm(entry);
108
+ const config = shortFormConfig(key);
109
+ return config?.type === 'read' ? { resource: config.resource, slug } : null;
110
+ }
111
+ if (entry.type !== 'read') {
112
+ return null;
113
+ }
114
+ return longFormConfig(entry.resource) ? { resource: entry.resource, slug: entry.slug } : null;
115
+ }
@@ -0,0 +1,157 @@
1
+ import _ from 'lodash';
2
+ import errors from '@tryghost/errors';
3
+ import {QUERY} from './config';
4
+ import type {RouteData, DataEntry, DataShortForm, DataLongFormEntry} from '@tryghost/adapter-base-route-settings';
5
+
6
+ /**
7
+ * A resolved Content API call: which controller to reach for, which method to
8
+ * call on it, and the options to call it with.
9
+ */
10
+ export interface ApiCallSpec {
11
+ controller: string;
12
+ type: 'read' | 'browse';
13
+ resource: string;
14
+ options: Record<string, unknown>;
15
+ }
16
+
17
+ /**
18
+ * The resource read that a data entry claims — the router that declares it owns
19
+ * that slug, and other routers redirect to it.
20
+ */
21
+ export interface ResourceRead {
22
+ resource: string;
23
+ slug: string;
24
+ }
25
+
26
+ interface ResourceConfig {
27
+ controller: string;
28
+ type?: 'read' | 'browse';
29
+ resource: string;
30
+ options?: Record<string, unknown>;
31
+ }
32
+
33
+ /**
34
+ * Not every entry in QUERY is reachable from route data — `previews` and `email`
35
+ * are internal routers with no `type`, so they resolve to no API call.
36
+ */
37
+ const RESOURCE_CONFIGS: Record<string, ResourceConfig | undefined> = QUERY;
38
+
39
+ /**
40
+ * The entry fields the Content API accepts as query options. Everything else on
41
+ * a data entry is routing metadata (`redirect`) or unsupported (`fields`), and
42
+ * is deliberately not forwarded.
43
+ */
44
+ const ALLOWED_QUERY_OPTIONS = ['limit', 'order', 'filter', 'include', 'slug', 'visibility', 'status', 'page'];
45
+
46
+ function unknownResource(resource: string) {
47
+ return new errors.IncorrectUsageError({message: `Unknown route data resource: ${resource}`});
48
+ }
49
+
50
+ // Shorthand names a resource the short way (`tag`), long form the way the API
51
+ // does (`tags`). Both land on the same config.
52
+ function splitShortForm(shortForm: DataShortForm): [string, string] {
53
+ const [key, slug] = shortForm.split('.');
54
+ return [key, slug];
55
+ }
56
+
57
+ function shortFormConfig(key: string): ResourceConfig | undefined {
58
+ return RESOURCE_CONFIGS[key];
59
+ }
60
+
61
+ function longFormConfig(resource: string): ResourceConfig | undefined {
62
+ return Object.values(RESOURCE_CONFIGS).find(config => config?.resource === resource);
63
+ }
64
+
65
+ function resolveShortForm(shortForm: DataShortForm): ApiCallSpec {
66
+ const [key, slug] = splitShortForm(shortForm);
67
+ const config = shortFormConfig(key);
68
+
69
+ if (!config?.type) {
70
+ throw unknownResource(key);
71
+ }
72
+
73
+ return {
74
+ controller: config.controller,
75
+ type: config.type,
76
+ resource: config.resource,
77
+ options: {...config.options, slug}
78
+ };
79
+ }
80
+
81
+ function resolveLongForm(entry: DataLongFormEntry): ApiCallSpec {
82
+ const config = longFormConfig(entry.resource);
83
+
84
+ if (!config) {
85
+ throw unknownResource(entry.resource);
86
+ }
87
+
88
+ const options: Record<string, unknown> = _.pick(entry, ALLOWED_QUERY_OPTIONS);
89
+
90
+ return {
91
+ controller: config.controller,
92
+ type: entry.type,
93
+ resource: config.resource,
94
+ // A `read` targets one known resource, so the resource defaults (e.g.
95
+ // `visibility: public` for tags) apply. A `browse` is author-driven and
96
+ // takes only what the entry asked for.
97
+ options: entry.type === 'read' ? _.defaults(options, config.options) : options
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Resolve a single route data entry into a Content API call.
103
+ *
104
+ * This is the one place that knows how a domain `resource` maps onto a Ghost API
105
+ * controller — routes.yaml talks about `tags`, the API is reached via
106
+ * `tagsPublic`. Callers work with the domain model and never name a controller.
107
+ *
108
+ * @example resolveApiCall('tag.food')
109
+ * // => {controller: 'tagsPublic', type: 'read', resource: 'tags', options: {slug: 'food', visibility: 'public'}}
110
+ */
111
+ export function resolveApiCall(entry: DataEntry): ApiCallSpec {
112
+ return typeof entry === 'string' ? resolveShortForm(entry) : resolveLongForm(entry);
113
+ }
114
+
115
+ /**
116
+ * Resolve a route's whole `data` block, keyed the way the theme sees it.
117
+ *
118
+ * Top-level shorthand (`data: tag.food`) has no author-given name, so it is
119
+ * keyed by its resource shorthand — `tag` — matching what themes already expect.
120
+ */
121
+ export function resolveRouteData(routeData?: RouteData): Record<string, ApiCallSpec> {
122
+ if (!routeData) {
123
+ return {};
124
+ }
125
+
126
+ if (typeof routeData === 'string') {
127
+ const [key] = splitShortForm(routeData);
128
+
129
+ return {[key]: resolveApiCall(routeData)};
130
+ }
131
+
132
+ return _.mapValues(routeData, entry => resolveApiCall(entry));
133
+ }
134
+
135
+ /**
136
+ * The resource read a data entry claims, or `null` if it claims none.
137
+ *
138
+ * Only a `read` names a single resource; a `browse` pulls a set and claims
139
+ * nothing. Unlike {@link resolveApiCall} this answers a routing question, so an
140
+ * entry it cannot make sense of is simply not a claim rather than an error.
141
+ *
142
+ * @example resolveResourceRead('tag.food') // => {resource: 'tags', slug: 'food'}
143
+ */
144
+ export function resolveResourceRead(entry: DataEntry): ResourceRead | null {
145
+ if (typeof entry === 'string') {
146
+ const [key, slug] = splitShortForm(entry);
147
+ const config = shortFormConfig(key);
148
+
149
+ return config?.type === 'read' ? {resource: config.resource, slug} : null;
150
+ }
151
+
152
+ if (entry.type !== 'read') {
153
+ return null;
154
+ }
155
+
156
+ return longFormConfig(entry.resource) ? {resource: entry.resource, slug: entry.slug} : null;
157
+ }
@@ -5,6 +5,7 @@ const ParentRouter = require('./parent-router');
5
5
  const controllers = require('./controllers');
6
6
  const middleware = require('./middleware');
7
7
  const RSSRouter = require('./rss-router');
8
+ const {toExpressNotation} = require('./permalink-adapter');
8
9
 
9
10
  /**
10
11
  * @description Collection Router for post resource.
@@ -26,15 +27,16 @@ class CollectionRouter extends ParentRouter {
26
27
 
27
28
  this.rss = object.rss !== false;
28
29
 
30
+ // convert domain `{slug}` -> Express/URL-service `:slug` at this boundary
29
31
  this.permalinks = {
30
- value: object.permalink
32
+ value: toExpressNotation(object.permalink)
31
33
  };
32
34
 
33
35
  // @NOTE: see renderer/templates - we use unshift to prepend the templates
34
36
  this.templates = (object.templates || []).reverse();
35
37
 
36
38
  this.filter = object.filter;
37
- this.data = object.data || {query: {}, router: {}};
39
+ this.data = object.data || {};
38
40
  this.order = object.order;
39
41
  this.limit = object.limit;
40
42
 
@@ -116,7 +118,7 @@ class CollectionRouter extends ParentRouter {
116
118
  templates: this.templates,
117
119
  identifier: this.identifier,
118
120
  name: this.routerName,
119
- data: this.data.query
121
+ data: this.data
120
122
  };
121
123
 
122
124
  next();
@@ -8,7 +8,7 @@ module.exports = {
8
8
  },
9
9
 
10
10
  get rss() {
11
- return require('./rss');
11
+ return require('./rss').rssController;
12
12
  },
13
13
 
14
14
  get previews() {
@@ -1,52 +1,47 @@
1
- const _ = require('lodash');
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rssController = rssController;
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ const node_url_1 = __importDefault(require("node:url"));
2
9
  const debug = require('@tryghost/debug')('services:routing:controllers:rss');
3
- const url = require('url');
4
10
  const security = require('@tryghost/security');
5
11
  const settingsCache = require('../../../../shared/settings-cache');
6
12
  const rssService = require('../../rss');
7
13
  const renderer = require('../../rendering');
8
14
  const dataService = require('../../data');
9
-
10
- // @TODO: is this really correct? Should we be using meta data title?
15
+ /**
16
+ * Generates the title for the RSS feed based on author, tag, or publication settings.
17
+ */
11
18
  function getTitle(relatedData) {
12
- relatedData = relatedData || {};
13
- let titleStart = _.get(relatedData, 'author[0].name') || _.get(relatedData, 'tag[0].name') || '';
14
-
19
+ const data = relatedData || {};
20
+ let titleStart = lodash_1.default.get(data, 'author[0].name') || lodash_1.default.get(data, 'tag[0].name') || '';
15
21
  titleStart += titleStart ? ' - ' : '';
16
22
  return titleStart + settingsCache.get('title');
17
23
  }
18
-
19
24
  /**
20
- * @description RSS controller.
21
- *
22
- * @TODO: The collection controller does almost the same. Merge!
23
- * @param {Object} req
24
- * @param {Object} res
25
- * @param {Function} next
25
+ * RSS routing controller.
26
+ * Fetches posts/meta data and delegates rendering to `rssService`.
26
27
  */
27
- module.exports = function rssController(req, res, next) {
28
+ function rssController(req, res, next) {
28
29
  debug('rssController');
29
-
30
30
  const pathOptions = {
31
31
  page: 1, // required for fetchData
32
32
  slug: req.params.slug ? security.string.safe(req.params.slug) : undefined
33
33
  };
34
-
35
34
  // CASE: Ghost is using an rss cache - normalize the URL for use as a key
36
- // @TODO: This belongs to the rss service O_o
37
- const baseUrl = url.parse(req.originalUrl).pathname;
38
-
39
- dataService.fetchData(pathOptions, res.routerOptions, res.locals)
35
+ const baseUrl = node_url_1.default.parse(req.originalUrl).pathname;
36
+ return dataService.fetchData(pathOptions, res.routerOptions, res.locals)
40
37
  .then(function formatResult(result) {
41
- const response = _.pick(result, ['posts', 'meta']);
42
-
43
- response.title = getTitle(result.data);
44
- response.description = settingsCache.get('description');
45
-
46
- return response;
47
- })
38
+ const response = lodash_1.default.pick(result, ['posts', 'meta']);
39
+ response.title = getTitle(result.data);
40
+ response.description = settingsCache.get('description');
41
+ return response;
42
+ })
48
43
  .then(function (data) {
49
- return rssService.render(res, baseUrl, data);
50
- })
44
+ return rssService.render(res, baseUrl, data);
45
+ })
51
46
  .catch(renderer.handleError(next));
52
- };
47
+ }
@@ -0,0 +1,66 @@
1
+ import type {Request, Response, NextFunction} from 'express';
2
+ import _ from 'lodash';
3
+ import url from 'node:url';
4
+
5
+ const debug = require('@tryghost/debug')('services:routing:controllers:rss');
6
+ const security = require('@tryghost/security');
7
+ const settingsCache = require('../../../../shared/settings-cache');
8
+ const rssService = require('../../rss');
9
+ const renderer = require('../../rendering');
10
+ const dataService = require('../../data');
11
+
12
+ export interface RSSPathOptions {
13
+ page: number;
14
+ slug?: string;
15
+ }
16
+
17
+ export interface RSSResponse extends Response {
18
+ routerOptions: Record<string, unknown>;
19
+ }
20
+
21
+ export interface RelatedData {
22
+ author?: Array<{name: string}>;
23
+ tag?: Array<{name: string}>;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ /**
28
+ * Generates the title for the RSS feed based on author, tag, or publication settings.
29
+ */
30
+ function getTitle(relatedData?: RelatedData): string {
31
+ const data = relatedData || {};
32
+ let titleStart = _.get(data, 'author[0].name') || _.get(data, 'tag[0].name') || '';
33
+
34
+ titleStart += titleStart ? ' - ' : '';
35
+ return titleStart + settingsCache.get('title');
36
+ }
37
+
38
+ /**
39
+ * RSS routing controller.
40
+ * Fetches posts/meta data and delegates rendering to `rssService`.
41
+ */
42
+ export function rssController(req: Request, res: RSSResponse, next: NextFunction): Promise<void> {
43
+ debug('rssController');
44
+
45
+ const pathOptions: RSSPathOptions = {
46
+ page: 1, // required for fetchData
47
+ slug: req.params.slug ? security.string.safe(req.params.slug) : undefined
48
+ };
49
+
50
+ // CASE: Ghost is using an rss cache - normalize the URL for use as a key
51
+ const baseUrl = url.parse(req.originalUrl).pathname;
52
+
53
+ return dataService.fetchData(pathOptions, res.routerOptions, res.locals)
54
+ .then(function formatResult(result: {posts?: unknown[]; meta?: unknown; data?: RelatedData}) {
55
+ const response = _.pick(result, ['posts', 'meta']) as Record<string, unknown>;
56
+
57
+ response.title = getTitle(result.data);
58
+ response.description = settingsCache.get('description');
59
+
60
+ return response;
61
+ })
62
+ .then(function (data: Record<string, unknown>) {
63
+ return rssService.render(res, baseUrl, data);
64
+ })
65
+ .catch(renderer.handleError(next));
66
+ }
@@ -1,6 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('services:routing:controllers:static');
3
3
  const renderer = require('../../rendering');
4
+ const {resolveRouteData} = require('../api-adapter');
4
5
 
5
6
  function processQuery(query, locals) {
6
7
  const api = require('../../proxy').api;
@@ -35,10 +36,10 @@ function processQuery(query, locals) {
35
36
  module.exports = function staticController(req, res, next) {
36
37
  debug('staticController', res.routerOptions);
37
38
 
38
- let promises = [];
39
+ const apiCalls = resolveRouteData(res.routerOptions.data);
39
40
 
40
- _.each(res.routerOptions.data, function (query) {
41
- promises.push(processQuery(query, res.locals));
41
+ const promises = _.map(apiCalls, function (apiCall) {
42
+ return processQuery(apiCall, res.locals);
42
43
  });
43
44
 
44
45
  return Promise.all(promises)
@@ -49,13 +50,13 @@ module.exports = function staticController(req, res, next) {
49
50
  response.data = {};
50
51
 
51
52
  let resultIndex = 0;
52
- _.each(res.routerOptions.data, function (config, name) {
53
- response.data[name] = result[resultIndex][config.resource];
53
+ _.each(apiCalls, function (apiCall, name) {
54
+ response.data[name] = result[resultIndex][apiCall.resource];
54
55
 
55
- if (config.type === 'browse') {
56
+ if (apiCall.type === 'browse') {
56
57
  response.data[name].meta = result[resultIndex].meta;
57
58
  // @TODO: remove in Ghost 3.0 (see https://github.com/TryGhost/Ghost/issues/10434)
58
- response.data[name][config.resource] = result[resultIndex][config.resource];
59
+ response.data[name][apiCall.resource] = result[resultIndex][apiCall.resource];
59
60
  }
60
61
 
61
62
  resultIndex = resultIndex + 1;
@@ -13,6 +13,7 @@ const express = require('../../../shared/express');
13
13
  const _ = require('lodash');
14
14
  const url = require('url');
15
15
  const security = require('@tryghost/security');
16
+ const {resolveResourceRead} = require('./api-adapter');
16
17
  const urlUtils = require('../../../shared/url-utils').default;
17
18
  const registry = require('./registry');
18
19
 
@@ -197,6 +198,11 @@ class ParentRouter {
197
198
 
198
199
  /**
199
200
  * @description Figure out if the router has a redirect enabled.
201
+ *
202
+ * A route claims a resource by reading it: `data: tag.bacon` on this router
203
+ * means /tag/bacon/ should redirect here from wherever else it lives. Only
204
+ * `read` entries claim a slug, and an entry can opt out with `redirect: false`.
205
+ *
200
206
  * @param {string} routerType
201
207
  * @param {string} slug
202
208
  * @returns {boolean}
@@ -204,14 +210,21 @@ class ParentRouter {
204
210
  isRedirectEnabled(routerType, slug) {
205
211
  debug('isRedirectEnabled', this.name, this.route && this.route.value, routerType, slug);
206
212
 
207
- if (!this.data || !Object.keys(this.data.router)) {
213
+ if (!this.data) {
208
214
  return false;
209
215
  }
210
216
 
211
- return _.find(this.data.router, function (entries, type) {
212
- if (routerType === type) {
213
- return _.find(entries, {redirect: true, slug: slug});
217
+ // Top-level shorthand (`data: tag.bacon`) is a single unnamed entry.
218
+ const entries = typeof this.data === 'string' ? [this.data] : Object.values(this.data);
219
+
220
+ return entries.some((entry) => {
221
+ if (typeof entry !== 'string' && entry.redirect === false) {
222
+ return false;
214
223
  }
224
+
225
+ const read = resolveResourceRead(entry);
226
+
227
+ return !!read && read.resource === routerType && read.slug === slug;
215
228
  });
216
229
  }
217
230