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.
- package/components/tryghost-i18n-0.0.0.tgz +0 -0
- package/core/boot.js +13 -9
- package/core/bridge.js +10 -6
- package/core/built/admin/assets/{PolarAngleAxis-Cavik65s.js → PolarAngleAxis-CB-FJBDa.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-CnqN6ZMT.js → _baseAssignValue-BEAjPAfy.js} +1 -1
- package/core/built/admin/assets/{a-large-small-Wi4LtZX-.js → a-large-small-L1BJa6mG.js} +1 -1
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-Cf8T5Jfd.mjs → code-editor-view-bOZzJ3a1.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BMWEh-vR.mjs → index-BRuMxaCz.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-Aj-hFSnY.mjs → index-CTAQj489.mjs} +5 -5
- package/core/built/admin/assets/admin-x-settings/{index-DjBJrXYn.mjs → index-WoKKBOe4.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-8WpWZZdI.mjs → modals-BQ5M-RTA.mjs} +3 -3
- package/core/built/admin/assets/{arrow-right-BiAOKOto.js → arrow-right-B2j-ecEK.js} +1 -1
- package/core/built/admin/assets/{at-sign-z5gBMxDW.js → at-sign-CtAw8_lw.js} +1 -1
- package/core/built/admin/assets/{audience-DxNilLIv.js → audience-CvSOqHuo.js} +1 -1
- package/core/built/admin/assets/automations-BVUNuVOh.js +1 -0
- package/core/built/admin/assets/automations-Bi9RJq5E.js +1 -0
- package/core/built/admin/assets/{avatar-flipboard-5OzseZFV.js → avatar-flipboard-DocLTXLh.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-D9YcSpW8.js → bluesky-sharing-DafX2lE1.js} +1 -1
- package/core/built/admin/assets/chart-BRQeD9eh.js +67 -0
- package/core/built/admin/assets/{chevron-up-DVt6fhV3.js → chevron-up-v4_QwmnL.js} +1 -1
- package/core/built/admin/assets/{chunk.524.fcd26ef812d73e2b0620.js → chunk.524.c3d844070c24d7984fd7.js} +4 -4
- package/core/built/admin/assets/{chunk.582.b61ffaad1c2fa4aba820.js → chunk.582.0e0ede1eac12926acceb.js} +6 -6
- package/core/built/admin/assets/{chunk.383.adb5a3fe32b6b98c43ff.js → chunk.995.4b32363ab39da3e7f5d7.js} +3 -3
- package/core/built/admin/assets/circle-alert-Dbz54fYb.js +1 -0
- package/core/built/admin/assets/{code-editor-view-CbQIqTrK.js → code-editor-view-D-pYCcdu.js} +1 -1
- package/core/built/admin/assets/comments-DSv09jRn.js +1 -0
- package/core/built/admin/assets/{content-helpers-jANWGIfA.js → content-helpers-DQdPPDGb.js} +1 -1
- package/core/built/admin/assets/{copy-BBLeLEnG.js → copy-BULQsPZ4.js} +1 -1
- package/core/built/admin/assets/{data-list-BveeOSGI.js → data-list-WxXC0iKE.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-DupAEk4P.js → deleted-feed-item-ZBLfgRkK.js} +1 -1
- package/core/built/admin/assets/{dropzone-D3PSdAHN.js → dropzone-B8mAoWrn.js} +1 -1
- package/core/built/admin/assets/{edit-profile-NrCR9_bi.js → edit-profile-Bj3th8H0.js} +1 -1
- package/core/built/admin/assets/editor-BZV40eAE.css +1 -0
- package/core/built/admin/assets/editor-u20VuPzn.js +7 -0
- package/core/built/admin/assets/{empty-indicator-Dhi_KeYX.js → empty-indicator-P3IsUguf.js} +1 -1
- package/core/built/admin/assets/{en-BontI3SP.js → en-C9bUnuy7.js} +1 -1
- package/core/built/admin/assets/{feed-B3Kfiz5R.js → feed-B94mO944.js} +1 -1
- package/core/built/admin/assets/{filter-query-core-Dwh4Qulu.js → filter-query-core-CM0KKIr2.js} +1 -1
- package/core/built/admin/assets/filters-D52FTE0z.js +1 -0
- package/core/built/admin/assets/gh-chart-Xa6Nvzwz.js +1 -0
- package/core/built/admin/assets/{ghost-c25434d5bbf5a5429883d4c3590f1946.js → ghost-82f1c4ffe0e1cd058ce4b66b5253b620.js} +159 -201
- package/core/built/admin/assets/{ghost-dark-52df27573e5f2a75e6cf4b1d06eea9e4.css → ghost-dark-9897cf5102772faedf566e5e54d4fbee.css} +1 -1
- package/core/built/admin/assets/{ghost-fcf460028e04192f392f16e173f2e3a8.css → ghost-e11f1814be0f38e98bb77b49eba11178.css} +1 -1
- package/core/built/admin/assets/{growth-DRH8zUIJ.js → growth-D3TW3JIB.js} +1 -1
- package/core/built/admin/assets/{hash-UvX5iOa9.js → hash-CstXS-iB.js} +1 -1
- package/core/built/admin/assets/{header-G1a38jnf.js → header-StkTITw-.js} +1 -1
- package/core/built/admin/assets/{inbox-By7HZskI.js → inbox-D1RM3kVF.js} +1 -1
- package/core/built/admin/assets/index-BEkJ9xtY.css +1 -0
- package/core/built/admin/assets/{index-CS_ZmI8F.js → index-BJx5Uas5.js} +1 -1
- package/core/built/admin/assets/index-BKzg6OE7.js +1 -0
- package/core/built/admin/assets/{index-D7JyKf1m.js → index-BM-MBdD0.js} +1 -1
- package/core/built/admin/assets/index-BRzvVuXt.js +2 -0
- package/core/built/admin/assets/index-B_3xqeM9.js +1 -0
- package/core/built/admin/assets/{index-ydyrczVl.js → index-C-OdR2wJ.js} +1 -1
- package/core/built/admin/assets/{index-X-MG1ikg.js → index-C59QlJqT.js} +1 -1
- package/core/built/admin/assets/{index-CJo0Qh4V.js → index-CCq_1tCo.js} +1 -1
- package/core/built/admin/assets/{index-qHUvPjQB.js → index-CYyEyBYw.js} +11 -11
- package/core/built/admin/assets/{index-CX4ztLSk.js → index-DHbiBjyq.js} +1 -1
- package/core/built/admin/assets/{index-BP6hBbPK.js → index-DTVgSMyL.js} +2 -2
- package/core/built/admin/assets/{index-vZMbI8e8.js → index-DiUjEgRw.js} +1 -1
- package/core/built/admin/assets/{index-YM4ZplGj.js → index-DugxKzA6.js} +1 -1
- package/core/built/admin/assets/{index-D8InZCr2.js → index-DzVHlhO1.js} +1 -1
- package/core/built/admin/assets/{index-D4b7U6K-.js → index-LsaD9Cq7.js} +1 -1
- package/core/built/admin/assets/{index-DRUPF8nN.js → index-x0hNfyqU.js} +1 -1
- package/core/built/admin/assets/{inline-Bc9zHR7e.js → inline-B4cWemrJ.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-B-ZnqRE1.js → koenig-lexical-BOdeH3QJ.js} +1 -1
- package/core/built/admin/assets/{kpi-card-zyi5Onb0.js → kpi-card-P7zSpcx0.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CzlPIEPE.js → kpi-card-plhYOEtA.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dsa5ldXK.js → kpi-tabs-DZw2Qktb.js} +1 -1
- package/core/built/admin/assets/{kpis-Cw5ZRQhk.js → kpis-CDHM0xhW.js} +1 -1
- package/core/built/admin/assets/{label-BvxmsD1M.js → label-Ckacdx14.js} +1 -1
- package/core/built/admin/assets/{links-CiS5BPKS.js → links-BtXE5d0Y.js} +1 -1
- package/core/built/admin/assets/{list-filter-zmkB7rg4.js → list-filter-DLjjiktn.js} +1 -1
- package/core/built/admin/assets/{list-header-Cp779e3H.js → list-header-Cc7Ia3nr.js} +1 -1
- package/core/built/admin/assets/loader-circle-DLJd-dNm.js +1 -0
- package/core/built/admin/assets/{mail-i2qs8CIG.js → mail-B97w28e2.js} +1 -1
- package/core/built/admin/assets/{main-layout-KCvKOX8F.js → main-layout-bwWvzc9B.js} +1 -1
- package/core/built/admin/assets/{members-w601v5JJ.js → members-CokS8MI2.js} +5 -5
- package/core/built/admin/assets/{message-square-text-bG9IoWIt.js → message-square-text-D3rsjDFR.js} +1 -1
- package/core/built/admin/assets/minus-BzXQVmMw.js +1 -0
- package/core/built/admin/assets/{modals-lNzhiL0o.js → modals-D0lip-iy.js} +2 -2
- package/core/built/admin/assets/{moderation-CyfNRSWj.js → moderation-Dv7AadCV.js} +1 -1
- package/core/built/admin/assets/newsletter-BazC5rIj.js +1 -0
- package/core/built/admin/assets/{newsletters-B8xSXSr_.js → newsletters-m6HQgU5J.js} +1 -1
- package/core/built/admin/assets/{note-dNfKEpxw.js → note-CCGiEKz-.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-Drbw0QtT.js → onboarding-route-Cbt7qWNE.js} +1 -1
- package/core/built/admin/assets/overview-TN50tzUh.js +1 -0
- package/core/built/admin/assets/{pagemenu-VJXMoMB4.js → pagemenu-BXrHWtsU.js} +1 -1
- package/core/built/admin/assets/{pencil-DHNmlLjO.js → pencil-BL1r8KVy.js} +1 -1
- package/core/built/admin/assets/{post-analytics-PsqLsS2e.js → post-analytics-BqEXi6Ps.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-QLZsZCFg.js → post-analytics-context-DekYNlb3.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-PecXsIT-.js → post-analytics-header-AkNbn6uc.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-B4Rvo7W1.js → post-share-modal-CXDGVKoQ.js} +1 -1
- package/core/built/admin/assets/posts/{app-utils-DIc5TmxO.mjs → app-utils-DxA7edI_.mjs} +2 -2
- package/core/built/admin/assets/posts/{automations-BZpT35kJ.mjs → automations-Cwu_gHl4.mjs} +64 -67
- package/core/built/admin/assets/posts/automations-D81hV_2c.mjs +13 -0
- package/core/built/admin/assets/posts/{avatar-K63cvqSS.mjs → avatar-IkjEzEjI.mjs} +19 -19
- package/core/built/admin/assets/posts/{button-ufGBCcsV.mjs → button-KaJZKcou.mjs} +21 -21
- package/core/built/admin/assets/posts/{check-IcEnuTpD.mjs → check-Cf74RvZ1.mjs} +21 -21
- package/core/built/admin/assets/posts/{comments-CyJU9Ums.mjs → comments-pdKhYbYG.mjs} +54 -52
- package/core/built/admin/assets/posts/createLucideIcon-Cj7h3r9g.mjs +320 -0
- package/core/built/admin/assets/posts/{dialog-D6_lBvtT.mjs → dialog-Ci3W8fjx.mjs} +71 -76
- package/core/built/admin/assets/posts/{dropdown-menu-BSSmAory.mjs → dropdown-menu-DZ9XCHSw.mjs} +5 -5
- package/core/built/admin/assets/posts/editor-Bb-xle0o.mjs +6084 -0
- package/core/built/admin/assets/posts/ellipsis-ChhZo9aT.mjs +10 -0
- package/core/built/admin/assets/posts/{empty-indicator-DvGCvRAX.mjs → empty-indicator-DnFs7hDy.mjs} +2 -2
- package/core/built/admin/assets/posts/{filters-BLBaHhJz.mjs → filters-CO_FUPX-.mjs} +42 -40
- package/core/built/admin/assets/posts/get-site-timezone-C21yKZT_.mjs +87 -0
- package/core/built/admin/assets/posts/{growth-Bz1ChJNB.mjs → growth-CtX9Efm2.mjs} +12 -12
- package/core/built/admin/assets/posts/heading-DZ_KtDTL.mjs +138 -0
- package/core/built/admin/assets/posts/{hooks-BEngBys9.mjs → hooks-DVhBDVlA.mjs} +3 -2
- package/core/built/admin/assets/posts/{index-BAF0YXsp.mjs → index-B4db8w_H.mjs} +181 -172
- package/core/built/admin/assets/posts/{inline-C2YFQAdU.mjs → inline-BfB-SNk0.mjs} +2 -2
- package/core/built/admin/assets/posts/{input-surface-TqJlNSp_.mjs → input-surface-CNwZJloi.mjs} +2 -2
- package/core/built/admin/assets/posts/{kpis-DAX5_0-Q.mjs → kpis-BvY_5Y1u.mjs} +8792 -8803
- package/core/built/admin/assets/posts/{links-A1YTsBbF.mjs → links-Ck-h7SKg.mjs} +57 -57
- package/core/built/admin/assets/posts/{loading-indicator-BZsjmp8g.mjs → loading-indicator-B_rU9Lie.mjs} +3 -3
- package/core/built/admin/assets/posts/mail-DiZ3oKEW.mjs +9 -0
- package/core/built/admin/assets/posts/{main-layout-D_HHTP_n.mjs → main-layout-aYo6qOBZ.mjs} +2 -2
- package/core/built/admin/assets/posts/{newsletter-BRzgjOIa.mjs → newsletter-BQc53TF2.mjs} +31 -30
- package/core/built/admin/assets/posts/{overview-C41VFjn7.mjs → overview-blz9Ak9q.mjs} +42 -41
- package/core/built/admin/assets/posts/plus-YVjtYaK3.mjs +15 -0
- package/core/built/admin/assets/posts/{post-analytics-DGgMkFaf.mjs → post-analytics-BQbWGoeu.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-B1r3HBrp.mjs → post-analytics-context-Be7THgnd.mjs} +7 -7
- package/core/built/admin/assets/posts/{post-analytics-header-DTWSO0Aq.mjs → post-analytics-header-iDFy7Bao.mjs} +2762 -2764
- package/core/built/admin/assets/posts/{post-share-modal-ohHwk0Yt.mjs → post-share-modal-akP16idN.mjs} +50 -48
- package/core/built/admin/assets/posts/posts.js +2 -1
- package/core/built/admin/assets/posts/{settings-CZYL6Jhr.mjs → settings-BdQhfHxY.mjs} +2 -2
- package/core/built/admin/assets/posts/{sheet-D7YesQJE.mjs → sheet-3dBlKeXf.mjs} +14 -13
- package/core/built/admin/assets/posts/{skeleton-S1k58YKa.mjs → skeleton-DMgSvYqr.mjs} +11 -11
- package/core/built/admin/assets/posts/{source-icon-DnpCy5N4.mjs → source-icon-Bp5Qxq1E.mjs} +4 -4
- package/core/built/admin/assets/posts/{stats-CbOpowXb.mjs → stats-DJx9qc_u.mjs} +4 -4
- package/core/built/admin/assets/posts/{table-Cw3Wxf1C.mjs → table-BpVwuzeH.mjs} +2 -2
- package/core/built/admin/assets/posts/{data-list-BnZARP88.mjs → tabs-CbZCpxfI.mjs} +4885 -5284
- package/core/built/admin/assets/posts/{tags-BoMt6Ce3.mjs → tags-CNtvDS6f.mjs} +33 -33
- package/core/built/admin/assets/posts/{tags-CNdsvTtZ.mjs → tags-DFOoFZ_3.mjs} +2 -2
- package/core/built/admin/assets/posts/{tooltip-CTcyINxz.mjs → tooltip-Cc_09RDU.mjs} +106 -117
- package/core/built/admin/assets/posts/value-CwGM7M-1.mjs +410 -0
- package/core/built/admin/assets/posts/{virtual-list-window-Bs88yrut.mjs → virtual-list-window-D6nRoL0y.mjs} +5 -5
- package/core/built/admin/assets/posts/{web-Bsd8eTP6.mjs → web-yfPssEt8.mjs} +1042 -1040
- package/core/built/admin/assets/posts/x-DrUGcpfp.mjs +9 -0
- package/core/built/admin/assets/posts/zap-DAN0ur-o.mjs +24 -0
- package/core/built/admin/assets/posts-D-jyPPE6.js +1 -0
- package/core/built/admin/assets/power-Cc5ncYVl.js +1 -0
- package/core/built/admin/assets/{referrers-kcBT5smx.js → referrers-qnXsKsuv.js} +1 -1
- package/core/built/admin/assets/{repeat-Cn2sKd-L.js → repeat-BGIBynZH.js} +1 -1
- package/core/built/admin/assets/{reply-D7E-ZMd5.js → reply-DOKdAsL_.js} +1 -1
- package/core/built/admin/assets/{rocket-DnH0XAGW.js → rocket-CAN2NB82.js} +1 -1
- package/core/built/admin/assets/{select-f0MMMQWu.js → select-8S9kYGSM.js} +1 -1
- package/core/built/admin/assets/{settings-BAQNP3gU.js → settings-DeD-bmZc.js} +4 -4
- package/core/built/admin/assets/{settings-ORttG1jT.js → settings-e4IB9sx6.js} +1 -1
- package/core/built/admin/assets/{share-modal-CXU4tGY8.js → share-modal-CBavk20I.js} +1 -1
- package/core/built/admin/assets/{sort-button-BXMrChFu.js → sort-button-DlSKBzUq.js} +1 -1
- package/core/built/admin/assets/{source-icon-cjy79jGT.js → source-icon-27VIJSXr.js} +1 -1
- package/core/built/admin/assets/{sprout-DZ4tZ78G.js → sprout-D66nqc79.js} +1 -1
- package/core/built/admin/assets/{square-vOzqJf7x.js → square-DccaXQWe.js} +1 -1
- package/core/built/admin/assets/stats/{audience-5NeJICtP.mjs → audience-B84ClTDM.mjs} +3 -3
- package/core/built/admin/assets/stats/{content-helpers-DFSTl3uB.mjs → content-helpers-CBAddUg7.mjs} +4 -4
- package/core/built/admin/assets/stats/{index-C4T7u699.mjs → index-Bz5oVpED.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-B8bp6qe2.mjs → index-Cs8i1uR4.mjs} +74 -75
- package/core/built/admin/assets/stats/{index-wvi17m79.mjs → index-DEsP_Tbd.mjs} +8 -8
- package/core/built/admin/assets/stats/{index-CFfAYHdD.mjs → index-Ds-pM47x.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-Chqk7VlR.mjs → index-Wzk1IcAz.mjs} +6 -6
- package/core/built/admin/assets/stats/{kpi-tabs-Df9yOI2P.mjs → kpi-tabs-Cs4l8B_M.mjs} +7 -7
- package/core/built/admin/assets/stats/{sort-button-Doi8OgMU.mjs → sort-button-rR-GA9Hu.mjs} +3 -3
- package/core/built/admin/assets/stats/{stats-FXeCefyd.mjs → stats-RFpEJpEq.mjs} +34 -34
- package/core/built/admin/assets/stats/stats.js +1 -1
- package/core/built/admin/assets/stats/{use-growth-stats-93C_3EZk.mjs → use-growth-stats-DwgFzbq9.mjs} +3 -3
- package/core/built/admin/assets/{stats-CIGz2Win.js → stats-B4OybenW.js} +1 -1
- package/core/built/admin/assets/{stats-view-BTKRxYTQ.js → stats-view-BuExwFXa.js} +1 -1
- package/core/built/admin/assets/{step-1-BSMHbT-q.js → step-1-D_5LOQrC.js} +1 -1
- package/core/built/admin/assets/{step-2-_YC0LCZv.js → step-2-BZLBj8cj.js} +1 -1
- package/core/built/admin/assets/{step-3-DWMqP2bO.js → step-3-s5uNbcOH.js} +1 -1
- package/core/built/admin/assets/{table-mHKjXQba.js → table-pGS7EeJe.js} +1 -1
- package/core/built/admin/assets/{tabs-B93IVRBz.js → tabs-CjdKssUe.js} +1 -1
- package/core/built/admin/assets/{tags-DCb-x2ZB.js → tags-B0vRk0A6.js} +1 -1
- package/core/built/admin/assets/{tags-kUHJ1MZo.js → tags-Dso4-FA_.js} +1 -1
- package/core/built/admin/assets/{textarea-DswCtM7_.js → textarea-DzFnun0_.js} +1 -1
- package/core/built/admin/assets/{tiers-DueraQKd.js → tiers-4Vkr_D-L.js} +1 -1
- package/core/built/admin/assets/{toggle-group-ChTo8MPD.js → toggle-group-hDsr47IB.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DrW3fYWo.js → topic-filter-C7TpPe1G.js} +1 -1
- package/core/built/admin/assets/{trash-B2ESGftj.js → trash-DCVuk7ZX.js} +1 -1
- package/core/built/admin/assets/{underline-vo-bPRac.js → underline-CjK8la2G.js} +1 -1
- package/core/built/admin/assets/{upload-DXQRkBBq.js → upload-CPYhmtWZ.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-CoB8-lsd.js → use-growth-stats-CaFSgp_R.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-Y_Wv2N6g.js → use-simple-pagination-D0vJBMvF.js} +1 -1
- package/core/built/admin/assets/{user-round-check-Dp9a2x-e.js → user-round-check-CzQQeFd8.js} +1 -1
- package/core/built/admin/assets/{user-round-x-DwCTGlxc.js → user-round-x-BDFm8yQj.js} +1 -1
- package/core/built/admin/assets/value-BGBiTkF3.js +1 -0
- package/core/built/admin/assets/{vendor-17f3b979ba6f3898d1a8c5249cc22ff1.js → vendor-ce3fc901ccbcf37f36eb791b4e454138.js} +29 -31
- package/core/built/admin/assets/{virtual-list-window-BF7IvGMC.js → virtual-list-window-EKtWZ5cP.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-IEmcxJvG.js → wallet-cards-C1NOBbWE.js} +1 -1
- package/core/built/admin/assets/web-DCA7HR1S.js +1 -0
- package/core/built/admin/index.html +8 -8
- package/core/frontend/helpers/social_accounts.js +91 -0
- package/core/frontend/services/sitemap/handler.js +14 -2
- package/core/frontend/services/sitemap/site-map-manager.js +133 -9
- package/core/frontend/web/middleware/handle-image-sizes.js +4 -0
- package/core/server/api/endpoints/automations.js +38 -45
- package/core/server/models/member.js +30 -25
- package/core/server/services/automations/automations-api.js +58 -0
- package/core/server/services/automations/automations-api.ts +71 -0
- package/core/server/services/automations/automations-repository.js +2 -0
- package/core/server/services/automations/automations-repository.ts +63 -0
- package/core/server/services/automations/fake-database-automations-repository.js +210 -0
- package/core/server/services/automations/fake-database-automations-repository.ts +273 -0
- package/core/server/services/automations/temporary-fake-database.js +5 -4
- package/core/server/services/gifts/email-templates/gift-purchase-confirmation.hbs +10 -32
- package/core/server/services/gifts/email-templates/gift-purchase-confirmation.js +6 -8
- package/core/server/services/gifts/email-templates/gift-purchase-confirmation.ts +6 -8
- package/core/server/services/gifts/email-templates/gift-reminder.hbs +12 -37
- package/core/server/services/gifts/email-templates/gift-reminder.js +5 -9
- package/core/server/services/gifts/email-templates/gift-reminder.ts +5 -9
- package/core/server/services/gifts/gift-service.js +1 -1
- package/core/server/services/gifts/gift-service.ts +2 -2
- package/core/server/services/member-welcome-emails/service.js +0 -1
- package/core/server/services/members/members-api/controllers/router-controller.js +52 -7
- package/core/server/services/members/members-api/services/member-bread-service.js +0 -8
- package/core/server/services/route-settings/route-settings.js +18 -2
- package/core/server/services/staff/email-templates/gift.hbs +1 -1
- package/core/server/services/staff/email-templates/gift.txt.js +1 -1
- package/core/server/services/staff/email-templates/new-gift-subscription.hbs +1 -1
- package/core/server/services/staff/email-templates/new-gift-subscription.txt.js +1 -1
- package/core/server/services/staff/staff-service-emails.js +1 -1
- package/core/server/services/url/index.js +16 -1
- package/core/server/services/url/lazy-find-resource.js +49 -0
- package/core/server/services/url/lazy-find-resource.ts +62 -0
- package/core/server/services/url/lazy-url-service.js +220 -0
- package/core/server/services/url/lazy-url-service.ts +265 -0
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/gift-preview/Inter.ttf +0 -0
- package/core/server/web/gift-preview/controller.js +56 -6
- package/core/server/web/gift-preview/gift-card-noise.png +0 -0
- package/core/server/web/gift-preview/gift-card-orb.png +0 -0
- package/core/server/web/gift-preview/image.js +170 -43
- package/core/shared/config/defaults.json +1 -0
- package/core/shared/one-at-a-time.js +28 -33
- package/core/shared/one-at-a-time.ts +58 -0
- package/package.json +18 -16
- package/pnpm-lock.yaml +989 -356
- package/core/built/admin/assets/automations-D1uMjd06.js +0 -1
- package/core/built/admin/assets/chart-_EXssQtm.js +0 -67
- package/core/built/admin/assets/comments-E00h1SbR.js +0 -1
- package/core/built/admin/assets/filters-CC7XfjOS.js +0 -1
- package/core/built/admin/assets/gh-chart-B9_ywrJJ.js +0 -1
- package/core/built/admin/assets/index-AsV-aY0z.css +0 -1
- package/core/built/admin/assets/index-B0y_ynZw.js +0 -1
- package/core/built/admin/assets/index-Bs6wWbLA.js +0 -2
- package/core/built/admin/assets/index-Cb4641XW.js +0 -1
- package/core/built/admin/assets/loader-circle-B5UPqSok.js +0 -1
- package/core/built/admin/assets/minus-DWx9VUtr.js +0 -1
- package/core/built/admin/assets/newsletter-fkQye9OK.js +0 -1
- package/core/built/admin/assets/overview-CZ-VrnXy.js +0 -1
- package/core/built/admin/assets/posts/createLucideIcon-DcUfTBt_.mjs +0 -454
- package/core/built/admin/assets/posts/get-site-timezone-DlXmHA3y.mjs +0 -93
- package/core/built/admin/assets/posts-Bx1RWrub.js +0 -1
- 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));
|
|
Binary file
|
|
@@ -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('&', '&')
|
|
@@ -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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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');
|
|
Binary file
|
|
Binary file
|