ghost 6.37.1 → 6.38.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 (258) hide show
  1. package/components/tryghost-i18n-0.0.0.tgz +0 -0
  2. package/core/boot.js +13 -9
  3. package/core/bridge.js +10 -6
  4. package/core/built/admin/assets/{PolarAngleAxis-Cavik65s.js → PolarAngleAxis-CB-FJBDa.js} +1 -1
  5. package/core/built/admin/assets/{_baseAssignValue-CnqN6ZMT.js → _baseAssignValue-BEAjPAfy.js} +1 -1
  6. package/core/built/admin/assets/{a-large-small-Wi4LtZX-.js → a-large-small-L1BJa6mG.js} +1 -1
  7. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  8. package/core/built/admin/assets/admin-x-settings/{code-editor-view-Cf8T5Jfd.mjs → code-editor-view-bOZzJ3a1.mjs} +2 -2
  9. package/core/built/admin/assets/admin-x-settings/{index-BMWEh-vR.mjs → index-BRuMxaCz.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-Aj-hFSnY.mjs → index-CTAQj489.mjs} +5 -5
  11. package/core/built/admin/assets/admin-x-settings/{index-DjBJrXYn.mjs → index-WoKKBOe4.mjs} +2 -2
  12. package/core/built/admin/assets/admin-x-settings/{modals-8WpWZZdI.mjs → modals-BQ5M-RTA.mjs} +3 -3
  13. package/core/built/admin/assets/{arrow-right-BiAOKOto.js → arrow-right-B2j-ecEK.js} +1 -1
  14. package/core/built/admin/assets/{at-sign-z5gBMxDW.js → at-sign-CtAw8_lw.js} +1 -1
  15. package/core/built/admin/assets/{audience-DxNilLIv.js → audience-CvSOqHuo.js} +1 -1
  16. package/core/built/admin/assets/automations-BVUNuVOh.js +1 -0
  17. package/core/built/admin/assets/automations-Bi9RJq5E.js +1 -0
  18. package/core/built/admin/assets/{avatar-flipboard-5OzseZFV.js → avatar-flipboard-DocLTXLh.js} +1 -1
  19. package/core/built/admin/assets/{bluesky-sharing-D9YcSpW8.js → bluesky-sharing-DafX2lE1.js} +1 -1
  20. package/core/built/admin/assets/chart-BRQeD9eh.js +67 -0
  21. package/core/built/admin/assets/{chevron-up-DVt6fhV3.js → chevron-up-v4_QwmnL.js} +1 -1
  22. package/core/built/admin/assets/{chunk.524.fcd26ef812d73e2b0620.js → chunk.524.c3d844070c24d7984fd7.js} +4 -4
  23. package/core/built/admin/assets/{chunk.582.b61ffaad1c2fa4aba820.js → chunk.582.0e0ede1eac12926acceb.js} +6 -6
  24. package/core/built/admin/assets/{chunk.383.adb5a3fe32b6b98c43ff.js → chunk.995.4b32363ab39da3e7f5d7.js} +3 -3
  25. package/core/built/admin/assets/circle-alert-Dbz54fYb.js +1 -0
  26. package/core/built/admin/assets/{code-editor-view-CbQIqTrK.js → code-editor-view-D-pYCcdu.js} +1 -1
  27. package/core/built/admin/assets/comments-DSv09jRn.js +1 -0
  28. package/core/built/admin/assets/{content-helpers-jANWGIfA.js → content-helpers-DQdPPDGb.js} +1 -1
  29. package/core/built/admin/assets/{copy-BBLeLEnG.js → copy-BULQsPZ4.js} +1 -1
  30. package/core/built/admin/assets/{data-list-BveeOSGI.js → data-list-WxXC0iKE.js} +1 -1
  31. package/core/built/admin/assets/{deleted-feed-item-DupAEk4P.js → deleted-feed-item-ZBLfgRkK.js} +1 -1
  32. package/core/built/admin/assets/{dropzone-D3PSdAHN.js → dropzone-B8mAoWrn.js} +1 -1
  33. package/core/built/admin/assets/{edit-profile-NrCR9_bi.js → edit-profile-Bj3th8H0.js} +1 -1
  34. package/core/built/admin/assets/editor-BZV40eAE.css +1 -0
  35. package/core/built/admin/assets/editor-u20VuPzn.js +7 -0
  36. package/core/built/admin/assets/{empty-indicator-Dhi_KeYX.js → empty-indicator-P3IsUguf.js} +1 -1
  37. package/core/built/admin/assets/{en-BontI3SP.js → en-C9bUnuy7.js} +1 -1
  38. package/core/built/admin/assets/{feed-B3Kfiz5R.js → feed-B94mO944.js} +1 -1
  39. package/core/built/admin/assets/{filter-query-core-Dwh4Qulu.js → filter-query-core-CM0KKIr2.js} +1 -1
  40. package/core/built/admin/assets/filters-D52FTE0z.js +1 -0
  41. package/core/built/admin/assets/gh-chart-Xa6Nvzwz.js +1 -0
  42. package/core/built/admin/assets/{ghost-c25434d5bbf5a5429883d4c3590f1946.js → ghost-82f1c4ffe0e1cd058ce4b66b5253b620.js} +159 -201
  43. package/core/built/admin/assets/{ghost-dark-52df27573e5f2a75e6cf4b1d06eea9e4.css → ghost-dark-9897cf5102772faedf566e5e54d4fbee.css} +1 -1
  44. package/core/built/admin/assets/{ghost-fcf460028e04192f392f16e173f2e3a8.css → ghost-e11f1814be0f38e98bb77b49eba11178.css} +1 -1
  45. package/core/built/admin/assets/{growth-DRH8zUIJ.js → growth-D3TW3JIB.js} +1 -1
  46. package/core/built/admin/assets/{hash-UvX5iOa9.js → hash-CstXS-iB.js} +1 -1
  47. package/core/built/admin/assets/{header-G1a38jnf.js → header-StkTITw-.js} +1 -1
  48. package/core/built/admin/assets/{inbox-By7HZskI.js → inbox-D1RM3kVF.js} +1 -1
  49. package/core/built/admin/assets/index-BEkJ9xtY.css +1 -0
  50. package/core/built/admin/assets/{index-CS_ZmI8F.js → index-BJx5Uas5.js} +1 -1
  51. package/core/built/admin/assets/index-BKzg6OE7.js +1 -0
  52. package/core/built/admin/assets/{index-D7JyKf1m.js → index-BM-MBdD0.js} +1 -1
  53. package/core/built/admin/assets/index-BRzvVuXt.js +2 -0
  54. package/core/built/admin/assets/index-B_3xqeM9.js +1 -0
  55. package/core/built/admin/assets/{index-ydyrczVl.js → index-C-OdR2wJ.js} +1 -1
  56. package/core/built/admin/assets/{index-X-MG1ikg.js → index-C59QlJqT.js} +1 -1
  57. package/core/built/admin/assets/{index-CJo0Qh4V.js → index-CCq_1tCo.js} +1 -1
  58. package/core/built/admin/assets/{index-qHUvPjQB.js → index-CYyEyBYw.js} +11 -11
  59. package/core/built/admin/assets/{index-CX4ztLSk.js → index-DHbiBjyq.js} +1 -1
  60. package/core/built/admin/assets/{index-BP6hBbPK.js → index-DTVgSMyL.js} +2 -2
  61. package/core/built/admin/assets/{index-vZMbI8e8.js → index-DiUjEgRw.js} +1 -1
  62. package/core/built/admin/assets/{index-YM4ZplGj.js → index-DugxKzA6.js} +1 -1
  63. package/core/built/admin/assets/{index-D8InZCr2.js → index-DzVHlhO1.js} +1 -1
  64. package/core/built/admin/assets/{index-D4b7U6K-.js → index-LsaD9Cq7.js} +1 -1
  65. package/core/built/admin/assets/{index-DRUPF8nN.js → index-x0hNfyqU.js} +1 -1
  66. package/core/built/admin/assets/{inline-Bc9zHR7e.js → inline-B4cWemrJ.js} +1 -1
  67. package/core/built/admin/assets/{koenig-lexical-B-ZnqRE1.js → koenig-lexical-BOdeH3QJ.js} +1 -1
  68. package/core/built/admin/assets/{kpi-card-zyi5Onb0.js → kpi-card-P7zSpcx0.js} +1 -1
  69. package/core/built/admin/assets/{kpi-card-CzlPIEPE.js → kpi-card-plhYOEtA.js} +1 -1
  70. package/core/built/admin/assets/{kpi-tabs-Dsa5ldXK.js → kpi-tabs-DZw2Qktb.js} +1 -1
  71. package/core/built/admin/assets/{kpis-Cw5ZRQhk.js → kpis-CDHM0xhW.js} +1 -1
  72. package/core/built/admin/assets/{label-BvxmsD1M.js → label-Ckacdx14.js} +1 -1
  73. package/core/built/admin/assets/{links-CiS5BPKS.js → links-BtXE5d0Y.js} +1 -1
  74. package/core/built/admin/assets/{list-filter-zmkB7rg4.js → list-filter-DLjjiktn.js} +1 -1
  75. package/core/built/admin/assets/{list-header-Cp779e3H.js → list-header-Cc7Ia3nr.js} +1 -1
  76. package/core/built/admin/assets/loader-circle-DLJd-dNm.js +1 -0
  77. package/core/built/admin/assets/{mail-i2qs8CIG.js → mail-B97w28e2.js} +1 -1
  78. package/core/built/admin/assets/{main-layout-KCvKOX8F.js → main-layout-bwWvzc9B.js} +1 -1
  79. package/core/built/admin/assets/{members-w601v5JJ.js → members-CokS8MI2.js} +5 -5
  80. package/core/built/admin/assets/{message-square-text-bG9IoWIt.js → message-square-text-D3rsjDFR.js} +1 -1
  81. package/core/built/admin/assets/minus-BzXQVmMw.js +1 -0
  82. package/core/built/admin/assets/{modals-lNzhiL0o.js → modals-D0lip-iy.js} +2 -2
  83. package/core/built/admin/assets/{moderation-CyfNRSWj.js → moderation-Dv7AadCV.js} +1 -1
  84. package/core/built/admin/assets/newsletter-BazC5rIj.js +1 -0
  85. package/core/built/admin/assets/{newsletters-B8xSXSr_.js → newsletters-m6HQgU5J.js} +1 -1
  86. package/core/built/admin/assets/{note-dNfKEpxw.js → note-CCGiEKz-.js} +1 -1
  87. package/core/built/admin/assets/{onboarding-route-Drbw0QtT.js → onboarding-route-Cbt7qWNE.js} +1 -1
  88. package/core/built/admin/assets/overview-TN50tzUh.js +1 -0
  89. package/core/built/admin/assets/{pagemenu-VJXMoMB4.js → pagemenu-BXrHWtsU.js} +1 -1
  90. package/core/built/admin/assets/{pencil-DHNmlLjO.js → pencil-BL1r8KVy.js} +1 -1
  91. package/core/built/admin/assets/{post-analytics-PsqLsS2e.js → post-analytics-BqEXi6Ps.js} +1 -1
  92. package/core/built/admin/assets/{post-analytics-context-QLZsZCFg.js → post-analytics-context-DekYNlb3.js} +1 -1
  93. package/core/built/admin/assets/{post-analytics-header-PecXsIT-.js → post-analytics-header-AkNbn6uc.js} +1 -1
  94. package/core/built/admin/assets/{post-share-modal-B4Rvo7W1.js → post-share-modal-CXDGVKoQ.js} +1 -1
  95. package/core/built/admin/assets/posts/{app-utils-DIc5TmxO.mjs → app-utils-DxA7edI_.mjs} +2 -2
  96. package/core/built/admin/assets/posts/{automations-BZpT35kJ.mjs → automations-Cwu_gHl4.mjs} +64 -67
  97. package/core/built/admin/assets/posts/automations-D81hV_2c.mjs +13 -0
  98. package/core/built/admin/assets/posts/{avatar-K63cvqSS.mjs → avatar-IkjEzEjI.mjs} +19 -19
  99. package/core/built/admin/assets/posts/{button-ufGBCcsV.mjs → button-KaJZKcou.mjs} +21 -21
  100. package/core/built/admin/assets/posts/{check-IcEnuTpD.mjs → check-Cf74RvZ1.mjs} +21 -21
  101. package/core/built/admin/assets/posts/{comments-CyJU9Ums.mjs → comments-pdKhYbYG.mjs} +54 -52
  102. package/core/built/admin/assets/posts/createLucideIcon-Cj7h3r9g.mjs +320 -0
  103. package/core/built/admin/assets/posts/{dialog-D6_lBvtT.mjs → dialog-Ci3W8fjx.mjs} +71 -76
  104. package/core/built/admin/assets/posts/{dropdown-menu-BSSmAory.mjs → dropdown-menu-DZ9XCHSw.mjs} +5 -5
  105. package/core/built/admin/assets/posts/editor-Bb-xle0o.mjs +6084 -0
  106. package/core/built/admin/assets/posts/ellipsis-ChhZo9aT.mjs +10 -0
  107. package/core/built/admin/assets/posts/{empty-indicator-DvGCvRAX.mjs → empty-indicator-DnFs7hDy.mjs} +2 -2
  108. package/core/built/admin/assets/posts/{filters-BLBaHhJz.mjs → filters-CO_FUPX-.mjs} +42 -40
  109. package/core/built/admin/assets/posts/get-site-timezone-C21yKZT_.mjs +87 -0
  110. package/core/built/admin/assets/posts/{growth-Bz1ChJNB.mjs → growth-CtX9Efm2.mjs} +12 -12
  111. package/core/built/admin/assets/posts/heading-DZ_KtDTL.mjs +138 -0
  112. package/core/built/admin/assets/posts/{hooks-BEngBys9.mjs → hooks-DVhBDVlA.mjs} +3 -2
  113. package/core/built/admin/assets/posts/{index-BAF0YXsp.mjs → index-B4db8w_H.mjs} +181 -172
  114. package/core/built/admin/assets/posts/{inline-C2YFQAdU.mjs → inline-BfB-SNk0.mjs} +2 -2
  115. package/core/built/admin/assets/posts/{input-surface-TqJlNSp_.mjs → input-surface-CNwZJloi.mjs} +2 -2
  116. package/core/built/admin/assets/posts/{kpis-DAX5_0-Q.mjs → kpis-BvY_5Y1u.mjs} +8792 -8803
  117. package/core/built/admin/assets/posts/{links-A1YTsBbF.mjs → links-Ck-h7SKg.mjs} +57 -57
  118. package/core/built/admin/assets/posts/{loading-indicator-BZsjmp8g.mjs → loading-indicator-B_rU9Lie.mjs} +3 -3
  119. package/core/built/admin/assets/posts/mail-DiZ3oKEW.mjs +9 -0
  120. package/core/built/admin/assets/posts/{main-layout-D_HHTP_n.mjs → main-layout-aYo6qOBZ.mjs} +2 -2
  121. package/core/built/admin/assets/posts/{newsletter-BRzgjOIa.mjs → newsletter-BQc53TF2.mjs} +31 -30
  122. package/core/built/admin/assets/posts/{overview-C41VFjn7.mjs → overview-blz9Ak9q.mjs} +42 -41
  123. package/core/built/admin/assets/posts/plus-YVjtYaK3.mjs +15 -0
  124. package/core/built/admin/assets/posts/{post-analytics-DGgMkFaf.mjs → post-analytics-BQbWGoeu.mjs} +6 -6
  125. package/core/built/admin/assets/posts/{post-analytics-context-B1r3HBrp.mjs → post-analytics-context-Be7THgnd.mjs} +7 -7
  126. package/core/built/admin/assets/posts/{post-analytics-header-DTWSO0Aq.mjs → post-analytics-header-iDFy7Bao.mjs} +2762 -2764
  127. package/core/built/admin/assets/posts/{post-share-modal-ohHwk0Yt.mjs → post-share-modal-akP16idN.mjs} +50 -48
  128. package/core/built/admin/assets/posts/posts.js +2 -1
  129. package/core/built/admin/assets/posts/{settings-CZYL6Jhr.mjs → settings-BdQhfHxY.mjs} +2 -2
  130. package/core/built/admin/assets/posts/{sheet-D7YesQJE.mjs → sheet-3dBlKeXf.mjs} +14 -13
  131. package/core/built/admin/assets/posts/{skeleton-S1k58YKa.mjs → skeleton-DMgSvYqr.mjs} +11 -11
  132. package/core/built/admin/assets/posts/{source-icon-DnpCy5N4.mjs → source-icon-Bp5Qxq1E.mjs} +4 -4
  133. package/core/built/admin/assets/posts/{stats-CbOpowXb.mjs → stats-DJx9qc_u.mjs} +4 -4
  134. package/core/built/admin/assets/posts/{table-Cw3Wxf1C.mjs → table-BpVwuzeH.mjs} +2 -2
  135. package/core/built/admin/assets/posts/{data-list-BnZARP88.mjs → tabs-CbZCpxfI.mjs} +4885 -5284
  136. package/core/built/admin/assets/posts/{tags-BoMt6Ce3.mjs → tags-CNtvDS6f.mjs} +33 -33
  137. package/core/built/admin/assets/posts/{tags-CNdsvTtZ.mjs → tags-DFOoFZ_3.mjs} +2 -2
  138. package/core/built/admin/assets/posts/{tooltip-CTcyINxz.mjs → tooltip-Cc_09RDU.mjs} +106 -117
  139. package/core/built/admin/assets/posts/value-CwGM7M-1.mjs +410 -0
  140. package/core/built/admin/assets/posts/{virtual-list-window-Bs88yrut.mjs → virtual-list-window-D6nRoL0y.mjs} +5 -5
  141. package/core/built/admin/assets/posts/{web-Bsd8eTP6.mjs → web-yfPssEt8.mjs} +1042 -1040
  142. package/core/built/admin/assets/posts/x-DrUGcpfp.mjs +9 -0
  143. package/core/built/admin/assets/posts/zap-DAN0ur-o.mjs +24 -0
  144. package/core/built/admin/assets/posts-D-jyPPE6.js +1 -0
  145. package/core/built/admin/assets/power-Cc5ncYVl.js +1 -0
  146. package/core/built/admin/assets/{referrers-kcBT5smx.js → referrers-qnXsKsuv.js} +1 -1
  147. package/core/built/admin/assets/{repeat-Cn2sKd-L.js → repeat-BGIBynZH.js} +1 -1
  148. package/core/built/admin/assets/{reply-D7E-ZMd5.js → reply-DOKdAsL_.js} +1 -1
  149. package/core/built/admin/assets/{rocket-DnH0XAGW.js → rocket-CAN2NB82.js} +1 -1
  150. package/core/built/admin/assets/{select-f0MMMQWu.js → select-8S9kYGSM.js} +1 -1
  151. package/core/built/admin/assets/{settings-BAQNP3gU.js → settings-DeD-bmZc.js} +4 -4
  152. package/core/built/admin/assets/{settings-ORttG1jT.js → settings-e4IB9sx6.js} +1 -1
  153. package/core/built/admin/assets/{share-modal-CXU4tGY8.js → share-modal-CBavk20I.js} +1 -1
  154. package/core/built/admin/assets/{sort-button-BXMrChFu.js → sort-button-DlSKBzUq.js} +1 -1
  155. package/core/built/admin/assets/{source-icon-cjy79jGT.js → source-icon-27VIJSXr.js} +1 -1
  156. package/core/built/admin/assets/{sprout-DZ4tZ78G.js → sprout-D66nqc79.js} +1 -1
  157. package/core/built/admin/assets/{square-vOzqJf7x.js → square-DccaXQWe.js} +1 -1
  158. package/core/built/admin/assets/stats/{audience-5NeJICtP.mjs → audience-B84ClTDM.mjs} +3 -3
  159. package/core/built/admin/assets/stats/{content-helpers-DFSTl3uB.mjs → content-helpers-CBAddUg7.mjs} +4 -4
  160. package/core/built/admin/assets/stats/{index-C4T7u699.mjs → index-Bz5oVpED.mjs} +5 -5
  161. package/core/built/admin/assets/stats/{index-B8bp6qe2.mjs → index-Cs8i1uR4.mjs} +74 -75
  162. package/core/built/admin/assets/stats/{index-wvi17m79.mjs → index-DEsP_Tbd.mjs} +8 -8
  163. package/core/built/admin/assets/stats/{index-CFfAYHdD.mjs → index-Ds-pM47x.mjs} +5 -5
  164. package/core/built/admin/assets/stats/{index-Chqk7VlR.mjs → index-Wzk1IcAz.mjs} +6 -6
  165. package/core/built/admin/assets/stats/{kpi-tabs-Df9yOI2P.mjs → kpi-tabs-Cs4l8B_M.mjs} +7 -7
  166. package/core/built/admin/assets/stats/{sort-button-Doi8OgMU.mjs → sort-button-rR-GA9Hu.mjs} +3 -3
  167. package/core/built/admin/assets/stats/{stats-FXeCefyd.mjs → stats-RFpEJpEq.mjs} +34 -34
  168. package/core/built/admin/assets/stats/stats.js +1 -1
  169. package/core/built/admin/assets/stats/{use-growth-stats-93C_3EZk.mjs → use-growth-stats-DwgFzbq9.mjs} +3 -3
  170. package/core/built/admin/assets/{stats-CIGz2Win.js → stats-B4OybenW.js} +1 -1
  171. package/core/built/admin/assets/{stats-view-BTKRxYTQ.js → stats-view-BuExwFXa.js} +1 -1
  172. package/core/built/admin/assets/{step-1-BSMHbT-q.js → step-1-D_5LOQrC.js} +1 -1
  173. package/core/built/admin/assets/{step-2-_YC0LCZv.js → step-2-BZLBj8cj.js} +1 -1
  174. package/core/built/admin/assets/{step-3-DWMqP2bO.js → step-3-s5uNbcOH.js} +1 -1
  175. package/core/built/admin/assets/{table-mHKjXQba.js → table-pGS7EeJe.js} +1 -1
  176. package/core/built/admin/assets/{tabs-B93IVRBz.js → tabs-CjdKssUe.js} +1 -1
  177. package/core/built/admin/assets/{tags-DCb-x2ZB.js → tags-B0vRk0A6.js} +1 -1
  178. package/core/built/admin/assets/{tags-kUHJ1MZo.js → tags-Dso4-FA_.js} +1 -1
  179. package/core/built/admin/assets/{textarea-DswCtM7_.js → textarea-DzFnun0_.js} +1 -1
  180. package/core/built/admin/assets/{tiers-DueraQKd.js → tiers-4Vkr_D-L.js} +1 -1
  181. package/core/built/admin/assets/{toggle-group-ChTo8MPD.js → toggle-group-hDsr47IB.js} +1 -1
  182. package/core/built/admin/assets/{topic-filter-DrW3fYWo.js → topic-filter-C7TpPe1G.js} +1 -1
  183. package/core/built/admin/assets/{trash-B2ESGftj.js → trash-DCVuk7ZX.js} +1 -1
  184. package/core/built/admin/assets/{underline-vo-bPRac.js → underline-CjK8la2G.js} +1 -1
  185. package/core/built/admin/assets/{upload-DXQRkBBq.js → upload-CPYhmtWZ.js} +1 -1
  186. package/core/built/admin/assets/{use-growth-stats-CoB8-lsd.js → use-growth-stats-CaFSgp_R.js} +1 -1
  187. package/core/built/admin/assets/{use-simple-pagination-Y_Wv2N6g.js → use-simple-pagination-D0vJBMvF.js} +1 -1
  188. package/core/built/admin/assets/{user-round-check-Dp9a2x-e.js → user-round-check-CzQQeFd8.js} +1 -1
  189. package/core/built/admin/assets/{user-round-x-DwCTGlxc.js → user-round-x-BDFm8yQj.js} +1 -1
  190. package/core/built/admin/assets/value-BGBiTkF3.js +1 -0
  191. package/core/built/admin/assets/{vendor-17f3b979ba6f3898d1a8c5249cc22ff1.js → vendor-ce3fc901ccbcf37f36eb791b4e454138.js} +29 -31
  192. package/core/built/admin/assets/{virtual-list-window-BF7IvGMC.js → virtual-list-window-EKtWZ5cP.js} +1 -1
  193. package/core/built/admin/assets/{wallet-cards-IEmcxJvG.js → wallet-cards-C1NOBbWE.js} +1 -1
  194. package/core/built/admin/assets/web-DCA7HR1S.js +1 -0
  195. package/core/built/admin/index.html +8 -8
  196. package/core/frontend/helpers/social_accounts.js +91 -0
  197. package/core/frontend/services/sitemap/handler.js +14 -2
  198. package/core/frontend/services/sitemap/site-map-manager.js +133 -9
  199. package/core/frontend/web/middleware/handle-image-sizes.js +4 -0
  200. package/core/server/api/endpoints/automations.js +38 -45
  201. package/core/server/models/member.js +30 -25
  202. package/core/server/services/automations/automations-api.js +58 -0
  203. package/core/server/services/automations/automations-api.ts +71 -0
  204. package/core/server/services/automations/automations-repository.js +2 -0
  205. package/core/server/services/automations/automations-repository.ts +63 -0
  206. package/core/server/services/automations/fake-database-automations-repository.js +210 -0
  207. package/core/server/services/automations/fake-database-automations-repository.ts +273 -0
  208. package/core/server/services/automations/temporary-fake-database.js +5 -4
  209. package/core/server/services/gifts/email-templates/gift-purchase-confirmation.hbs +10 -32
  210. package/core/server/services/gifts/email-templates/gift-purchase-confirmation.js +6 -8
  211. package/core/server/services/gifts/email-templates/gift-purchase-confirmation.ts +6 -8
  212. package/core/server/services/gifts/email-templates/gift-reminder.hbs +12 -37
  213. package/core/server/services/gifts/email-templates/gift-reminder.js +5 -9
  214. package/core/server/services/gifts/email-templates/gift-reminder.ts +5 -9
  215. package/core/server/services/gifts/gift-service.js +1 -1
  216. package/core/server/services/gifts/gift-service.ts +2 -2
  217. package/core/server/services/member-welcome-emails/service.js +0 -1
  218. package/core/server/services/members/members-api/controllers/router-controller.js +52 -7
  219. package/core/server/services/members/members-api/services/member-bread-service.js +0 -8
  220. package/core/server/services/route-settings/route-settings.js +18 -2
  221. package/core/server/services/staff/email-templates/gift.hbs +1 -1
  222. package/core/server/services/staff/email-templates/gift.txt.js +1 -1
  223. package/core/server/services/staff/email-templates/new-gift-subscription.hbs +1 -1
  224. package/core/server/services/staff/email-templates/new-gift-subscription.txt.js +1 -1
  225. package/core/server/services/staff/staff-service-emails.js +1 -1
  226. package/core/server/services/url/index.js +16 -1
  227. package/core/server/services/url/lazy-find-resource.js +49 -0
  228. package/core/server/services/url/lazy-find-resource.ts +62 -0
  229. package/core/server/services/url/lazy-url-service.js +220 -0
  230. package/core/server/services/url/lazy-url-service.ts +265 -0
  231. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  232. package/core/server/web/gift-preview/Inter.ttf +0 -0
  233. package/core/server/web/gift-preview/controller.js +56 -6
  234. package/core/server/web/gift-preview/gift-card-noise.png +0 -0
  235. package/core/server/web/gift-preview/gift-card-orb.png +0 -0
  236. package/core/server/web/gift-preview/image.js +170 -43
  237. package/core/shared/config/defaults.json +1 -0
  238. package/core/shared/one-at-a-time.js +28 -33
  239. package/core/shared/one-at-a-time.ts +58 -0
  240. package/package.json +18 -16
  241. package/pnpm-lock.yaml +989 -356
  242. package/core/built/admin/assets/automations-D1uMjd06.js +0 -1
  243. package/core/built/admin/assets/chart-_EXssQtm.js +0 -67
  244. package/core/built/admin/assets/comments-E00h1SbR.js +0 -1
  245. package/core/built/admin/assets/filters-CC7XfjOS.js +0 -1
  246. package/core/built/admin/assets/gh-chart-B9_ywrJJ.js +0 -1
  247. package/core/built/admin/assets/index-AsV-aY0z.css +0 -1
  248. package/core/built/admin/assets/index-B0y_ynZw.js +0 -1
  249. package/core/built/admin/assets/index-Bs6wWbLA.js +0 -2
  250. package/core/built/admin/assets/index-Cb4641XW.js +0 -1
  251. package/core/built/admin/assets/loader-circle-B5UPqSok.js +0 -1
  252. package/core/built/admin/assets/minus-DWx9VUtr.js +0 -1
  253. package/core/built/admin/assets/newsletter-fkQye9OK.js +0 -1
  254. package/core/built/admin/assets/overview-CZ-VrnXy.js +0 -1
  255. package/core/built/admin/assets/posts/createLucideIcon-DcUfTBt_.mjs +0 -454
  256. package/core/built/admin/assets/posts/get-site-timezone-DlXmHA3y.mjs +0 -93
  257. package/core/built/admin/assets/posts-Bx1RWrub.js +0 -1
  258. package/core/built/admin/assets/web-DSME3e8c.js +0 -1
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LazyUrlService = void 0;
4
+ /* eslint-disable @typescript-eslint/no-require-imports */
5
+ const nql = require('@tryghost/nql');
6
+ const debug = require('@tryghost/debug')('services:url:lazy');
7
+ const localUtils = require('../../../shared/url-utils');
8
+ // The same expansions used by UrlGenerator so users can write `tag:foo`,
9
+ // `author:jane`, etc. and have them rewritten to the underlying field paths.
10
+ const EXPANSIONS = [
11
+ { key: 'author', replacement: 'authors.slug' },
12
+ { key: 'tags', replacement: 'tags.slug' },
13
+ { key: 'tag', replacement: 'tags.slug' },
14
+ { key: 'authors', replacement: 'authors.slug' },
15
+ { key: 'primary_tag', replacement: 'primary_tag.slug' },
16
+ { key: 'primary_author', replacement: 'primary_author.slug' }
17
+ ];
18
+ // Same `page:true/false` legacy transformer the UrlGenerator uses, so old
19
+ // routes.yaml configs continue to work.
20
+ const PAGE_TRANSFORMER = nql.utils.mapKeyValues({
21
+ key: { from: 'page', to: 'type' },
22
+ values: [
23
+ { from: false, to: 'post' },
24
+ { from: true, to: 'page' }
25
+ ]
26
+ });
27
+ // Map a resource's `type` field (whatever the caller passed) onto the plural
28
+ // router resource type. We accept the singular DB column values ('post',
29
+ // 'page') as well as the plural router keys, because the migrated callers
30
+ // are inconsistent: API responses spread `attrs` (singular), but some
31
+ // helpers explicitly tag with the plural router key.
32
+ const TYPE_TO_ROUTER_TYPE = {
33
+ post: 'posts',
34
+ posts: 'posts',
35
+ page: 'pages',
36
+ pages: 'pages',
37
+ tag: 'tags',
38
+ tags: 'tags',
39
+ author: 'authors',
40
+ authors: 'authors'
41
+ };
42
+ /**
43
+ * On-demand URL service. Computes URLs and ownership per-call from the
44
+ * registered router configs instead of holding a precomputed map of every
45
+ * resource. Pure for forward lookups; resolveUrl() takes an optional DB
46
+ * lookup function so reverse lookups stay testable.
47
+ */
48
+ class LazyUrlService {
49
+ urlUtils;
50
+ findResource;
51
+ routerConfigs;
52
+ constructor({ urlUtils = localUtils, findResource = null } = {}) {
53
+ this.urlUtils = urlUtils;
54
+ this.findResource = findResource;
55
+ // Router configs in priority order. Position is the registration order.
56
+ this.routerConfigs = [];
57
+ }
58
+ onRouterAddedType(identifier, filter, resourceType, permalink) {
59
+ debug('onRouterAddedType', identifier, resourceType, permalink, filter);
60
+ const config = { identifier, filter, resourceType, permalink, nql: null };
61
+ if (filter) {
62
+ config.nql = nql(filter, { expansions: EXPANSIONS, transformer: PAGE_TRANSFORMER });
63
+ }
64
+ this.routerConfigs.push(config);
65
+ }
66
+ onRouterUpdated() {
67
+ // No state to regenerate. The next URL request reads the (possibly new)
68
+ // router config; in-flight requests that already snapshotted the old
69
+ // config keep using it, which matches the documented atomic-reload
70
+ // behaviour.
71
+ }
72
+ /**
73
+ * Drop all registered routers. Called when routes.yaml is reloaded.
74
+ */
75
+ reset() {
76
+ this.routerConfigs = [];
77
+ }
78
+ hasFinished() {
79
+ return true;
80
+ }
81
+ getUrlForResource(resource, options = {}) {
82
+ const routerType = this._routerTypeOf(resource);
83
+ if (!routerType) {
84
+ return this._formatPath('/404/', options);
85
+ }
86
+ const candidates = this.routerConfigs.filter(c => c.resourceType === routerType);
87
+ for (const config of candidates) {
88
+ // NQL filters are evaluated against the original resource (with
89
+ // its singular DB `type` field intact) because the page:true/false
90
+ // transformer rewrites to type:'post'/'page'.
91
+ if (this._matchesFilter(config, resource)) {
92
+ const path = this.urlUtils.replacePermalink(config.permalink, resource);
93
+ return this._formatPath(path, options);
94
+ }
95
+ }
96
+ return this._formatPath('/404/', options);
97
+ }
98
+ ownsResource(routerIdentifier, resource) {
99
+ const config = this.routerConfigs.find(c => c.identifier === routerIdentifier);
100
+ if (!config || !resource) {
101
+ return false;
102
+ }
103
+ const routerType = this._routerTypeOf(resource);
104
+ if (config.resourceType !== routerType) {
105
+ return false;
106
+ }
107
+ return this._matchesFilter(config, resource);
108
+ }
109
+ /**
110
+ * Translate the resource's `type` field into the plural router resource
111
+ * type (`'posts'` / `'pages'` / `'tags'` / `'authors'`). Returns `null`
112
+ * when the type is missing or unrecognised.
113
+ */
114
+ _routerTypeOf(resource) {
115
+ if (!resource || !resource.type) {
116
+ return null;
117
+ }
118
+ return TYPE_TO_ROUTER_TYPE[resource.type] || null;
119
+ }
120
+ /**
121
+ * Resolve a URL path to a resource record. Iterates router configs in
122
+ * priority order, pattern-matching the path against each permalink
123
+ * template, querying the database for a matching resource, and verifying
124
+ * any NQL filter still matches for posts.
125
+ */
126
+ async resolveUrl(urlPath) {
127
+ if (!this.findResource) {
128
+ return null;
129
+ }
130
+ for (const config of this.routerConfigs) {
131
+ const params = this._matchPermalink(config.permalink, urlPath);
132
+ if (!params) {
133
+ continue;
134
+ }
135
+ const resource = await this.findResource(config.resourceType, params);
136
+ if (!resource) {
137
+ continue;
138
+ }
139
+ // For posts/pages with NQL filters, confirm the DB record still
140
+ // satisfies the filter. The match runs against the raw record
141
+ // (with its singular `type` column) because the page:true/false
142
+ // transformer rewrites to type:'post'/'page'.
143
+ if (config.nql && !this._matchesFilter(config, resource)) {
144
+ continue;
145
+ }
146
+ return Object.assign({}, resource, { type: config.resourceType });
147
+ }
148
+ return null;
149
+ }
150
+ _matchesFilter(config, resource) {
151
+ if (!config.nql) {
152
+ return true;
153
+ }
154
+ try {
155
+ return !!config.nql.queryJSON(resource);
156
+ }
157
+ catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ debug('NQL match failed', config.filter, message);
160
+ return false;
161
+ }
162
+ }
163
+ _formatPath(path, options) {
164
+ if (options.absolute) {
165
+ return this.urlUtils.createUrl(path, options.absolute);
166
+ }
167
+ if (options.withSubdirectory) {
168
+ return this.urlUtils.createUrl(path, false, true);
169
+ }
170
+ return path;
171
+ }
172
+ /**
173
+ * Match a Ghost permalink template (e.g. `/:slug/`,
174
+ * `/:year/:month/:slug/`) against a URL path and extract the named
175
+ * fields. Ghost's RouteSettings validator rewrites `{field}` placeholders
176
+ * into `:field` form before they reach the URL service, so this parser
177
+ * works on the `:field` syntax.
178
+ *
179
+ * Implementation: walk the permalink and path one segment at a time.
180
+ * Each segment must be either a literal match or a single placeholder.
181
+ * Mixing literals and placeholders inside a segment is unsupported and
182
+ * not a documented Ghost feature; rejecting it here also keeps us out of
183
+ * regex-backtracking territory entirely.
184
+ */
185
+ _matchPermalink(permalink, urlPath) {
186
+ if (typeof permalink !== 'string' || typeof urlPath !== 'string') {
187
+ return null;
188
+ }
189
+ const permalinkSegments = permalink.split('/');
190
+ const pathSegments = urlPath.split('/');
191
+ if (permalinkSegments.length !== pathSegments.length) {
192
+ return null;
193
+ }
194
+ const params = {};
195
+ for (let i = 0; i < permalinkSegments.length; i += 1) {
196
+ const templateSegment = permalinkSegments[i];
197
+ const pathSegment = pathSegments[i];
198
+ if (templateSegment.startsWith(':')) {
199
+ const fieldName = templateSegment.slice(1);
200
+ if (!/^\w+$/.test(fieldName) || pathSegment.length === 0) {
201
+ return null;
202
+ }
203
+ try {
204
+ params[fieldName] = decodeURIComponent(pathSegment);
205
+ }
206
+ catch {
207
+ // Malformed %-escapes (e.g. "/foo%ZZ/") throw URIError;
208
+ // treat as a non-match rather than crash the request.
209
+ return null;
210
+ }
211
+ }
212
+ else if (templateSegment !== pathSegment) {
213
+ return null;
214
+ }
215
+ }
216
+ return params;
217
+ }
218
+ }
219
+ exports.LazyUrlService = LazyUrlService;
220
+ module.exports = LazyUrlService;
@@ -0,0 +1,265 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ const nql = require('@tryghost/nql');
3
+ const debug = require('@tryghost/debug')('services:url:lazy');
4
+ const localUtils = require('../../../shared/url-utils');
5
+ /* eslint-enable @typescript-eslint/no-require-imports */
6
+
7
+ import type {Resource, UrlOptions, LazyUrlServiceBackend} from './url-service-facade';
8
+
9
+ // The same expansions used by UrlGenerator so users can write `tag:foo`,
10
+ // `author:jane`, etc. and have them rewritten to the underlying field paths.
11
+ const EXPANSIONS = [
12
+ {key: 'author', replacement: 'authors.slug'},
13
+ {key: 'tags', replacement: 'tags.slug'},
14
+ {key: 'tag', replacement: 'tags.slug'},
15
+ {key: 'authors', replacement: 'authors.slug'},
16
+ {key: 'primary_tag', replacement: 'primary_tag.slug'},
17
+ {key: 'primary_author', replacement: 'primary_author.slug'}
18
+ ];
19
+
20
+ // Same `page:true/false` legacy transformer the UrlGenerator uses, so old
21
+ // routes.yaml configs continue to work.
22
+ const PAGE_TRANSFORMER = nql.utils.mapKeyValues({
23
+ key: {from: 'page', to: 'type'},
24
+ values: [
25
+ {from: false, to: 'post'},
26
+ {from: true, to: 'page'}
27
+ ]
28
+ });
29
+
30
+ // Map a resource's `type` field (whatever the caller passed) onto the plural
31
+ // router resource type. We accept the singular DB column values ('post',
32
+ // 'page') as well as the plural router keys, because the migrated callers
33
+ // are inconsistent: API responses spread `attrs` (singular), but some
34
+ // helpers explicitly tag with the plural router key.
35
+ const TYPE_TO_ROUTER_TYPE: Record<string, string> = {
36
+ post: 'posts',
37
+ posts: 'posts',
38
+ page: 'pages',
39
+ pages: 'pages',
40
+ tag: 'tags',
41
+ tags: 'tags',
42
+ author: 'authors',
43
+ authors: 'authors'
44
+ };
45
+
46
+ interface NqlInstance {
47
+ queryJSON(record: Record<string, unknown>): unknown;
48
+ }
49
+
50
+ interface RouterConfig {
51
+ identifier: string;
52
+ filter: string | null;
53
+ resourceType: string;
54
+ permalink: string;
55
+ nql: NqlInstance | null;
56
+ }
57
+
58
+ /**
59
+ * Async function that resolves a slug (or other params) to a database
60
+ * record. Injected so the URL service stays pure for forward lookups —
61
+ * only `resolveUrl` actually hits the DB, and only via this hook.
62
+ */
63
+ export type FindResource = (
64
+ routerType: string,
65
+ params: Record<string, string>
66
+ ) => Promise<Record<string, unknown> | null>;
67
+
68
+ interface LazyUrlServiceDeps {
69
+ urlUtils?: typeof localUtils;
70
+ findResource?: FindResource | null;
71
+ }
72
+
73
+ /**
74
+ * On-demand URL service. Computes URLs and ownership per-call from the
75
+ * registered router configs instead of holding a precomputed map of every
76
+ * resource. Pure for forward lookups; resolveUrl() takes an optional DB
77
+ * lookup function so reverse lookups stay testable.
78
+ */
79
+ export class LazyUrlService implements LazyUrlServiceBackend {
80
+ private urlUtils: typeof localUtils;
81
+ private findResource: FindResource | null;
82
+ private routerConfigs: RouterConfig[];
83
+
84
+ constructor({urlUtils = localUtils, findResource = null}: LazyUrlServiceDeps = {}) {
85
+ this.urlUtils = urlUtils;
86
+ this.findResource = findResource;
87
+ // Router configs in priority order. Position is the registration order.
88
+ this.routerConfigs = [];
89
+ }
90
+
91
+ onRouterAddedType(
92
+ identifier: string,
93
+ filter: string | null,
94
+ resourceType: string,
95
+ permalink: string
96
+ ): void {
97
+ debug('onRouterAddedType', identifier, resourceType, permalink, filter);
98
+ const config: RouterConfig = {identifier, filter, resourceType, permalink, nql: null};
99
+ if (filter) {
100
+ config.nql = nql(filter, {expansions: EXPANSIONS, transformer: PAGE_TRANSFORMER});
101
+ }
102
+ this.routerConfigs.push(config);
103
+ }
104
+
105
+ onRouterUpdated(): void {
106
+ // No state to regenerate. The next URL request reads the (possibly new)
107
+ // router config; in-flight requests that already snapshotted the old
108
+ // config keep using it, which matches the documented atomic-reload
109
+ // behaviour.
110
+ }
111
+
112
+ /**
113
+ * Drop all registered routers. Called when routes.yaml is reloaded.
114
+ */
115
+ reset(): void {
116
+ this.routerConfigs = [];
117
+ }
118
+
119
+ hasFinished(): boolean {
120
+ return true;
121
+ }
122
+
123
+ getUrlForResource(resource: Resource, options: UrlOptions = {}): string {
124
+ const routerType = this._routerTypeOf(resource);
125
+ if (!routerType) {
126
+ return this._formatPath('/404/', options);
127
+ }
128
+ const candidates = this.routerConfigs.filter(c => c.resourceType === routerType);
129
+ for (const config of candidates) {
130
+ // NQL filters are evaluated against the original resource (with
131
+ // its singular DB `type` field intact) because the page:true/false
132
+ // transformer rewrites to type:'post'/'page'.
133
+ if (this._matchesFilter(config, resource)) {
134
+ const path = this.urlUtils.replacePermalink(config.permalink, resource);
135
+ return this._formatPath(path, options);
136
+ }
137
+ }
138
+ return this._formatPath('/404/', options);
139
+ }
140
+
141
+ ownsResource(routerIdentifier: string, resource: Resource | null): boolean {
142
+ const config = this.routerConfigs.find(c => c.identifier === routerIdentifier);
143
+ if (!config || !resource) {
144
+ return false;
145
+ }
146
+ const routerType = this._routerTypeOf(resource);
147
+ if (config.resourceType !== routerType) {
148
+ return false;
149
+ }
150
+ return this._matchesFilter(config, resource);
151
+ }
152
+
153
+ /**
154
+ * Translate the resource's `type` field into the plural router resource
155
+ * type (`'posts'` / `'pages'` / `'tags'` / `'authors'`). Returns `null`
156
+ * when the type is missing or unrecognised.
157
+ */
158
+ private _routerTypeOf(resource: Resource | null | undefined): string | null {
159
+ if (!resource || !resource.type) {
160
+ return null;
161
+ }
162
+ return TYPE_TO_ROUTER_TYPE[resource.type] || null;
163
+ }
164
+
165
+ /**
166
+ * Resolve a URL path to a resource record. Iterates router configs in
167
+ * priority order, pattern-matching the path against each permalink
168
+ * template, querying the database for a matching resource, and verifying
169
+ * any NQL filter still matches for posts.
170
+ */
171
+ async resolveUrl(urlPath: string): Promise<Resource | null> {
172
+ if (!this.findResource) {
173
+ return null;
174
+ }
175
+ for (const config of this.routerConfigs) {
176
+ const params = this._matchPermalink(config.permalink, urlPath);
177
+ if (!params) {
178
+ continue;
179
+ }
180
+ const resource = await this.findResource(config.resourceType, params);
181
+ if (!resource) {
182
+ continue;
183
+ }
184
+ // For posts/pages with NQL filters, confirm the DB record still
185
+ // satisfies the filter. The match runs against the raw record
186
+ // (with its singular `type` column) because the page:true/false
187
+ // transformer rewrites to type:'post'/'page'.
188
+ if (config.nql && !this._matchesFilter(config, resource)) {
189
+ continue;
190
+ }
191
+ return Object.assign({}, resource, {type: config.resourceType}) as Resource;
192
+ }
193
+ return null;
194
+ }
195
+
196
+ private _matchesFilter(config: RouterConfig, resource: Record<string, unknown>): boolean {
197
+ if (!config.nql) {
198
+ return true;
199
+ }
200
+ try {
201
+ return !!config.nql.queryJSON(resource);
202
+ } catch (err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ debug('NQL match failed', config.filter, message);
205
+ return false;
206
+ }
207
+ }
208
+
209
+ private _formatPath(path: string, options: UrlOptions): string {
210
+ if (options.absolute) {
211
+ return this.urlUtils.createUrl(path, options.absolute);
212
+ }
213
+ if (options.withSubdirectory) {
214
+ return this.urlUtils.createUrl(path, false, true);
215
+ }
216
+ return path;
217
+ }
218
+
219
+ /**
220
+ * Match a Ghost permalink template (e.g. `/:slug/`,
221
+ * `/:year/:month/:slug/`) against a URL path and extract the named
222
+ * fields. Ghost's RouteSettings validator rewrites `{field}` placeholders
223
+ * into `:field` form before they reach the URL service, so this parser
224
+ * works on the `:field` syntax.
225
+ *
226
+ * Implementation: walk the permalink and path one segment at a time.
227
+ * Each segment must be either a literal match or a single placeholder.
228
+ * Mixing literals and placeholders inside a segment is unsupported and
229
+ * not a documented Ghost feature; rejecting it here also keeps us out of
230
+ * regex-backtracking territory entirely.
231
+ */
232
+ private _matchPermalink(permalink: string, urlPath: string): Record<string, string> | null {
233
+ if (typeof permalink !== 'string' || typeof urlPath !== 'string') {
234
+ return null;
235
+ }
236
+ const permalinkSegments = permalink.split('/');
237
+ const pathSegments = urlPath.split('/');
238
+ if (permalinkSegments.length !== pathSegments.length) {
239
+ return null;
240
+ }
241
+ const params: Record<string, string> = {};
242
+ for (let i = 0; i < permalinkSegments.length; i += 1) {
243
+ const templateSegment = permalinkSegments[i];
244
+ const pathSegment = pathSegments[i];
245
+ if (templateSegment.startsWith(':')) {
246
+ const fieldName = templateSegment.slice(1);
247
+ if (!/^\w+$/.test(fieldName) || pathSegment.length === 0) {
248
+ return null;
249
+ }
250
+ try {
251
+ params[fieldName] = decodeURIComponent(pathSegment);
252
+ } catch {
253
+ // Malformed %-escapes (e.g. "/foo%ZZ/") throw URIError;
254
+ // treat as a non-match rather than crash the request.
255
+ return null;
256
+ }
257
+ } else if (templateSegment !== pathSegment) {
258
+ return null;
259
+ }
260
+ }
261
+ return params;
262
+ }
263
+ }
264
+
265
+ module.exports = LazyUrlService;
@@ -189,6 +189,7 @@ module.exports = function apiRoutes() {
189
189
  router.get('/automations', mw.authAdminApi, http(api.automations.browse));
190
190
  router.get('/automations/:id', mw.authAdminApi, http(api.automations.read));
191
191
  router.put('/automations/poll', mw.authAdminApiWithUrl, http(api.automations.poll));
192
+ router.put('/automations/:id', mw.authAdminApi, http(api.automations.edit));
192
193
 
193
194
  // ## Automated Emails
194
195
  router.get('/automated_emails', mw.authAdminApi, http(api.automatedEmails.browse));
@@ -11,6 +11,24 @@ function getCadenceLabel(cadence, duration) {
11
11
  return t('{count} month', {count: duration});
12
12
  }
13
13
 
14
+ function getOgTitle({cadence, duration, tierName, siteTitle}) {
15
+ if (cadence === 'year') {
16
+ return t(`You've been gifted a {duration}-year {tierName} membership to {siteTitle}`, {
17
+ duration,
18
+ tierName,
19
+ siteTitle,
20
+ interpolation: {escapeValue: false}
21
+ });
22
+ }
23
+
24
+ return t(`You've been gifted a {duration}-month {tierName} membership to {siteTitle}`, {
25
+ duration,
26
+ tierName,
27
+ siteTitle,
28
+ interpolation: {escapeValue: false}
29
+ });
30
+ }
31
+
14
32
  function escapeHtml(str) {
15
33
  return str
16
34
  .replaceAll('&', '&amp;')
@@ -22,6 +40,7 @@ function escapeHtml(str) {
22
40
  async function giftPreview(req, res) {
23
41
  const labs = require('../../../shared/labs');
24
42
  const giftService = require('../../services/gifts').service;
43
+ const tiersService = require('../../services/tiers');
25
44
  const urlUtils = require('../../../shared/url-utils');
26
45
  const settingsCache = require('../../../shared/settings-cache');
27
46
 
@@ -35,6 +54,7 @@ async function giftPreview(req, res) {
35
54
  const siteTitle = settingsCache.get('title') || 'Ghost';
36
55
 
37
56
  let gift;
57
+ let tier;
38
58
 
39
59
  try {
40
60
  gift = await giftService.getByToken(token);
@@ -42,17 +62,23 @@ async function giftPreview(req, res) {
42
62
  if (!gift) {
43
63
  throw new errors.NotFoundError({message: `Gift not found for token`});
44
64
  }
65
+
66
+ tier = await tiersService.api.read(gift.tierId);
67
+
68
+ if (!tier) {
69
+ throw new errors.NotFoundError({message: `Tier not found for gift: ${gift.id}`});
70
+ }
45
71
  } catch (err) {
46
72
  logging.warn(`Gift preview: failed to load required gift data, redirecting to homepage`, err);
47
73
 
48
74
  return res.redirect(302, siteUrl + '/');
49
75
  }
50
76
 
51
- const cadenceLabel = getCadenceLabel(gift.cadence, gift.duration);
52
- const ogTitle = t(`You've been gifted {cadenceLabel} of {siteTitle}`, {
53
- cadenceLabel,
54
- siteTitle,
55
- interpolation: {escapeValue: false}
77
+ const ogTitle = getOgTitle({
78
+ cadence: gift.cadence,
79
+ duration: gift.duration,
80
+ tierName: tier.name,
81
+ siteTitle
56
82
  });
57
83
  const ogDescription = t('Open this link to redeem your gift.');
58
84
  const ogImage = `${siteUrl}/gift/${encodeURIComponent(token)}/image`;
@@ -98,16 +124,40 @@ async function giftPreview(req, res) {
98
124
 
99
125
  async function giftPreviewImage(req, res) {
100
126
  const labs = require('../../../shared/labs');
127
+ const giftService = require('../../services/gifts').service;
101
128
  const settingsCache = require('../../../shared/settings-cache');
129
+ const tiersService = require('../../services/tiers');
102
130
 
103
131
  if (!labs.isSet('giftSubscriptions')) {
104
132
  return res.sendStatus(404);
105
133
  }
106
134
 
107
135
  const accentColor = settingsCache.get('accent_color') || '#15171A';
136
+ const siteTitle = settingsCache.get('title') || 'Ghost';
137
+ const {token} = req.params;
108
138
 
109
139
  try {
110
- const png = await generateGiftPreviewImage({accentColor});
140
+ const gift = await giftService.getByToken(token);
141
+
142
+ if (!gift) {
143
+ throw new errors.NotFoundError({message: `Gift not found for token`});
144
+ }
145
+
146
+ const tier = await tiersService.api.read(gift.tierId);
147
+
148
+ if (!tier) {
149
+ throw new errors.NotFoundError({message: `Tier not found for gift: ${gift.id}`});
150
+ }
151
+
152
+ const png = await generateGiftPreviewImage({
153
+ accentColor,
154
+ siteTitle,
155
+ tierLabel: t('{tierName} membership', {
156
+ tierName: tier.name,
157
+ interpolation: {escapeValue: false}
158
+ }),
159
+ cadenceLabel: getCadenceLabel(gift.cadence, gift.duration)
160
+ });
111
161
 
112
162
  res.set('Content-Type', 'image/png');
113
163
  res.set('Cache-Control', 'public, max-age=86400');