emdash 0.17.1 → 0.18.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 (285) hide show
  1. package/dist/api/route-utils.mjs +11 -11
  2. package/dist/{api-Dmz40c2V.mjs → api-Cs7DAACP.mjs} +12 -12
  3. package/dist/{api-Dmz40c2V.mjs.map → api-Cs7DAACP.mjs.map} +1 -1
  4. package/dist/{apply-CuuZG6op.mjs → apply-BWMV4Zmw.mjs} +16 -16
  5. package/dist/{apply-CuuZG6op.mjs.map → apply-BWMV4Zmw.mjs.map} +1 -1
  6. package/dist/astro/index.mjs +1 -1
  7. package/dist/astro/middleware/auth.mjs +2 -2
  8. package/dist/astro/middleware/redirect.mjs +5 -5
  9. package/dist/astro/middleware.d.mts.map +1 -1
  10. package/dist/astro/middleware.mjs +274 -91
  11. package/dist/astro/middleware.mjs.map +1 -1
  12. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  13. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  14. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  15. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  16. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  17. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  18. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  19. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  20. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +9 -9
  21. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
  22. package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
  23. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
  24. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  25. package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
  26. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  27. package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
  28. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  29. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  30. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  31. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  32. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +26 -26
  33. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +26 -26
  34. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
  35. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +26 -26
  36. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +26 -26
  37. package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
  38. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  39. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
  40. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +26 -26
  41. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
  42. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +26 -26
  43. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +27 -27
  44. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +26 -26
  45. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +27 -27
  46. package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
  47. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
  48. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  49. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
  50. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  51. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  52. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  53. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  54. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  55. package/dist/astro/routes/api/auth/dev-bypass.mjs +4 -4
  56. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  57. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  58. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  59. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  60. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  61. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  62. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  63. package/dist/astro/routes/api/auth/me.mjs +4 -4
  64. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  65. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  66. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  67. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  68. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  69. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  70. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  71. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  72. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  73. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  74. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  75. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  76. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  77. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  78. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  79. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  80. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  81. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  82. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  83. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +9 -9
  84. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  86. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  87. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  88. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  89. package/dist/astro/routes/api/dashboard.mjs +7 -7
  90. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  91. package/dist/astro/routes/api/import/probe.mjs +4 -4
  92. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  93. package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
  94. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  95. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  96. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
  97. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  98. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +6 -6
  99. package/dist/astro/routes/api/manifest.mjs +3 -3
  100. package/dist/astro/routes/api/mcp.mjs +26 -26
  101. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  102. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  103. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  104. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  105. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  106. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  107. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  108. package/dist/astro/routes/api/media.mjs +5 -5
  109. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  110. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  111. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  112. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  113. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  114. package/dist/astro/routes/api/menus/index.mjs +5 -5
  115. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  116. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  117. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  118. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  119. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  120. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  121. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  122. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  123. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  124. package/dist/astro/routes/api/redirects/404s/index.mjs +7 -7
  125. package/dist/astro/routes/api/redirects/404s/summary.mjs +7 -7
  126. package/dist/astro/routes/api/redirects/_id_.mjs +8 -8
  127. package/dist/astro/routes/api/redirects/index.mjs +8 -8
  128. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  129. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  130. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +26 -26
  131. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
  132. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
  133. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
  134. package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
  135. package/dist/astro/routes/api/schema/index.mjs +7 -7
  136. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
  137. package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
  138. package/dist/astro/routes/api/search/enable.mjs +8 -8
  139. package/dist/astro/routes/api/search/index.mjs +7 -7
  140. package/dist/astro/routes/api/search/rebuild.mjs +8 -8
  141. package/dist/astro/routes/api/search/stats.mjs +7 -7
  142. package/dist/astro/routes/api/search/suggest.mjs +7 -7
  143. package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
  144. package/dist/astro/routes/api/sections/index.mjs +7 -7
  145. package/dist/astro/routes/api/settings/email.mjs +4 -4
  146. package/dist/astro/routes/api/settings.mjs +9 -9
  147. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  148. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  149. package/dist/astro/routes/api/setup/dev-bypass.mjs +16 -16
  150. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  151. package/dist/astro/routes/api/setup/index.mjs +17 -17
  152. package/dist/astro/routes/api/setup/status.mjs +3 -3
  153. package/dist/astro/routes/api/snapshot.mjs +3 -3
  154. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
  155. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
  156. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
  157. package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
  158. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  159. package/dist/astro/routes/api/typegen.mjs +5 -5
  160. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  161. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
  162. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
  163. package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -6
  164. package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
  165. package/dist/astro/routes/api/widget-components.mjs +2 -2
  166. package/dist/astro/routes/robots.txt.mjs +5 -5
  167. package/dist/astro/routes/sitemap-_collection_.xml.mjs +5 -5
  168. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  169. package/dist/{authorize-_wWM_44T.mjs → authorize-CotM4Yiu.mjs} +2 -2
  170. package/dist/{authorize-_wWM_44T.mjs.map → authorize-CotM4Yiu.mjs.map} +1 -1
  171. package/dist/{byline-BrIVWLm-.mjs → byline-CWQ9aSoz.mjs} +4 -4
  172. package/dist/{byline-BrIVWLm-.mjs.map → byline-CWQ9aSoz.mjs.map} +1 -1
  173. package/dist/{bylines-C_POWmGT.mjs → bylines-BJSva1Un.mjs} +4 -4
  174. package/dist/{bylines-C_POWmGT.mjs.map → bylines-BJSva1Un.mjs.map} +1 -1
  175. package/dist/{bylines-sqExMElV.mjs → bylines-LJMgENMI.mjs} +3 -3
  176. package/dist/{bylines-sqExMElV.mjs.map → bylines-LJMgENMI.mjs.map} +1 -1
  177. package/dist/{cache-wsDkA8ru.mjs → cache-lZL7SgVb.mjs} +2 -2
  178. package/dist/{cache-wsDkA8ru.mjs.map → cache-lZL7SgVb.mjs.map} +1 -1
  179. package/dist/{chunks-BAYkM-CF.mjs → chunks-BU-vP9Dh.mjs} +2 -2
  180. package/dist/{chunks-BAYkM-CF.mjs.map → chunks-BU-vP9Dh.mjs.map} +1 -1
  181. package/dist/cli/index.mjs +14 -14
  182. package/dist/{comment-Cd29aktf.mjs → comment-C4jVbCM8.mjs} +2 -2
  183. package/dist/{comment-Cd29aktf.mjs.map → comment-C4jVbCM8.mjs.map} +1 -1
  184. package/dist/{comments-B7ufhkxN.mjs → comments-BTAbC0Ek.mjs} +3 -3
  185. package/dist/{comments-B7ufhkxN.mjs.map → comments-BTAbC0Ek.mjs.map} +1 -1
  186. package/dist/{content-BbqKo3Kc.mjs → content-CyqOmOzm.mjs} +3 -3
  187. package/dist/{content-BbqKo3Kc.mjs.map → content-CyqOmOzm.mjs.map} +1 -1
  188. package/dist/{context-BsF1rhoI.mjs → context-DZ7bEh5-.mjs} +8 -8
  189. package/dist/{context-BsF1rhoI.mjs.map → context-DZ7bEh5-.mjs.map} +1 -1
  190. package/dist/{dashboard-BwIX9r-X.mjs → dashboard-B5WQpNTP.mjs} +4 -4
  191. package/dist/{dashboard-BwIX9r-X.mjs.map → dashboard-B5WQpNTP.mjs.map} +1 -1
  192. package/dist/db/index.mjs +2 -2
  193. package/dist/{dialect-helpers-BKCvISIQ.mjs → dialect-helpers-DRI5pyY3.mjs} +3 -3
  194. package/dist/dialect-helpers-DRI5pyY3.mjs.map +1 -0
  195. package/dist/{error-npZWBSb7.mjs → error-DJOsMVSt.mjs} +2 -2
  196. package/dist/{error-npZWBSb7.mjs.map → error-DJOsMVSt.mjs.map} +1 -1
  197. package/dist/{fts-manager-DmUAk-kQ.mjs → fts-manager-DR1ERA0c.mjs} +3 -3
  198. package/dist/{fts-manager-DmUAk-kQ.mjs.map → fts-manager-DR1ERA0c.mjs.map} +1 -1
  199. package/dist/index-CjKdMZ3U.d.mts.map +1 -1
  200. package/dist/index.mjs +35 -35
  201. package/dist/{load-DsoLq7ex.mjs → load-6ZrRhepW.mjs} +2 -2
  202. package/dist/{load-DsoLq7ex.mjs.map → load-6ZrRhepW.mjs.map} +1 -1
  203. package/dist/{loader-CJ6lWO0d.mjs → loader-Dyx8dhFV.mjs} +4 -4
  204. package/dist/{loader-CJ6lWO0d.mjs.map → loader-Dyx8dhFV.mjs.map} +1 -1
  205. package/dist/media/local-runtime.mjs +5 -5
  206. package/dist/{media-jk_HzzOl.mjs → media-C-oovGCG.mjs} +2 -2
  207. package/dist/{media-jk_HzzOl.mjs.map → media-C-oovGCG.mjs.map} +1 -1
  208. package/dist/{menus-CyMO6GBx.mjs → menus-BKkxXCmd.mjs} +30 -11
  209. package/dist/menus-BKkxXCmd.mjs.map +1 -0
  210. package/dist/{menus-B-5-3aon.mjs → menus-DugoYwTX.mjs} +2 -2
  211. package/dist/{menus-B-5-3aon.mjs.map → menus-DugoYwTX.mjs.map} +1 -1
  212. package/dist/{parse-4zO5Y2DL.mjs → parse-BBkFmLVr.mjs} +2 -2
  213. package/dist/{parse-4zO5Y2DL.mjs.map → parse-BBkFmLVr.mjs.map} +1 -1
  214. package/dist/{query-Bt52mHXp.mjs → query-Ctlq1aOk.mjs} +10 -10
  215. package/dist/{query-Bt52mHXp.mjs.map → query-Ctlq1aOk.mjs.map} +1 -1
  216. package/dist/{rate-limit-D6VQqBk_.mjs → rate-limit-CH6W6ikK.mjs} +2 -2
  217. package/dist/{rate-limit-D6VQqBk_.mjs.map → rate-limit-CH6W6ikK.mjs.map} +1 -1
  218. package/dist/{redirect-BZUJltlj.mjs → redirect-C6tJA7tk.mjs} +3 -3
  219. package/dist/{redirect-BZUJltlj.mjs.map → redirect-C6tJA7tk.mjs.map} +1 -1
  220. package/dist/{redirects-DnYuqsEf.mjs → redirects-CacE9eQa.mjs} +3 -3
  221. package/dist/{redirects-DnYuqsEf.mjs.map → redirects-CacE9eQa.mjs.map} +1 -1
  222. package/dist/{registry-Dn6gsx3L.mjs → registry-CIDxZbhh.mjs} +5 -5
  223. package/dist/{registry-Dn6gsx3L.mjs.map → registry-CIDxZbhh.mjs.map} +1 -1
  224. package/dist/runner-DM1yR5qd.d.mts.map +1 -1
  225. package/dist/{runner-eAgyIkeg.mjs → runner-pt6Wl-l-.mjs} +11 -6
  226. package/dist/runner-pt6Wl-l-.mjs.map +1 -0
  227. package/dist/runtime.mjs +3 -3
  228. package/dist/{schema--mYZX4D7.mjs → schema-B4tk0HAG.mjs} +4 -4
  229. package/dist/{schema--mYZX4D7.mjs.map → schema-B4tk0HAG.mjs.map} +1 -1
  230. package/dist/{search-C6U_NvZI.mjs → search-f-fNfwab.mjs} +4 -4
  231. package/dist/{search-C6U_NvZI.mjs.map → search-f-fNfwab.mjs.map} +1 -1
  232. package/dist/{sections-Ba-rJLKb.mjs → sections-biElLfT9.mjs} +3 -3
  233. package/dist/{sections-Ba-rJLKb.mjs.map → sections-biElLfT9.mjs.map} +1 -1
  234. package/dist/seed/index.mjs +14 -14
  235. package/dist/seo/index.mjs +1 -0
  236. package/dist/seo/index.mjs.map +1 -1
  237. package/dist/{seo-BTzb5ksq.mjs → seo-BR39kvTF.mjs} +2 -2
  238. package/dist/{seo-BTzb5ksq.mjs.map → seo-BR39kvTF.mjs.map} +1 -1
  239. package/dist/{service-Cn-kIfZn.mjs → service-BhR2acnc.mjs} +2 -2
  240. package/dist/{service-Cn-kIfZn.mjs.map → service-BhR2acnc.mjs.map} +1 -1
  241. package/dist/{settings-C65OSm41.mjs → settings-D_NJvjgN.mjs} +3 -3
  242. package/dist/{settings-C65OSm41.mjs.map → settings-D_NJvjgN.mjs.map} +1 -1
  243. package/dist/{settings-ChlQbwU0.mjs → settings-b5zW1R1T.mjs} +3 -3
  244. package/dist/{settings-ChlQbwU0.mjs.map → settings-b5zW1R1T.mjs.map} +1 -1
  245. package/dist/{taxonomies-ByLlXrv5.mjs → taxonomies-Crtzy4MT.mjs} +8 -7
  246. package/dist/taxonomies-Crtzy4MT.mjs.map +1 -0
  247. package/dist/{taxonomies-CbO6v7EE.mjs → taxonomies-Mhn9rjTQ.mjs} +4 -4
  248. package/dist/{taxonomies-CbO6v7EE.mjs.map → taxonomies-Mhn9rjTQ.mjs.map} +1 -1
  249. package/dist/{taxonomy-BBK-UAEo.mjs → taxonomy-DTZrIQpi.mjs} +3 -3
  250. package/dist/{taxonomy-BBK-UAEo.mjs.map → taxonomy-DTZrIQpi.mjs.map} +1 -1
  251. package/dist/{types-SF1DwGf2.mjs → types-K3MDsxpy.mjs} +2 -2
  252. package/dist/{types-SF1DwGf2.mjs.map → types-K3MDsxpy.mjs.map} +1 -1
  253. package/dist/{user-X4rtyO4Y.mjs → user-DzEUl5zA.mjs} +2 -2
  254. package/dist/{user-X4rtyO4Y.mjs.map → user-DzEUl5zA.mjs.map} +1 -1
  255. package/dist/{validate-DactmcJG.mjs → validate-JCXcsqiY.mjs} +2 -2
  256. package/dist/{validate-DactmcJG.mjs.map → validate-JCXcsqiY.mjs.map} +1 -1
  257. package/dist/{validation-BYA4i85b.mjs → validation-Bq-VyKJg.mjs} +6 -6
  258. package/dist/{validation-BYA4i85b.mjs.map → validation-Bq-VyKJg.mjs.map} +1 -1
  259. package/dist/version-CnS-Cr8A.mjs +7 -0
  260. package/dist/{version-CWbvq9LG.mjs.map → version-CnS-Cr8A.mjs.map} +1 -1
  261. package/dist/{widgets-DG-1jxnz.mjs → widgets-Bap1eS1X.mjs} +2 -2
  262. package/dist/{widgets-DG-1jxnz.mjs.map → widgets-Bap1eS1X.mjs.map} +1 -1
  263. package/dist/{zod-generator-BNAObjSt.mjs → zod-generator-BSDpkqSH.mjs} +4 -3
  264. package/dist/zod-generator-BSDpkqSH.mjs.map +1 -0
  265. package/package.json +7 -7
  266. package/src/astro/middleware/stream-end-metrics.ts +96 -0
  267. package/src/astro/middleware.ts +114 -40
  268. package/src/components/EmDashImage.astro +1 -0
  269. package/src/database/dialect-helpers.ts +8 -2
  270. package/src/database/migrations/019_i18n.ts +2 -2
  271. package/src/database/migrations/runner.ts +7 -2
  272. package/src/emdash-runtime.ts +177 -126
  273. package/src/menus/index.ts +27 -9
  274. package/src/plugins/hooks.ts +35 -6
  275. package/src/plugins/manager.ts +1 -0
  276. package/src/schema/zod-generator.ts +6 -2
  277. package/src/seo/index.ts +10 -1
  278. package/src/taxonomies/index.ts +12 -8
  279. package/src/utils/init-lock.ts +143 -0
  280. package/dist/dialect-helpers-BKCvISIQ.mjs.map +0 -1
  281. package/dist/menus-CyMO6GBx.mjs.map +0 -1
  282. package/dist/runner-eAgyIkeg.mjs.map +0 -1
  283. package/dist/taxonomies-ByLlXrv5.mjs.map +0 -1
  284. package/dist/version-CWbvq9LG.mjs +0 -7
  285. package/dist/zod-generator-BNAObjSt.mjs.map +0 -1
@@ -22,7 +22,7 @@ import { getAuthMode } from "./auth/mode.js";
22
22
  import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
23
23
  import { isSqlite } from "./database/dialect-helpers.js";
24
24
  import { kyselyLogOption } from "./database/instrumentation.js";
25
- import { runMigrations } from "./database/migrations/runner.js";
25
+ import { MIGRATION_RACE_WAIT_MS, runMigrations } from "./database/migrations/runner.js";
26
26
  import { RevisionRepository } from "./database/repositories/revision.js";
27
27
  import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
28
28
  import { validateIdentifier } from "./database/validate.js";
@@ -41,6 +41,7 @@ import type {
41
41
  } from "./plugins/types.js";
42
42
  import type { FieldType } from "./schema/types.js";
43
43
  import { hashString } from "./utils/hash.js";
44
+ import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
44
45
  import { COMMIT, VERSION } from "./version.js";
45
46
 
46
47
  const LEADING_SLASH_PATTERN = /^\//;
@@ -310,9 +311,38 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
310
311
  return { ...item };
311
312
  }
312
313
 
313
- // Module-level caches (persist across requests within worker)
314
- const dbCache = new Map<string, Kysely<Database>>();
315
- let dbInitPromise: Promise<Kysely<Database>> | null = null;
314
+ /**
315
+ * Db init lock reclaim deadline. Derived from the migration race wait so
316
+ * they can't drift apart: a healthy init can legitimately block for the
317
+ * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
318
+ * connect and migrator work, before it should be presumed dead. The outer
319
+ * runtime init lock (middleware.ts) must use a strictly larger deadline —
320
+ * it wraps create() → getDatabase() → this lock, and equal deadlines would
321
+ * let the outer reclaim while the inner is legitimately still working.
322
+ */
323
+ export const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;
324
+
325
+ /**
326
+ * Db cache + its init lock live on globalThis behind a Symbol: the bundler
327
+ * can duplicate this module across SSR chunks (same reasoning as
328
+ * request-cache.ts), and a duplicated cache/lock would mean concurrent
329
+ * independent db inits — and duplicate migrators — per isolate.
330
+ */
331
+ const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
332
+ interface DbHolder {
333
+ cache: Map<string, Kysely<Database>>;
334
+ lock: InitLock;
335
+ }
336
+ const globalSymbolStore = globalThis as Record<symbol, unknown>;
337
+ function getDbHolder(): DbHolder {
338
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below
339
+ let holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;
340
+ if (!holder) {
341
+ holder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };
342
+ globalSymbolStore[DB_HOLDER_KEY] = holder;
343
+ }
344
+ return holder;
345
+ }
316
346
  const storageCache = new Map<string, Storage>();
317
347
  const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
318
348
  /**
@@ -887,19 +917,45 @@ export class EmDashRuntime {
887
917
  // Initialize storage (sync)
888
918
  const storage = EmDashRuntime.getStorage(deps);
889
919
 
890
- // Fetch plugin states from database
920
+ // Fetch plugin states and site info concurrently — independent reads
921
+ // against different tables (_plugin_state vs options), so they share
922
+ // one round-trip window instead of paying two sequential ones. Each
923
+ // phase() wrapper still records that phase's own duration, and each
924
+ // body keeps its own non-fatal catch.
891
925
  let pluginStates: Map<string, string> = new Map();
892
- await phase("rt.plugins", "Plugin states", async () => {
893
- try {
894
- const states = await db
895
- .selectFrom("_plugin_state")
896
- .select(["plugin_id", "status"])
897
- .execute();
898
- pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
899
- } catch {
900
- // Plugin state table may not exist yet
901
- }
902
- });
926
+ let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
927
+ await Promise.all([
928
+ // Fetch plugin states from database
929
+ phase("rt.plugins", "Plugin states", async () => {
930
+ try {
931
+ const states = await db
932
+ .selectFrom("_plugin_state")
933
+ .select(["plugin_id", "status"])
934
+ .execute();
935
+ pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
936
+ } catch {
937
+ // Plugin state table may not exist yet
938
+ }
939
+ }),
940
+ // Load site info for plugin context extensions (1 batch query instead of 3)
941
+ phase("rt.site", "Site info options", async () => {
942
+ try {
943
+ const optionsRepo = new OptionsRepository(db);
944
+ const siteOpts = await optionsRepo.getMany<string>([
945
+ "emdash:site_title",
946
+ "emdash:site_url",
947
+ "emdash:locale",
948
+ ]);
949
+ siteInfo = {
950
+ siteName: siteOpts.get("emdash:site_title") ?? undefined,
951
+ siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
952
+ locale: siteOpts.get("emdash:locale") ?? undefined,
953
+ };
954
+ } catch {
955
+ // Options table may not exist yet (pre-setup)
956
+ }
957
+ }),
958
+ ]);
903
959
 
904
960
  // Build set of enabled plugins
905
961
  const enabledPlugins = new Set<string>();
@@ -910,26 +966,6 @@ export class EmDashRuntime {
910
966
  }
911
967
  }
912
968
 
913
- // Load site info for plugin context extensions (1 batch query instead of 3)
914
- let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
915
- await phase("rt.site", "Site info options", async () => {
916
- try {
917
- const optionsRepo = new OptionsRepository(db);
918
- const siteOpts = await optionsRepo.getMany<string>([
919
- "emdash:site_title",
920
- "emdash:site_url",
921
- "emdash:locale",
922
- ]);
923
- siteInfo = {
924
- siteName: siteOpts.get("emdash:site_title") ?? undefined,
925
- siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
926
- locale: siteOpts.get("emdash:locale") ?? undefined,
927
- };
928
- } catch {
929
- // Options table may not exist yet (pre-setup)
930
- }
931
- });
932
-
933
969
  // Build the full list of pipeline-eligible plugins: all configured
934
970
  // plugins (regardless of current enabled status) plus built-in plugins.
935
971
  // rebuildHookPipeline() filters this to only enabled plugins.
@@ -1050,32 +1086,43 @@ export class EmDashRuntime {
1050
1086
  EmDashRuntime.loadSandboxedPlugins(deps, db, storage),
1051
1087
  );
1052
1088
 
1053
- // Cold-start: load marketplace-installed plugins from site R2 via
1054
- // the sandbox runner. In bypass mode this was already handled above.
1089
+ // Cold-start: load marketplace- and registry-installed plugins from
1090
+ // site R2 via the sandbox runner. The two tiers only depend on the
1091
+ // sandbox phase above, not on each other, so when both are enabled
1092
+ // they run concurrently instead of paying two sequential loads.
1093
+ // In bypass mode marketplace plugins were already handled above.
1094
+ const installedTierPhases: Promise<void>[] = [];
1055
1095
  if (deps.config.marketplace && storage && !deps.sandboxBypassed) {
1056
- await phase("rt.market", "Marketplace plugins", () =>
1057
- EmDashRuntime.loadInstalledSandboxedPlugins(
1058
- "marketplace",
1059
- db,
1060
- storage,
1061
- deps,
1062
- sandboxedPlugins,
1096
+ installedTierPhases.push(
1097
+ phase("rt.market", "Marketplace plugins", () =>
1098
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1099
+ "marketplace",
1100
+ db,
1101
+ storage,
1102
+ deps,
1103
+ sandboxedPlugins,
1104
+ ),
1063
1105
  ),
1064
1106
  );
1065
1107
  }
1066
1108
 
1067
1109
  // Cold-start: load registry-installed plugins from site R2
1068
1110
  if (deps.config.experimental?.registry && storage) {
1069
- await phase("rt.registry", "Registry plugins", () =>
1070
- EmDashRuntime.loadInstalledSandboxedPlugins(
1071
- "registry",
1072
- db,
1073
- storage,
1074
- deps,
1075
- sandboxedPlugins,
1111
+ installedTierPhases.push(
1112
+ phase("rt.registry", "Registry plugins", () =>
1113
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1114
+ "registry",
1115
+ db,
1116
+ storage,
1117
+ deps,
1118
+ sandboxedPlugins,
1119
+ ),
1076
1120
  ),
1077
1121
  );
1078
1122
  }
1123
+ if (installedTierPhases.length > 0) {
1124
+ await Promise.all(installedTierPhases);
1125
+ }
1079
1126
 
1080
1127
  // Initialize media providers
1081
1128
  const mediaProviders = new Map<string, MediaProvider>();
@@ -1270,83 +1317,86 @@ export class EmDashRuntime {
1270
1317
 
1271
1318
  const cacheKey = dbConfig.entrypoint;
1272
1319
 
1273
- // Return cached instance if available
1274
- const cached = dbCache.get(cacheKey);
1275
- if (cached) {
1276
- return cached;
1277
- }
1278
-
1279
- // Use initialization lock to prevent race conditions.
1280
- // Sharing this promise across requests is safe because the Kysely instance
1281
- // doesn't hold a request-scoped resource — the DO dialect uses a getStub()
1282
- // factory that creates a fresh stub per query execution.
1283
- if (dbInitPromise) {
1284
- return dbInitPromise;
1285
- }
1286
-
1287
- dbInitPromise = (async () => {
1288
- const dialect = deps.createDialect(dbConfig.config);
1289
- const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1290
-
1291
- await runMigrations(db);
1292
-
1293
- // Note: legacy installs may carry a stray `emdash:manifest_cache`
1294
- // row in the options table from versions that persisted a JSON
1295
- // manifest. The runtime no longer reads or writes it. We do not
1296
- // proactively delete it: the row is a few hundred bytes of dead
1297
- // weight and is never on the read path, whereas a one-shot
1298
- // cleanup-flag check costs an extra `options.get()` on every
1299
- // isolate cold boot forever. Cheaper to leave it.
1300
-
1301
- // Auto-seed schema if no collections exist and setup hasn't run.
1302
- // This covers first-load on sites that skip the setup wizard.
1303
- // Dev-bypass and the wizard apply seeds explicitly.
1304
- try {
1305
- const [collectionCount, setupOption] = await Promise.all([
1306
- db
1307
- .selectFrom("_emdash_collections")
1308
- .select((eb) => eb.fn.countAll<number>().as("count"))
1309
- .executeTakeFirstOrThrow(),
1310
- db
1311
- .selectFrom("options")
1312
- .select("value")
1313
- .where("name", "=", "emdash:setup_complete")
1314
- .executeTakeFirst(),
1315
- ]);
1316
-
1317
- const setupDone = (() => {
1318
- try {
1319
- return setupOption && JSON.parse(setupOption.value) === true;
1320
- } catch {
1321
- return false;
1322
- }
1323
- })();
1324
-
1325
- if (collectionCount.count === 0 && !setupDone) {
1326
- const { applySeed } = await import("./seed/apply.js");
1327
- const { loadSeed } = await import("./seed/load.js");
1328
- const { validateSeed } = await import("./seed/validate.js");
1329
-
1330
- const seed = await loadSeed();
1331
- const validation = validateSeed(seed);
1332
- if (validation.valid) {
1333
- await applySeed(db, seed, { onConflict: "skip" });
1334
- console.log("Auto-seeded default collections");
1320
+ // Waiters poll the cache rather than sharing the initializing request's
1321
+ // promise: if the request that owns the init is cancelled mid-await
1322
+ // (e.g. client disconnect during cold migrations), a shared promise
1323
+ // never settles — and the owner's `finally` that would clear it never
1324
+ // runs — deadlocking every later request in the isolate. Prevention:
1325
+ // the in-flight init is anchored via after()/waitUntil so a cancelled
1326
+ // owner's init still completes and populates the cache. Net: a stale
1327
+ // lock is reclaimed after a deadline.
1328
+ const holder = getDbHolder();
1329
+ return initWithLock(
1330
+ holder.lock,
1331
+ () => holder.cache.get(cacheKey),
1332
+ async (isCurrentClaim) => {
1333
+ const dialect = deps.createDialect(dbConfig.config);
1334
+ const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1335
+
1336
+ await runMigrations(db);
1337
+
1338
+ // Note: legacy installs may carry a stray `emdash:manifest_cache`
1339
+ // row in the options table from versions that persisted a JSON
1340
+ // manifest. The runtime no longer reads or writes it. We do not
1341
+ // proactively delete it: the row is a few hundred bytes of dead
1342
+ // weight and is never on the read path, whereas a one-shot
1343
+ // cleanup-flag check costs an extra `options.get()` on every
1344
+ // isolate cold boot forever. Cheaper to leave it.
1345
+
1346
+ // Auto-seed schema if no collections exist and setup hasn't run.
1347
+ // This covers first-load on sites that skip the setup wizard.
1348
+ // Dev-bypass and the wizard apply seeds explicitly.
1349
+ try {
1350
+ const [collectionCount, setupOption] = await Promise.all([
1351
+ db
1352
+ .selectFrom("_emdash_collections")
1353
+ .select((eb) => eb.fn.countAll<number>().as("count"))
1354
+ .executeTakeFirstOrThrow(),
1355
+ db
1356
+ .selectFrom("options")
1357
+ .select("value")
1358
+ .where("name", "=", "emdash:setup_complete")
1359
+ .executeTakeFirst(),
1360
+ ]);
1361
+
1362
+ const setupDone = (() => {
1363
+ try {
1364
+ return setupOption && JSON.parse(setupOption.value) === true;
1365
+ } catch {
1366
+ return false;
1367
+ }
1368
+ })();
1369
+
1370
+ if (collectionCount.count === 0 && !setupDone) {
1371
+ const { applySeed } = await import("./seed/apply.js");
1372
+ const { loadSeed } = await import("./seed/load.js");
1373
+ const { validateSeed } = await import("./seed/validate.js");
1374
+
1375
+ const seed = await loadSeed();
1376
+ const validation = validateSeed(seed);
1377
+ if (validation.valid) {
1378
+ await applySeed(db, seed, { onConflict: "skip" });
1379
+ console.log("Auto-seeded default collections");
1380
+ }
1335
1381
  }
1382
+ } catch {
1383
+ // Tables may not exist yet. Non-fatal.
1336
1384
  }
1337
- } catch {
1338
- // Tables may not exist yet. Non-fatal.
1339
- }
1340
1385
 
1341
- dbCache.set(cacheKey, db);
1342
- return db;
1343
- })();
1344
-
1345
- try {
1346
- return await dbInitPromise;
1347
- } finally {
1348
- dbInitPromise = null;
1349
- }
1386
+ // Publish only while still the current owner: a reclaimed slow
1387
+ // init must not flip the cached Kysely identity back after the
1388
+ // reclaimer has published its own. The unpublished instance is
1389
+ // still returned and fully valid for the request that built it.
1390
+ if (isCurrentClaim()) {
1391
+ holder.cache.set(cacheKey, db);
1392
+ }
1393
+ return db;
1394
+ },
1395
+ {
1396
+ deadlineMs: DB_INIT_DEADLINE_MS,
1397
+ anchor: (promise) => after(() => promise),
1398
+ },
1399
+ );
1350
1400
  }
1351
1401
 
1352
1402
  /**
@@ -1778,6 +1828,7 @@ export class EmDashRuntime {
1778
1828
  pipeline,
1779
1829
  isActive: () => true,
1780
1830
  getOption: (key) => optionsRepo.get<string>(key),
1831
+ getOptions: (keys) => optionsRepo.getMany<string>(keys),
1781
1832
  setOption: (key, value) => optionsRepo.set(key, value),
1782
1833
  deleteOption: async (key) => {
1783
1834
  await optionsRepo.delete(key);
@@ -136,15 +136,10 @@ async function buildMenuTree(
136
136
  }
137
137
  }
138
138
 
139
- const urlPatterns = new Map<string, string | null>();
140
- if (collectionSlugs.size > 0) {
141
- const rows = await db
142
- .selectFrom("_emdash_collections")
143
- .select(["slug", "url_pattern"])
144
- .where("slug", "in", [...collectionSlugs])
145
- .execute();
146
- for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
147
- }
139
+ const urlPatterns =
140
+ collectionSlugs.size > 0
141
+ ? await getCollectionUrlPatterns(db, collectionSlugs)
142
+ : new Map<string, string | null>();
148
143
 
149
144
  const resolvedItems = await Promise.all(
150
145
  items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
@@ -173,6 +168,29 @@ async function buildMenuTree(
173
168
  return rootItems;
174
169
  }
175
170
 
171
+ /**
172
+ * Look up the `url_pattern` for a set of collection slugs, request-cached so
173
+ * a page rendering several menus (header, footer, ...) only pays for the
174
+ * lookup once per distinct slug set. Callers must treat the returned map as
175
+ * read-only — it is shared across cache hits within the request.
176
+ */
177
+ function getCollectionUrlPatterns(
178
+ db: Kysely<Database>,
179
+ collectionSlugs: Set<string>,
180
+ ): Promise<Map<string, string | null>> {
181
+ const key = `menu-collection-patterns:${[...collectionSlugs].toSorted().join(",")}`;
182
+ return requestCached(key, async () => {
183
+ const rows = await db
184
+ .selectFrom("_emdash_collections")
185
+ .select(["slug", "url_pattern"])
186
+ .where("slug", "in", [...collectionSlugs])
187
+ .execute();
188
+ const urlPatterns = new Map<string, string | null>();
189
+ for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
190
+ return urlPatterns;
191
+ });
192
+ }
193
+
176
194
  /**
177
195
  * Resolve a single menu item's URL. `reference_id` is a translation_group
178
196
  * (migration 036 remapped all existing references); we join it against
@@ -1297,6 +1297,13 @@ export interface ExclusiveHookResolutionOptions {
1297
1297
  isActive: (pluginId: string) => boolean;
1298
1298
  /** Read an option value from persistent storage. */
1299
1299
  getOption: (key: string) => Promise<string | null>;
1300
+ /**
1301
+ * Batch-read option values for many keys in a single round trip.
1302
+ * When provided, resolution reads all current selections through this
1303
+ * instead of one getOption() call per hook. Keys absent from the
1304
+ * returned map are treated as unset.
1305
+ */
1306
+ getOptions?: (keys: string[]) => Promise<ReadonlyMap<string, string>>;
1300
1307
  /** Write an option value to persistent storage. */
1301
1308
  setOption: (key: string, value: string) => Promise<void>;
1302
1309
  /** Delete an option from persistent storage. */
@@ -1322,8 +1329,26 @@ const EXCLUSIVE_HOOK_KEY_PREFIX = "emdash:exclusive_hook:";
1322
1329
  * 5. If multiple providers and no hint → leave unselected (admin must choose).
1323
1330
  */
1324
1331
  export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions): Promise<void> {
1325
- const { pipeline, isActive, getOption, setOption, deleteOption, preferredHints } = opts;
1332
+ const { pipeline, isActive, getOption, getOptions, setOption, deleteOption, preferredHints } =
1333
+ opts;
1326
1334
  const exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();
1335
+ if (exclusiveHookNames.length === 0) return;
1336
+
1337
+ // Batch-read current selections in one round trip when the caller
1338
+ // provides a batch reader (1 query instead of N sequential gets).
1339
+ let batchedSelections: ReadonlyMap<string, string> | undefined;
1340
+ if (getOptions) {
1341
+ try {
1342
+ batchedSelections = await getOptions(
1343
+ exclusiveHookNames.map((hookName) => `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`),
1344
+ );
1345
+ } catch {
1346
+ // Options table may not be ready. Matches the per-key tolerance
1347
+ // below: every hook's read would fail, so resolution is skipped
1348
+ // entirely without touching any selection.
1349
+ return;
1350
+ }
1351
+ }
1327
1352
 
1328
1353
  for (const hookName of exclusiveHookNames) {
1329
1354
  const providers = pipeline.getExclusiveHookProviders(hookName);
@@ -1333,11 +1358,15 @@ export async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions
1333
1358
 
1334
1359
  const key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;
1335
1360
  let currentSelection: string | null = null;
1336
- try {
1337
- currentSelection = await getOption(key);
1338
- } catch {
1339
- // Options table may not be ready
1340
- continue;
1361
+ if (batchedSelections) {
1362
+ currentSelection = batchedSelections.get(key) ?? null;
1363
+ } else {
1364
+ try {
1365
+ currentSelection = await getOption(key);
1366
+ } catch {
1367
+ // Options table may not be ready
1368
+ continue;
1369
+ }
1341
1370
  }
1342
1371
 
1343
1372
  // If selection exists and the plugin is still active → keep it
@@ -544,6 +544,7 @@ export class PluginManager {
544
544
  pipeline: this.hookPipeline!,
545
545
  isActive: (pluginId) => this.isActive(pluginId),
546
546
  getOption: (key) => optionsRepo.get<string>(key),
547
+ getOptions: (keys) => optionsRepo.getMany<string>(keys),
547
548
  setOption: (key, value) => optionsRepo.set(key, value),
548
549
  deleteOption: async (key) => {
549
550
  await optionsRepo.delete(key);
@@ -288,6 +288,9 @@ export function generateTypeScript(collection: CollectionWithFields): string {
288
288
  lines.push(` publishedAt: Date | null;`);
289
289
  // Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry
290
290
  lines.push(` bylines?: ContentBylineCredit[];`);
291
+ // Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,
292
+ // keyed by taxonomy name (e.g. data.terms?.tag)
293
+ lines.push(` terms?: Record<string, TaxonomyTerm[]>;`);
291
294
  lines.push(`}`);
292
295
 
293
296
  return lines.join("\n");
@@ -312,8 +315,9 @@ export function generateTypesFile(collections: CollectionWithFields[]): string {
312
315
  c.fields.some((f) => f.type === "portableText"),
313
316
  );
314
317
 
315
- // Build imports - ContentBylineCredit is always needed for bylines
316
- const imports = ["ContentBylineCredit"];
318
+ // Build imports - ContentBylineCredit and TaxonomyTerm are always needed
319
+ // for the hydrated bylines/terms fields
320
+ const imports = ["ContentBylineCredit", "TaxonomyTerm"];
317
321
  if (needsPortableText) {
318
322
  imports.push("PortableTextBlock");
319
323
  }
package/src/seo/index.ts CHANGED
@@ -170,7 +170,16 @@ function buildMediaUrl(imageRef: string, siteUrl?: string): string {
170
170
  return imageRef;
171
171
  }
172
172
 
173
- // Build from media API path
173
+ // Root-relative path the CMS SEO panel stores seo_image as
174
+ // "/_emdash/api/media/file/01KS....svg" (already includes the API
175
+ // prefix). Without this branch we'd re-prefix and produce
176
+ // "${siteUrl}/_emdash/api/media/file//_emdash/api/media/file/<id>"
177
+ // which 404s and breaks <meta property="og:image">.
178
+ if (imageRef.startsWith("/")) {
179
+ return siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, "")}${imageRef}` : imageRef;
180
+ }
181
+
182
+ // Bare media_id — build the full media API path
174
183
  const mediaPath = `/_emdash/api/media/file/${imageRef}`;
175
184
  if (siteUrl) {
176
185
  return `${siteUrl.replace(TRAILING_SLASH_RE, "")}${mediaPath}`;
@@ -189,13 +189,6 @@ export async function getTerm(
189
189
 
190
190
  if (!row) return null;
191
191
 
192
- const countResult = await db
193
- .selectFrom("content_taxonomies")
194
- .select((eb) => eb.fn.count<number>("entry_id").as("count"))
195
- .where("taxonomy_id", "=", row.translation_group ?? row.id)
196
- .executeTakeFirst();
197
- const count = countResult?.count ?? 0;
198
-
199
192
  let childrenQuery = db
200
193
  .selectFrom("taxonomies")
201
194
  .selectAll()
@@ -203,7 +196,18 @@ export async function getTerm(
203
196
  .orderBy("label", "asc");
204
197
  const termLocale = row.locale;
205
198
  if (termLocale) childrenQuery = childrenQuery.where("locale", "=", termLocale);
206
- const childRows = await childrenQuery.execute();
199
+
200
+ // The usage-count and children queries both depend only on the term row,
201
+ // so run them concurrently to save a round trip on remote databases.
202
+ const [countResult, childRows] = await Promise.all([
203
+ db
204
+ .selectFrom("content_taxonomies")
205
+ .select((eb) => eb.fn.count<number>("entry_id").as("count"))
206
+ .where("taxonomy_id", "=", row.translation_group ?? row.id)
207
+ .executeTakeFirst(),
208
+ childrenQuery.execute(),
209
+ ]);
210
+ const count = countResult?.count ?? 0;
207
211
 
208
212
  const children = childRows.map<TaxonomyTerm>((child) => ({
209
213
  id: child.id,