ghost 6.56.0 → 6.57.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 (385) hide show
  1. package/bin/validate-adapters.js +74 -0
  2. package/bin/validate-adapters.ts +87 -0
  3. package/bin/warm-compile-cache.sh +60 -0
  4. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
  8. package/core/app.js +1 -1
  9. package/core/boot.js +21 -25
  10. package/core/bridge.js +10 -12
  11. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  12. package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
  13. package/core/built/admin/assets/{account-migration-D3O_yVck.js → account-migration-Cv_ol1ko.js} +1 -1
  14. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  15. package/core/built/admin/assets/activitypub/{account-migration-D8FbP-Z-.js → account-migration-DHFbePl6.js} +7 -7
  16. package/core/built/admin/assets/activitypub/activitypub.js +3 -3
  17. package/core/built/admin/assets/activitypub/{avatar-11EA1VSs.js → avatar-CT4tS2_S.js} +2 -2
  18. package/core/built/admin/assets/activitypub/{avatar-flipboard-jfbLG-xe.js → avatar-flipboard-ByYWKipo.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{bluesky-sharing-f1jTQe2P.js → bluesky-sharing-srVnSHDz.js} +8 -8
  20. package/core/built/admin/assets/activitypub/{content-formatters-nmTOPY9M.js → content-formatters-C2sos5Ht.js} +3 -3
  21. package/core/built/admin/assets/activitypub/{copy-BCwo9ep3.js → copy-B1UBTOoZ.js} +2 -2
  22. package/core/built/admin/assets/activitypub/{dist-BEfX-0aW.js → dist-MPsb2WZ1.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{domain-AaDlZlrA.js → domain-DzcM9Dzo.js} +9 -9
  24. package/core/built/admin/assets/activitypub/{edit-profile-DPi8Tl5M.js → edit-profile-CsQ_4xAk.js} +5 -5
  25. package/core/built/admin/assets/activitypub/{explore-CxTzjILQ.js → explore-CJe0DZBK.js} +8 -8
  26. package/core/built/admin/assets/activitypub/{feed-DPv5x2MX.js → feed-CKV3qWON.js} +9 -9
  27. package/core/built/admin/assets/activitypub/{field-DbgA8lat.js → field-DckHfDJf.js} +3 -3
  28. package/core/built/admin/assets/activitypub/{hash-BWuZoYkA.js → hash-Dlp8RM55.js} +2 -2
  29. package/core/built/admin/assets/activitypub/{inbox-35VDQ2K2.js → inbox-CsPy4R0M.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{inbox-DSrF_i1O.js → inbox-DAsjzUUe.js} +10 -10
  31. package/core/built/admin/assets/activitypub/{label-CPjFBbo7.js → label-DWyo9E5T.js} +3 -3
  32. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  33. package/core/built/admin/assets/activitypub/{moderation-BkDC1XVZ.js → moderation-B0Inc03M.js} +7 -7
  34. package/core/built/admin/assets/activitypub/{note-xEyH4xT-.js → note-CSE54lER.js} +7 -7
  35. package/core/built/admin/assets/activitypub/{notifications-DClaL3ui.js → notifications-RSwbKqdh.js} +9 -9
  36. package/core/built/admin/assets/activitypub/{preferences-DpnOa-4Z.js → preferences-CE231n2_.js} +11 -11
  37. package/core/built/admin/assets/activitypub/{profile-Dvi9mz-6.js → profile-aaQ1j1Eo.js} +13 -13
  38. package/core/built/admin/assets/activitypub/{reply-Cs9ySd6Z.js → reply-Cjb0Vs3m.js} +2 -2
  39. package/core/built/admin/assets/activitypub/{routes-DFBeTnfn.js → routes-Djb0mc9k.js} +26 -26
  40. package/core/built/admin/assets/activitypub/{separator-C5u_VjzY.js → separator-D8pROI5n.js} +3 -3
  41. package/core/built/admin/assets/activitypub/{settings-CV2fFu6D.js → settings-DMc32IQl.js} +5 -5
  42. package/core/built/admin/assets/activitypub/{site-_TgDd_EM.js → site-DVIcsohX.js} +4 -4
  43. package/core/built/admin/assets/activitypub/{step-1-BfnNL2oF.js → step-1-9Yey9usF.js} +5 -5
  44. package/core/built/admin/assets/activitypub/{step-2-DLndZkO-.js → step-2-DdYmKRXV.js} +6 -6
  45. package/core/built/admin/assets/activitypub/{step-3-A-PZTtRE.js → step-3-hdSkcjZU.js} +10 -10
  46. package/core/built/admin/assets/activitypub/{tabs-BUbz7tea.js → tabs-CUqseYzQ.js} +6 -6
  47. package/core/built/admin/assets/activitypub/{topic-filter-BdX_xYYT.js → topic-filter-Coni6OXM.js} +3 -3
  48. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-EwuRemHh.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  49. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-cy4ACBc4.js → use-navigate-with-base-path-B046AM38.js} +24 -7
  50. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CyFcGfan.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  51. package/core/built/admin/assets/activitypub/{x-7zk8mV2p.js → x-BRfRYd16.js} +2 -2
  52. package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
  53. package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
  54. package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
  55. package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
  56. package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
  57. package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
  58. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  59. package/core/built/admin/assets/{audience-CkBb2LlO.js → audience-DTJtXXWY.js} +1 -1
  60. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  61. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  62. package/core/built/admin/assets/{automations-cMllU545.js → automations-dtQ_MRZ7.js} +1 -1
  63. package/core/built/admin/assets/{bluesky-sharing-DPGpyD-G.js → bluesky-sharing-B8Es3AuV.js} +1 -1
  64. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  65. package/core/built/admin/assets/{breadcrumb-DIfcF5ow.js → breadcrumb-tg5zL--z.js} +1 -1
  66. package/core/built/admin/assets/{chart-helpers-DV0FkdcD.js → chart-helpers-CZElTGYL.js} +1 -1
  67. package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
  68. package/core/built/admin/assets/{chunk.524.87b52f4fada529df2c4c.js → chunk.524.cd5498dd4e195f679266.js} +4 -4
  69. package/core/built/admin/assets/{chunk.582.340bcca873e806cbd4bb.js → chunk.582.6f92ec1f03734bf2ea3f.js} +6 -6
  70. package/core/built/admin/assets/{chunk.332.05991a101d67f732dec3.js → chunk.873.b8fc05a8ab877fb819ed.js} +28 -27
  71. package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
  72. package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
  73. package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
  74. package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
  75. package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
  76. package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
  77. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  78. package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
  79. package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
  80. package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
  81. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  82. package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
  83. package/core/built/admin/assets/{date-range-select-dXc-GsVe.js → date-range-select-PB2n1HZf.js} +1 -1
  84. package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
  85. package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
  86. package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
  87. package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
  88. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  89. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
  90. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  91. package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
  92. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
  93. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  94. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  95. package/core/built/admin/assets/{domain-B5ogXUBa.js → domain-BR8FIdjD.js} +1 -1
  96. package/core/built/admin/assets/{dropzone-BnFG7dDP.js → dropzone-hNxmUvwD.js} +1 -1
  97. package/core/built/admin/assets/{edit-profile-B07jCZy9.js → edit-profile-CENrkY0l.js} +1 -1
  98. package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
  99. package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
  100. package/core/built/admin/assets/{explore-Ca1y8HW_.js → explore-BmrnljWw.js} +1 -1
  101. package/core/built/admin/assets/{feed-x4iFMBP4.js → feed-Dy4li6i8.js} +1 -1
  102. package/core/built/admin/assets/{field-ChCAabZg.js → field-Dh_lwk13.js} +1 -1
  103. package/core/built/admin/assets/{filter-sources-LdtH1e-T.js → filter-sources-D2zDuJH5.js} +1 -1
  104. package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
  105. package/core/built/admin/assets/{filters-CjVKRP8U.js → filters-CFt_BDe4.js} +1 -1
  106. package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
  107. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  108. package/core/built/admin/assets/{ghost-7f01131115186d1af84ead76e79646dd.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +300 -298
  109. package/core/built/admin/assets/{gift-link-modal-Buqh2vXh.js → gift-link-modal-zMtlJhLQ.js} +1 -1
  110. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  111. package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
  112. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  113. package/core/built/admin/assets/{growth-Dy2UOSay.js → growth-C38Xtp8D.js} +2 -2
  114. package/core/built/admin/assets/{growth-DajOBdN0.js → growth-CFws67_i.js} +1 -1
  115. package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
  116. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  117. package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
  118. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  119. package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
  120. package/core/built/admin/assets/{inbox-BUOLDoji.js → inbox-BrSRzyvz.js} +13 -13
  121. package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
  122. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  123. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  124. package/core/built/admin/assets/{input-group-uSK3XT7b.js → input-group-6vEPvX9S.js} +1 -1
  125. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  126. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  127. package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
  128. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  129. package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
  130. package/core/built/admin/assets/{koenig-lexical-BFc4yFed.js → koenig-lexical-Cj62r_y9.js} +1 -1
  131. package/core/built/admin/assets/{kpi-card-BN-E6WYX.js → kpi-card-DOUnkZcN.js} +1 -1
  132. package/core/built/admin/assets/{kpi-card-CCt-qeWD.js → kpi-card-IHDo8Q56.js} +1 -1
  133. package/core/built/admin/assets/{kpi-helpers-DlxTbTs4.js → kpi-helpers-CjUdECAK.js} +1 -1
  134. package/core/built/admin/assets/{kpi-tabs-BlcUttpn.js → kpi-tabs-X3DkOYbI.js} +1 -1
  135. package/core/built/admin/assets/{label-AVrxwmsz.js → label-Dq6QaOuS.js} +1 -1
  136. package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
  137. package/core/built/admin/assets/{labels-DAIqKed7.js → labels-DAPjKPy2.js} +1 -1
  138. package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
  139. package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
  140. package/core/built/admin/assets/{link-helpers-D9LsDOkG.js → link-helpers-Bzkk68GD.js} +1 -1
  141. package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
  142. package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
  143. package/core/built/admin/assets/members-DxbSKngD.js +11 -0
  144. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  145. package/core/built/admin/assets/{moderation-CQ3urTFE.js → moderation-Da5lhhWh.js} +1 -1
  146. package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
  147. package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
  148. package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
  149. package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
  150. package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
  151. package/core/built/admin/assets/{newsletters-vt3Ni1Ov.js → newsletters-BgrIZk0V.js} +1 -1
  152. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  153. package/core/built/admin/assets/{note-Dk877BTv.js → note-DYcLOmDb.js} +1 -1
  154. package/core/built/admin/assets/{notifications-CPlLjPxO.js → notifications-B2SPFIV9.js} +1 -1
  155. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  156. package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
  157. package/core/built/admin/assets/{onboarding-route-DdStOtjy.js → onboarding-route-DgdfxkQh.js} +1 -1
  158. package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
  159. package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
  160. package/core/built/admin/assets/{page-header-pp4dWmOS.js → page-header-CpmxJj5B.js} +1 -1
  161. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  162. package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
  163. package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
  164. package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
  165. package/core/built/admin/assets/{post-analytics-BMJrI81R.js → post-analytics-CXWBOGyP.js} +1 -1
  166. package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
  167. package/core/built/admin/assets/{post-analytics-provider-Btml5vJI.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
  168. package/core/built/admin/assets/{post-share-modal-AcUZkbfL.js → post-share-modal-B9Vmk_XM.js} +1 -1
  169. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  170. package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
  171. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  172. package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
  173. package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
  174. package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
  175. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  176. package/core/built/admin/assets/{router-provider-BmEZf6c6.js → router-provider-DSQfprxH.js} +1 -1
  177. package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
  178. package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
  179. package/core/built/admin/assets/{settings-BGgk7oCI.js → settings-CpvHWix7.js} +1 -1
  180. package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
  181. package/core/built/admin/assets/{share-modal-BeuydaBf.js → share-modal-ClIyPzc5.js} +1 -1
  182. package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
  183. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  184. package/core/built/admin/assets/{sort-button-Df4t2AL8.js → sort-button-CdBsm40v.js} +1 -1
  185. package/core/built/admin/assets/{source-icon-ajoaKb41.js → source-icon-CEYmVLXR.js} +1 -1
  186. package/core/built/admin/assets/{stats-C_lfxZAw.js → stats-D8nZCTy5.js} +1 -1
  187. package/core/built/admin/assets/{stats-view-Cxot1iD8.js → stats-view-CugFVf0g.js} +1 -1
  188. package/core/built/admin/assets/{step-1-ogAA4lDr.js → step-1-DOYjrORn.js} +1 -1
  189. package/core/built/admin/assets/{step-2-C5Xini-7.js → step-2-vA6RsarH.js} +1 -1
  190. package/core/built/admin/assets/{step-3-CZv1FV81.js → step-3-BtIBxMId.js} +2 -2
  191. package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
  192. package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
  193. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  194. package/core/built/admin/assets/{table-DFj1wiYv.js → table-TMCgCQK7.js} +1 -1
  195. package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
  196. package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
  197. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  198. package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
  199. package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
  200. package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
  201. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  202. package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
  203. package/core/built/admin/assets/{topic-filter-DGDxDalo.js → topic-filter-CbgoWIHn.js} +1 -1
  204. package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
  205. package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
  206. package/core/built/admin/assets/{use-analytics-data-C76v4LIp.js → use-analytics-data-B-b4wNSC.js} +1 -1
  207. package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
  208. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
  209. package/core/built/admin/assets/use-filter-params-BnbBDc7T.js +1 -0
  210. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  211. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  212. package/core/built/admin/assets/{use-growth-stats-aNXJOmQr.js → use-growth-stats-DPU5rt3W.js} +1 -1
  213. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  214. package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
  215. package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
  216. package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
  217. package/core/built/admin/assets/{use-post-referrers-CG0SZioj.js → use-post-referrers-C2mXyrp-.js} +1 -1
  218. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  219. package/core/built/admin/assets/{use-reply-chain-data-RLdPKRmS.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
  220. package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
  221. package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
  222. package/core/built/admin/assets/{use-simple-pagination-DgcF69KI.js → use-simple-pagination-D9ckKo5-.js} +1 -1
  223. package/core/built/admin/assets/{use-tinybird-query-C1R_Kbmy.js → use-tinybird-query-GSa32hVw.js} +1 -1
  224. package/core/built/admin/assets/{use-tinybird-token-C9fPjVM_.js → use-tinybird-token-CKLOZFsw.js} +1 -1
  225. package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
  226. package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
  227. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  228. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  229. package/core/built/admin/assets/{virtual-list-BmTUzeoF.js → virtual-list-RHeEL05L.js} +1 -1
  230. package/core/built/admin/assets/web-Bw2QMO5u.js +2 -0
  231. package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
  232. package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
  233. package/core/built/admin/index.html +15 -13
  234. package/core/frontend/helpers/authors.js +1 -1
  235. package/core/frontend/helpers/link.js +3 -8
  236. package/core/frontend/helpers/tags.js +1 -1
  237. package/core/frontend/meta/author-url.js +2 -2
  238. package/core/frontend/meta/url.js +4 -4
  239. package/core/frontend/services/proxy.js +1 -1
  240. package/core/frontend/services/routing/events.js +20 -6
  241. package/core/frontend/services/routing/permalink-adapter.js +0 -13
  242. package/core/frontend/services/routing/permalink-adapter.ts +0 -13
  243. package/core/frontend/services/routing/router-manager.js +15 -9
  244. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  245. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  246. package/core/server/adapters/route-settings/utils.js +10 -2
  247. package/core/server/adapters/route-settings/utils.ts +12 -2
  248. package/core/server/api/endpoints/email-post.js +1 -1
  249. package/core/server/api/endpoints/previews.js +1 -1
  250. package/core/server/api/endpoints/search-index-public.js +3 -3
  251. package/core/server/api/endpoints/search-index.js +4 -4
  252. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  253. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  254. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  255. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  256. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  257. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  258. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  259. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  260. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  261. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  262. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  263. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  264. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  265. package/core/server/data/schema/commands.js +21 -5
  266. package/core/server/data/schema/schema.js +24 -5
  267. package/core/server/lib/common/to-plain.ts +1 -1
  268. package/core/server/models/post.js +2 -2
  269. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  270. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  271. package/core/server/services/adapter-manager/adapter-paths.js +24 -0
  272. package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
  273. package/core/server/services/adapter-manager/base-classes.js +22 -0
  274. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  275. package/core/server/services/adapter-manager/index.js +4 -27
  276. package/core/server/services/adapter-manager/index.ts +4 -31
  277. package/core/server/services/adapter-manager/utils.js +50 -0
  278. package/core/server/services/adapter-manager/utils.ts +57 -0
  279. package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
  280. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  281. package/core/server/services/automations/service.js +2 -2
  282. package/core/server/services/automations/service.ts +2 -2
  283. package/core/server/services/comments/comments-service-emails.js +1 -1
  284. package/core/server/services/comments/comments-service.js +1 -1
  285. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
  286. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  287. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  288. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  289. package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
  290. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
  291. package/core/server/services/lib/mailgun-client.js +3 -3
  292. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  293. package/core/server/services/member-attribution/url-translator.js +5 -7
  294. package/core/server/services/member-welcome-emails/service.js +72 -12
  295. package/core/server/services/members/api.js +1 -1
  296. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  297. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  298. package/core/server/services/members/import-export/import/importer.js +25 -4
  299. package/core/server/services/members/import-export/import/importer.ts +26 -6
  300. package/core/server/services/members/import-export/import/row.ts +5 -1
  301. package/core/server/services/members/import-export/index.js +1 -5
  302. package/core/server/services/members/import-export/index.ts +2 -6
  303. package/core/server/services/members-custom-fields/queries.js +4 -13
  304. package/core/server/services/members-custom-fields/queries.ts +4 -13
  305. package/core/server/services/members-custom-fields/schema.js +18 -27
  306. package/core/server/services/members-custom-fields/schema.ts +17 -27
  307. package/core/server/services/members-custom-fields/storage.js +94 -27
  308. package/core/server/services/members-custom-fields/storage.ts +114 -39
  309. package/core/server/services/members-custom-fields/values-service.js +98 -219
  310. package/core/server/services/members-custom-fields/values-service.ts +110 -238
  311. package/core/server/services/mentions/resource-service.js +1 -1
  312. package/core/server/services/mentions/service.js +2 -2
  313. package/core/server/services/route-settings/dynamic-routing-service.js +9 -59
  314. package/core/server/services/slack-ping/slack-ping-service.js +2 -2
  315. package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
  316. package/core/server/services/stats/content-stats-service.js +3 -3
  317. package/core/server/services/stats/posts-stats-service.js +2 -2
  318. package/core/server/services/url/config.js +90 -135
  319. package/core/server/services/url/index.js +6 -49
  320. package/core/server/services/url/lazy-find-resource.js +14 -11
  321. package/core/server/services/url/lazy-find-resource.ts +14 -11
  322. package/core/server/services/url/lazy-url-service.js +174 -94
  323. package/core/server/services/url/lazy-url-service.ts +233 -97
  324. package/core/server/services/url/routable-resources.js +62 -71
  325. package/core/server/services/url/router-filter.js +12 -4
  326. package/core/server/services/url/router-filter.ts +12 -4
  327. package/core/server/services/webhooks/index.js +1 -1
  328. package/core/server/services/webhooks/serialize.js +2 -1
  329. package/core/shared/config/env/config.testing-mysql.json +1 -2
  330. package/core/shared/config/env/config.testing.json +1 -2
  331. package/core/shared/events/index.js +1 -2
  332. package/core/shared/labs.js +3 -3
  333. package/package.json +5 -6
  334. package/pnpm-lock.yaml +6 -9
  335. package/pnpm-workspace.yaml +2 -2
  336. package/tsconfig.json +1 -0
  337. package/core/built/admin/assets/Combination-BCm9FL4o.js +0 -41
  338. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +0 -1
  339. package/core/built/admin/assets/automated-emails-6t2bIRWf.js +0 -7
  340. package/core/built/admin/assets/checkbox-BIl077a0.js +0 -1
  341. package/core/built/admin/assets/code-editor-view-C0aWNwdl.js +0 -1
  342. package/core/built/admin/assets/comments-qwhIjH3c.js +0 -1
  343. package/core/built/admin/assets/disabled-sources-indicator-DYAXW-6V.js +0 -1
  344. package/core/built/admin/assets/dist-BUKmD51u.js +0 -1
  345. package/core/built/admin/assets/dist-CNlMj7ML.js +0 -1
  346. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  347. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  348. package/core/built/admin/assets/editor-DDYSOXGN.js +0 -8
  349. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  350. package/core/built/admin/assets/filters-CLehPo_D.js +0 -1
  351. package/core/built/admin/assets/index-08n05-rc.js +0 -67
  352. package/core/built/admin/assets/index-DgUzgFvS.css +0 -1
  353. package/core/built/admin/assets/label-picker-DVnpu_IF.js +0 -1
  354. package/core/built/admin/assets/layout-B0YLh7R9.js +0 -1
  355. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +0 -1
  356. package/core/built/admin/assets/member-detail-CIVAdKXf.js +0 -1
  357. package/core/built/admin/assets/members-oLPiSkXD.js +0 -10
  358. package/core/built/admin/assets/modals-CB7v-k3T.js +0 -31
  359. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +0 -1
  360. package/core/built/admin/assets/overview--4V-kx9c.js +0 -1
  361. package/core/built/admin/assets/overview-DSLv30oz.js +0 -1
  362. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +0 -1
  363. package/core/built/admin/assets/preferences-Ru4CRPHC.js +0 -5
  364. package/core/built/admin/assets/profile-BNb1oj7D.js +0 -1
  365. package/core/built/admin/assets/select-B0HteHrp.js +0 -1
  366. package/core/built/admin/assets/settings-B1C9Q3wo.js +0 -4
  367. package/core/built/admin/assets/social-urls-DA2lXrhW.js +0 -8
  368. package/core/built/admin/assets/switch-CyQcwMAv.js +0 -1
  369. package/core/built/admin/assets/tabs-D4y1gqUd.js +0 -1
  370. package/core/built/admin/assets/tags-Dmft-Y4O.js +0 -1
  371. package/core/built/admin/assets/toggle-group-R2QMFvXW.js +0 -1
  372. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +0 -1
  373. package/core/built/admin/assets/web-BqW0b0_C.js +0 -1
  374. package/core/built/admin/assets/web-Cy5GKMWM.js +0 -2
  375. package/core/server/services/url/local-file-cache.js +0 -75
  376. package/core/server/services/url/queue.js +0 -260
  377. package/core/server/services/url/resource.js +0 -102
  378. package/core/server/services/url/resources.js +0 -480
  379. package/core/server/services/url/url-generator.js +0 -276
  380. package/core/server/services/url/url-service-facade.js +0 -366
  381. package/core/server/services/url/url-service-facade.ts +0 -519
  382. package/core/server/services/url/url-service.js +0 -387
  383. package/core/server/services/url/urls.js +0 -140
  384. package/core/shared/events/url-resource-updated-event.js +0 -33
  385. /package/core/built/admin/assets/{dist-CqO2lWS0.js → dist-CqO2lWS02.js} +0 -0
@@ -1,14 +1,53 @@
1
1
  const debug = require('@tryghost/debug')('services:url:lazy');
2
2
  const errors = require('@tryghost/errors');
3
+ const logging = require('@tryghost/logging');
3
4
  const localUtils = require('../../../shared/url-utils').default;
4
5
  const {matchPermalink, toLookupParams} = require('./permalink-matcher');
5
6
  const {buildFilter, filterMatches, routerTypeOf} = require('./router-filter');
7
+ const {fetchRoutableResources: defaultFetchRoutableResources} = require('./routable-resources');
6
8
  const urlConfig = require('./config');
7
9
 
8
- import type {Resource, UrlOptions, LazyUrlServiceBackend} from './url-service-facade';
9
10
  import type {CompiledFilter} from './router-filter';
10
11
  import type {PermalinkParams} from './permalink-matcher';
11
12
 
13
+ /**
14
+ * Routing-level resource. `type` is one of the plural router keys
15
+ * ('posts', 'pages', 'tags', 'authors'). Concrete records carry additional
16
+ * fields (slug, published_at, primary_tag, ...) read by the permalink
17
+ * templates and router filters.
18
+ */
19
+ export interface Resource {
20
+ type: string;
21
+ id: string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export interface UrlOptions {
26
+ absolute?: boolean;
27
+ withSubdirectory?: boolean;
28
+ // Diagnostic only: the producing api endpoint and the fetch shape its
29
+ // input serializer settled on, threaded from the output serializer so a
30
+ // thin-resource report names what under-fetched. Ignored by URL
31
+ // generation; read only into that report.
32
+ serializerContext?: {
33
+ apiType?: string;
34
+ docName?: string;
35
+ method?: string;
36
+ withRelated?: unknown;
37
+ columns?: unknown;
38
+ forcedUrlRelations?: unknown;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * On-demand enumeration of the routable rows of a type. Injectable so tests
44
+ * can drive `getRoutableResources` without a database.
45
+ */
46
+ export type FetchRoutableResources = (
47
+ type: string,
48
+ options: {columns?: string[]; requiredFields?: string[]; requiredRelations?: string[]}
49
+ ) => Promise<Array<Record<string, unknown>>>;
50
+
12
51
  export type ResourceLookupParams = {id: string} | {slug: string};
13
52
 
14
53
  export type FindResource = (
@@ -24,17 +63,15 @@ interface RouterConfig {
24
63
  compiledFilter: CompiledFilter | null;
25
64
  }
26
65
 
27
- // Per-resource-type base filters, mirroring eager's resource-fetch filters
28
- // (`modelOptions.filter` in services/url/config.js). Deliberately duplicated
29
- // rather than imported: lazy is meant to replace eager, at which point eager's
30
- // config goes away and this becomes the single source. `fields` lists the
31
- // record columns each filter reads; a resource that reaches URL generation
32
- // must carry them (a thin one is rejected — see _assertBaseFieldsPresent).
66
+ // Per-resource-type gate on which rows get a URL at all — the forward-lookup
67
+ // counterpart of the fetch filters in routable-resources.js. `fields` lists
68
+ // the record columns each filter reads; a resource that reaches URL generation
69
+ // must carry them (see _missingBaseFields).
33
70
  //
34
71
  // Authors are intentionally absent: users.visibility is schema-pinned to
35
- // 'public' (isIn: [['public']]), so eager's visibility:public author filter
36
- // never excludes anyone — every author is routable, and serialized authors
37
- // drop visibility anyway (#10438).
72
+ // 'public' (isIn: [['public']]), so the visibility:public author filter never
73
+ // excludes anyone — every author is routable, and serialized authors drop
74
+ // visibility anyway (#10438).
38
75
  const BASE_FILTERS: Record<string, {filter: string; fields: string[]}> = {
39
76
  posts: {filter: 'status:published+type:post', fields: ['status', 'type']},
40
77
  pages: {filter: 'status:published+type:page', fields: ['status', 'type']},
@@ -47,6 +84,16 @@ interface BaseFilter {
47
84
  fields: string[];
48
85
  }
49
86
 
87
+ // Why a resource could not be routed: the columns it was missing, and which
88
+ // filter needed them. Reported, then degraded to /404/.
89
+ interface ThinResource {
90
+ resourceType: string;
91
+ missing: string[];
92
+ baseFilter?: string;
93
+ routerIdentifier?: string;
94
+ filter?: string | null;
95
+ }
96
+
50
97
  function buildBaseFilters(): Map<string, BaseFilter> {
51
98
  const baseFilters = new Map<string, BaseFilter>();
52
99
  for (const [type, {filter, fields}] of Object.entries(BASE_FILTERS)) {
@@ -58,19 +105,18 @@ function buildBaseFilters(): Map<string, BaseFilter> {
58
105
  return baseFilters;
59
106
  }
60
107
 
61
- // Columns eager drops from its in-memory URL cache (the `exclude` lists in
62
- // services/url/config.js). Eager evaluates a router's collection filter against
63
- // that reduced cached record, so an excluded column reads as absent — NQL then
64
- // treats it as null. Lazy loads full records and would see the real value, so
65
- // to preserve eager's behaviour it strips these columns before evaluating
66
- // router filters (and neither requires nor force-loads them). Keyed by
67
- // resourceType ('posts'/'pages'/'tags'). Read from eager's config directly so
68
- // the two can't drift; when eager is retired this derives from whatever
69
- // replaces it. The base filter is unaffected — it runs against the full record.
108
+ // Columns a router filter must not see (the `exclude` lists in
109
+ // services/url/config.js), keyed by resourceType. Ghost previously answered
110
+ // URLs from a precomputed cache that dropped these columns, so a filter
111
+ // referencing one matched it as absent NQL reads absent as null. This
112
+ // service loads full records and would see the real value, so it strips them
113
+ // before evaluating router filters (and neither requires nor force-loads
114
+ // them), keeping routes.yaml files that rely on that behaviour working. The
115
+ // base filter above is unaffected it runs against the full record.
70
116
  function buildExcludedFilterFields(): Map<string, Set<string>> {
71
117
  const excluded = new Map<string, Set<string>>();
72
118
  for (const entry of urlConfig) {
73
- const exclude = entry?.modelOptions?.exclude;
119
+ const exclude = entry?.exclude;
74
120
  if (Array.isArray(exclude)) {
75
121
  excluded.set(entry.type, new Set(exclude));
76
122
  }
@@ -80,10 +126,19 @@ function buildExcludedFilterFields(): Map<string, Set<string>> {
80
126
 
81
127
  const EMPTY_FIELD_SET: ReadonlySet<string> = new Set();
82
128
 
129
+ // 1, 10, 100, ... — exact for integers, unlike a log10 comparison.
130
+ function isPowerOfTen(n: number): boolean {
131
+ let power = 1;
132
+ while (power < n) {
133
+ power *= 10;
134
+ }
135
+ return power === n;
136
+ }
137
+
83
138
  // Relation roots are loaded via getRequiredRelations (as withRelated), not as
84
139
  // scalar columns; `page`/`type` are the router-type discriminator, always set
85
140
  // on the resource. Everything else a router filter references is a scalar
86
- // own-column the resource must carry to be routed like eager.
141
+ // own-column the resource must carry to be routed.
87
142
  const FILTER_NON_SCALAR_FIELDS = new Set([
88
143
  'tag', 'tags', 'author', 'authors', 'primary_tag', 'primary_author', 'page', 'type'
89
144
  ]);
@@ -115,26 +170,38 @@ function filterScalarFields(filter: string | null): string[] {
115
170
  interface LazyUrlServiceDeps {
116
171
  urlUtils?: typeof localUtils;
117
172
  findResource: FindResource;
173
+ fetchRoutableResources?: FetchRoutableResources;
118
174
  }
119
175
 
120
176
  const ROUTER_TYPE_TO_DB_TYPE: Record<string, string> = {posts: 'post', pages: 'page'};
121
177
 
122
178
  /**
123
- * On-demand replacement for the eager UrlService: computes URLs and ownership
124
- * per call from the registered router configs instead of precomputing a full
125
- * map at boot. Forward lookups are pure; resolveUrl is the only DB-touching
126
- * path, and only through the injected findResource hook.
179
+ * Ghost's URL service: computes URLs and ownership per call from the
180
+ * registered router configs. Forward lookups are pure; resolveUrl and
181
+ * getRoutableResources are the only DB-touching paths, and only through the
182
+ * injected findResource / fetchRoutableResources hooks.
127
183
  */
128
- export class LazyUrlService implements LazyUrlServiceBackend {
184
+ export class LazyUrlService {
129
185
  private urlUtils: typeof localUtils;
130
186
  private findResource: FindResource;
187
+ private fetchRoutableResources: FetchRoutableResources;
131
188
  // Router configs in registration order, which is also their priority.
132
189
  private routerConfigs: RouterConfig[];
133
190
  private requiredRelations: string[] | null;
134
191
  private baseFilters: Map<string, BaseFilter>;
135
192
  private excludedFilterFields: Map<string, Set<string>>;
136
-
137
- constructor({urlUtils = localUtils, findResource}: LazyUrlServiceDeps) {
193
+ // True once routers have been registered. Cleared by reset() so the
194
+ // route-reload window re-gates the maintenance middleware.
195
+ private routersReady: boolean;
196
+ // How many times each thin-resource cause has been seen — see
197
+ // _degradeThinResource.
198
+ private reportedThinResources: Map<string, number>;
199
+
200
+ constructor({
201
+ urlUtils = localUtils,
202
+ findResource,
203
+ fetchRoutableResources = defaultFetchRoutableResources
204
+ }: LazyUrlServiceDeps) {
138
205
  if (typeof findResource !== 'function') {
139
206
  throw new errors.IncorrectUsageError({
140
207
  message: 'LazyUrlService requires a findResource function'
@@ -142,10 +209,13 @@ export class LazyUrlService implements LazyUrlServiceBackend {
142
209
  }
143
210
  this.urlUtils = urlUtils;
144
211
  this.findResource = findResource;
212
+ this.fetchRoutableResources = fetchRoutableResources;
145
213
  this.routerConfigs = [];
146
214
  this.requiredRelations = null;
147
215
  this.baseFilters = buildBaseFilters();
148
216
  this.excludedFilterFields = buildExcludedFilterFields();
217
+ this.routersReady = false;
218
+ this.reportedThinResources = new Map();
149
219
  }
150
220
 
151
221
  onRouterAddedType(identifier: string, filter: string | null, resourceType: string, permalink: string): void {
@@ -158,6 +228,7 @@ export class LazyUrlService implements LazyUrlServiceBackend {
158
228
  compiledFilter: buildFilter(filter)
159
229
  });
160
230
  this.requiredRelations = null;
231
+ this.routersReady = true;
161
232
  }
162
233
 
163
234
  onRouterUpdated(): void {
@@ -169,6 +240,8 @@ export class LazyUrlService implements LazyUrlServiceBackend {
169
240
  reset(): void {
170
241
  this.routerConfigs = [];
171
242
  this.requiredRelations = null;
243
+ this.routersReady = false;
244
+ this.reportedThinResources.clear();
172
245
  }
173
246
 
174
247
  getRequiredRelations(): string[] {
@@ -198,10 +271,10 @@ export class LazyUrlService implements LazyUrlServiceBackend {
198
271
  return [...this.requiredRelations];
199
272
  }
200
273
 
201
- // Columns a resource of this type must carry for the lazy service to build
274
+ // Columns a resource of this type must carry for the service to build
202
275
  // its URL: its base-filter columns plus the scalar columns its routers'
203
276
  // permalinks substitute and filters read. Relations are covered separately
204
- // by getRequiredRelations; eager needs none of this (it looks URLs up by id).
277
+ // by getRequiredRelations.
205
278
  getRequiredFields(routerType: string): string[] {
206
279
  const fields = new Set<string>();
207
280
  const excluded = this._excludedFilterFieldsFor(routerType);
@@ -233,9 +306,10 @@ export class LazyUrlService implements LazyUrlServiceBackend {
233
306
  fields.add(computed);
234
307
  }
235
308
  }
236
- // Skip columns eager drops from its cache: it never force-loads them
237
- // and evaluates their filters against an absent value, so lazy must
238
- // not require them either (see buildExcludedFilterFields).
309
+ // Skip the columns a router filter must not see: their filters are
310
+ // evaluated against an absent value, so requiring them would
311
+ // force-load a column that can't affect the result (see
312
+ // buildExcludedFilterFields).
239
313
  filterScalarFields(config.filter).forEach((field) => {
240
314
  if (!excluded.has(field)) {
241
315
  fields.add(field);
@@ -246,30 +320,50 @@ export class LazyUrlService implements LazyUrlServiceBackend {
246
320
  }
247
321
 
248
322
  hasFinished(): boolean {
249
- return true;
323
+ return this.routersReady;
250
324
  }
251
325
 
326
+ /**
327
+ * All routable rows of a type. The columns URL computation needs come
328
+ * from the active routing config, so the rows are never thin for it;
329
+ * callers name any extra columns they want back.
330
+ */
331
+ async getRoutableResources(type: string, {columns = []}: {columns?: string[]} = {}): Promise<Array<Record<string, unknown>>> {
332
+ return this.fetchRoutableResources(type, {
333
+ columns,
334
+ requiredFields: this.getRequiredFields(type),
335
+ requiredRelations: this.getRequiredRelations()
336
+ });
337
+ }
338
+
339
+ /**
340
+ * A thin resource — one missing a column its base filter or a router
341
+ * filter reads — is a caller bug: we cannot tell whether it routes. It is
342
+ * reported and degraded to /404/ rather than thrown, because 500ing a page
343
+ * that does route is worse than a 404 on one that may not.
344
+ */
252
345
  getUrlForResource(resource: Resource, options: UrlOptions = {}): string {
253
346
  const routerType = routerTypeOf(resource);
254
347
  if (!routerType) {
255
- return this._formatNotFound(options);
348
+ return this.notFoundUrl(options);
256
349
  }
257
350
 
258
351
  const record = this._recordForFilter(resource);
259
- // The router (collection) filter is evaluated against eager's reduced
260
- // column set; the base filter keeps the full record (it reads status /
261
- // type / visibility, which eager applies at query time, not in cache).
352
+ // The router (collection) filter is evaluated against the reduced
353
+ // column set (see buildExcludedFilterFields); the base filter keeps the
354
+ // full record, since it reads status / type / visibility.
262
355
  const filterRecord = this._recordForRouterFilter(record, routerType);
263
356
 
264
- // Eager only builds URLs for resources that pass the per-type base
265
- // filter (e.g. visibility:public tags, status:published posts), so a
266
- // resource failing it has no URL there and must 404 here too. Checked
267
- // only when a router for the type exists, since otherwise the resource
268
- // 404s regardless.
357
+ // Only resources that pass the per-type base filter (visibility:public
358
+ // tags, status:published posts) get a URL. Checked only when a router
359
+ // for the type exists, since otherwise the resource 404s regardless.
269
360
  if (this._hasRouterForType(routerType)) {
270
- this._assertBaseFieldsPresent(routerType, resource);
361
+ const missing = this._missingBaseFields(routerType, resource);
362
+ if (missing) {
363
+ return this._degradeThinResource(resource, missing, options);
364
+ }
271
365
  if (!this._baseFilterMatches(routerType, record)) {
272
- return this._formatNotFound(options);
366
+ return this.notFoundUrl(options);
273
367
  }
274
368
  }
275
369
 
@@ -277,13 +371,16 @@ export class LazyUrlService implements LazyUrlServiceBackend {
277
371
  if (config.resourceType !== routerType) {
278
372
  continue;
279
373
  }
280
- this._assertNotThin(config, resource, routerType);
374
+ const missing = this._missingRouterFields(config, resource, routerType);
375
+ if (missing) {
376
+ return this._degradeThinResource(resource, missing, options);
377
+ }
281
378
  if (filterMatches(config.compiledFilter, filterRecord)) {
282
379
  const path = this.urlUtils.replacePermalink(config.permalink, resource);
283
380
  return this._formatPath(path, options);
284
381
  }
285
382
  }
286
- return this._formatNotFound(options);
383
+ return this.notFoundUrl(options);
287
384
  }
288
385
 
289
386
  ownsResource(routerIdentifier: string, resource: Resource | null): boolean {
@@ -294,15 +391,15 @@ export class LazyUrlService implements LazyUrlServiceBackend {
294
391
  if (!routerType) {
295
392
  return false;
296
393
  }
297
- // A resource failing its base filter is not in eager's map, so no
298
- // router owns it. Mirrors the base-filter gate in getUrlForResource.
394
+ // A resource failing its base filter has no URL, so no router owns it.
395
+ // Mirrors the base-filter gate in getUrlForResource.
299
396
  const record = this._recordForFilter(resource);
300
397
  if (!this._baseFilterMatches(routerType, record)) {
301
398
  return false;
302
399
  }
303
400
  const filterRecord = this._recordForRouterFilter(record, routerType);
304
401
  // Ownership is exclusive: only the first matching router of the type
305
- // owns the resource, matching eager's reservation.
402
+ // owns the resource.
306
403
  const owner = this.routerConfigs.find(
307
404
  c => c.resourceType === routerType && filterMatches(c.compiledFilter, filterRecord)
308
405
  );
@@ -352,11 +449,11 @@ export class LazyUrlService implements LazyUrlServiceBackend {
352
449
  return null;
353
450
  }
354
451
 
355
- // Eager only resolves a URL that equals a resource's generated (canonical)
356
- // URL, so we regenerate the record's URL for this permalink and confirm the
357
- // captured params match it. Without this, derived/relation segments the
358
- // query can't filter on (year/month, primary_tag) would resolve any slug,
359
- // 200-ing a URL the eager service 404s.
452
+ // Only a URL that equals the resource's own generated (canonical) URL
453
+ // resolves, so we regenerate the record's URL for this permalink and
454
+ // confirm the captured params match it. Without this, derived/relation
455
+ // segments the query can't filter on (year/month, primary_tag) would
456
+ // resolve any slug, 200-ing a URL that has no page.
360
457
  private _matchesCanonicalUrl(config: RouterConfig, params: PermalinkParams, resource: Record<string, unknown>): boolean {
361
458
  const canonicalPath = this.urlUtils.replacePermalink(config.permalink, resource);
362
459
  const canonicalParams = matchPermalink(config.permalink, canonicalPath);
@@ -368,6 +465,52 @@ export class LazyUrlService implements LazyUrlServiceBackend {
368
465
  return Object.keys(captured).every(key => canonical[key] === captured[key]);
369
466
  }
370
467
 
468
+ // Reports which columns were missing alongside what the caller handed over
469
+ // — the resource's shape, and what the active routing config needs — so
470
+ // the fetch that under-fetched can be found from the log line alone.
471
+ //
472
+ // Reported on the first occurrence of each distinct cause, then at each
473
+ // order of magnitude. A caller that under-fetches does so for every row it
474
+ // serializes, so a browse over a large collection would otherwise emit one
475
+ // error per row — but reporting only once would hide that the problem is
476
+ // still happening, and how widely. `occurrences` carries that instead.
477
+ // Cleared on reset(), since a new routing config can make a previously
478
+ // thin resource fine (or newly broken).
479
+ private _degradeThinResource(resource: Resource, thin: ThinResource, options: UrlOptions): string {
480
+ const producedBy = options.serializerContext;
481
+ // The producer is part of the key: two endpoints under-fetching the
482
+ // same way are two bugs, and the endpoint set is small enough that
483
+ // this cannot flood.
484
+ const key = [
485
+ thin.resourceType,
486
+ thin.routerIdentifier ?? '',
487
+ thin.missing.join(','),
488
+ producedBy ? `${producedBy.apiType}:${producedBy.docName}:${producedBy.method}` : ''
489
+ ].join('|');
490
+ const occurrences = (this.reportedThinResources.get(key) ?? 0) + 1;
491
+ this.reportedThinResources.set(key, occurrences);
492
+ if (!isPowerOfTen(occurrences)) {
493
+ return this.notFoundUrl(options);
494
+ }
495
+
496
+ logging.error(new errors.InternalServerError({
497
+ message: 'URL service could not build a URL, degraded to /404/',
498
+ code: 'LAZY_URL_RESOLUTION_ERROR',
499
+ errorDetails: {
500
+ method: 'getUrlForResource',
501
+ type: resource.type,
502
+ id: resource.id,
503
+ status: (resource as Record<string, unknown>).status,
504
+ resourceKeys: Object.keys(resource),
505
+ requiredRelations: this.getRequiredRelations(),
506
+ occurrences,
507
+ ...(producedBy ? {serializer: producedBy} : {}),
508
+ ...thin
509
+ }
510
+ }));
511
+ return this.notFoundUrl(options);
512
+ }
513
+
371
514
  private _hasRouterForType(routerType: string): boolean {
372
515
  return this.routerConfigs.some(config => config.resourceType === routerType);
373
516
  }
@@ -382,33 +525,24 @@ export class LazyUrlService implements LazyUrlServiceBackend {
382
525
 
383
526
  // A resource that reaches URL generation must carry the columns its base
384
527
  // filter reads (status for posts/pages, visibility for tags) — production
385
- // callers always do (full models, or forced by the serializers). Without
386
- // them the filter can't be evaluated and we'd silently 404 a URL eager would
387
- // have produced, so refuse loudly instead of guessing.
388
- private _assertBaseFieldsPresent(routerType: string, resource: Resource): void {
528
+ // callers always do, via full models or the serializers' force-load.
529
+ // Without them the filter can't be evaluated at all, so we cannot tell a
530
+ // genuine miss from a caller that under-fetched.
531
+ private _missingBaseFields(routerType: string, resource: Resource): ThinResource | null {
389
532
  const base = this.baseFilters.get(routerType);
390
533
  if (!base) {
391
- return;
534
+ return null;
392
535
  }
393
536
  const r = resource as Record<string, unknown>;
394
537
  const missing = base.fields.filter(field => r[field] === undefined);
395
538
  if (missing.length === 0) {
396
- return;
539
+ return null;
397
540
  }
398
- throw new errors.InternalServerError({
399
- message: 'Thin resource passed to LazyUrlService.getUrlForResource',
400
- code: 'LAZY_URL_THIN_RESOURCE',
401
- errorDetails: {
402
- resourceType: routerType,
403
- resourceId: resource.id,
404
- baseFilter: base.filter,
405
- missing
406
- }
407
- });
541
+ return {resourceType: routerType, baseFilter: base.filter, missing};
408
542
  }
409
543
 
410
544
  // Normalizes the plural router type to the singular DB value for filter
411
- // evaluation only, so page: filters match as in the eager generator.
545
+ // evaluation only, so `page:` filters in routes.yaml match.
412
546
  private _recordForFilter(resource: Resource): Record<string, unknown> {
413
547
  const record = resource as Record<string, unknown>;
414
548
  const dbType = ROUTER_TYPE_TO_DB_TYPE[record.type as string];
@@ -419,8 +553,7 @@ export class LazyUrlService implements LazyUrlServiceBackend {
419
553
  return this.excludedFilterFields.get(routerType) ?? EMPTY_FIELD_SET;
420
554
  }
421
555
 
422
- // Strips the columns eager drops from its cache so a router/collection
423
- // filter is evaluated against the same reduced shape eager uses — an
556
+ // Strips the columns a router/collection filter must not see, so an
424
557
  // excluded column reads as absent (→ null in NQL) rather than its real
425
558
  // value. Only affects filters that reference an excluded column; every
426
559
  // other filter sees an identical record.
@@ -448,8 +581,16 @@ export class LazyUrlService implements LazyUrlServiceBackend {
448
581
  return path;
449
582
  }
450
583
 
451
- // Mirrors the eager miss path: the /404/ fallback carries no subdirectory.
452
- private _formatNotFound(options: UrlOptions): string {
584
+ /**
585
+ * The /404/ a resource gets when nothing routes it, formatted for the given
586
+ * options. Deliberately omits the argument `_formatPath` passes.
587
+ *
588
+ * The subdirectory is not lost by that omission: `createUrl` takes it from
589
+ * its own base whenever the url is relative, so a subdirectory install gets
590
+ * `/blog/404/` from here either way. The third argument `_formatPath`
591
+ * passes is `trailingSlash`, and `/404/` already ends in one.
592
+ */
593
+ private notFoundUrl(options: UrlOptions = {}): string {
453
594
  if (options.absolute) {
454
595
  return this.urlUtils.createUrl('/404/', options.absolute);
455
596
  }
@@ -460,13 +601,13 @@ export class LazyUrlService implements LazyUrlServiceBackend {
460
601
  }
461
602
 
462
603
  // A filtered router that references a relation the resource doesn't carry
463
- // can't be evaluated: lazy would 404 here while eager (full data in memory)
464
- // returns a URL. Callers must hand the service fully-inflated resources, so
465
- // a thin one is a programmer error we refuse loudly rather than mask as a
466
- // silent /404/.
467
- private _assertNotThin(config: RouterConfig, resource: Resource, routerType: string): void {
604
+ // can't be evaluated, so the resource would 404 for a reason that has
605
+ // nothing to do with the routing config. Callers must hand over
606
+ // fully-inflated resources; a thin one is reported rather than silently
607
+ // 404'd (see getUrlForResource).
608
+ private _missingRouterFields(config: RouterConfig, resource: Resource, routerType: string): ThinResource | null {
468
609
  if (!config.filter) {
469
- return;
610
+ return null;
470
611
  }
471
612
  const r = resource as Record<string, unknown>;
472
613
  const missing: string[] = [];
@@ -482,8 +623,8 @@ export class LazyUrlService implements LazyUrlServiceBackend {
482
623
  if (/\bprimary_author\b/.test(config.filter) && r.primary_author === undefined) {
483
624
  missing.push('primary_author');
484
625
  }
485
- // Columns eager drops from its cache are never required — its filters
486
- // match them as absent, so a resource lacking one is not thin here.
626
+ // Excluded columns are never required — their filters match them as
627
+ // absent, so a resource lacking one is not thin here.
487
628
  const excluded = this._excludedFilterFieldsFor(routerType);
488
629
  for (const field of filterScalarFields(config.filter)) {
489
630
  if (!excluded.has(field) && r[field] === undefined) {
@@ -491,19 +632,14 @@ export class LazyUrlService implements LazyUrlServiceBackend {
491
632
  }
492
633
  }
493
634
  if (missing.length === 0) {
494
- return;
495
- }
496
- throw new errors.InternalServerError({
497
- message: 'Thin resource passed to LazyUrlService.getUrlForResource',
498
- code: 'LAZY_URL_THIN_RESOURCE',
499
- errorDetails: {
500
- resourceType: routerType,
501
- resourceId: resource.id,
502
- routerIdentifier: config.identifier,
503
- filter: config.filter,
504
- missing
505
- }
506
- });
635
+ return null;
636
+ }
637
+ return {
638
+ resourceType: routerType,
639
+ routerIdentifier: config.identifier,
640
+ filter: config.filter,
641
+ missing
642
+ };
507
643
  }
508
644
  }
509
645