emdash 0.24.1 → 0.26.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 (422) hide show
  1. package/dist/api/route-utils.d.mts +3 -3
  2. package/dist/api/route-utils.mjs +12 -11
  3. package/dist/api/route-utils.mjs.map +1 -1
  4. package/dist/api/schemas/index.d.mts +1 -1
  5. package/dist/api/schemas/index.mjs +2 -2
  6. package/dist/{api-d_0wi9fW.mjs → api-BMfveMoB.mjs} +24 -20
  7. package/dist/api-BMfveMoB.mjs.map +1 -0
  8. package/dist/{apply-BE9UaWX0.mjs → apply--IVNSdXn.mjs} +19 -16
  9. package/dist/apply--IVNSdXn.mjs.map +1 -0
  10. package/dist/astro/index.d.mts +9 -9
  11. package/dist/astro/index.mjs +3 -2
  12. package/dist/astro/index.mjs.map +1 -1
  13. package/dist/astro/middleware/auth.d.mts +8 -8
  14. package/dist/astro/middleware/auth.mjs +2 -2
  15. package/dist/astro/middleware/redirect.mjs +4 -4
  16. package/dist/astro/middleware.d.mts +1 -1
  17. package/dist/astro/middleware.d.mts.map +1 -1
  18. package/dist/astro/middleware.mjs +99 -70
  19. package/dist/astro/middleware.mjs.map +1 -1
  20. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  21. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  22. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  23. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  24. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  25. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +6 -6
  26. package/dist/astro/routes/api/admin/byline-fields/index.mjs +6 -6
  27. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +6 -6
  28. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +12 -12
  29. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +12 -12
  30. package/dist/astro/routes/api/admin/bylines/index.mjs +12 -12
  31. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
  32. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  33. package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
  34. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  35. package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
  36. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  37. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  38. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  39. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  40. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +31 -31
  41. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +31 -31
  42. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +31 -31
  43. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +31 -31
  44. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +31 -31
  45. package/dist/astro/routes/api/admin/plugins/index.mjs +31 -31
  46. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  47. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +31 -31
  48. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +31 -31
  49. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +31 -31
  50. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +31 -31
  51. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +32 -32
  52. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +31 -31
  53. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +32 -32
  54. package/dist/astro/routes/api/admin/plugins/updates.mjs +31 -31
  55. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +31 -31
  56. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  57. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +31 -31
  58. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  59. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  60. package/dist/astro/routes/api/admin/users/_id_/index.mjs +5 -5
  61. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  62. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  63. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  64. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  65. package/dist/astro/routes/api/auth/invite/complete.mjs +5 -5
  66. package/dist/astro/routes/api/auth/invite/index.mjs +5 -5
  67. package/dist/astro/routes/api/auth/invite/register-options.mjs +5 -5
  68. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  69. package/dist/astro/routes/api/auth/magic-link/send.mjs +6 -6
  70. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  71. package/dist/astro/routes/api/auth/me.mjs +6 -6
  72. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  73. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  74. package/dist/astro/routes/api/auth/passkey/options.mjs +6 -6
  75. package/dist/astro/routes/api/auth/passkey/register/options.mjs +5 -5
  76. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +5 -5
  77. package/dist/astro/routes/api/auth/passkey/verify.mjs +5 -5
  78. package/dist/astro/routes/api/auth/signup/complete.mjs +5 -5
  79. package/dist/astro/routes/api/auth/signup/request.mjs +6 -6
  80. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  81. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +9 -9
  82. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +9 -9
  83. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  84. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  86. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  87. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +6 -6
  88. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +6 -6
  89. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  90. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  91. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +6 -6
  92. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +11 -11
  93. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  94. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  95. package/dist/astro/routes/api/content/_collection_/_id_.mjs +6 -6
  96. package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
  97. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  98. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  99. package/dist/astro/routes/api/dashboard.mjs +8 -8
  100. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  101. package/dist/astro/routes/api/import/probe.d.mts +2 -2
  102. package/dist/astro/routes/api/import/probe.mjs +6 -6
  103. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  104. package/dist/astro/routes/api/import/wordpress/execute.d.mts +8 -8
  105. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
  106. package/dist/astro/routes/api/import/wordpress/media.d.mts +13 -1
  107. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  108. package/dist/astro/routes/api/import/wordpress/media.mjs +13 -9
  109. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  110. package/dist/astro/routes/api/import/wordpress/prepare.mjs +8 -8
  111. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
  112. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +6 -6
  113. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +9 -9
  114. package/dist/astro/routes/api/manifest.mjs +3 -3
  115. package/dist/astro/routes/api/mcp.mjs +36 -36
  116. package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
  117. package/dist/astro/routes/api/media/_id_/confirm.mjs +44 -8
  118. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  119. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  120. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  121. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  122. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  123. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  124. package/dist/astro/routes/api/media/upload-url.mjs +8 -8
  125. package/dist/astro/routes/api/media.mjs +22 -21
  126. package/dist/astro/routes/api/media.mjs.map +1 -1
  127. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
  128. package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
  129. package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
  130. package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
  131. package/dist/astro/routes/api/menus/_name_.mjs +8 -8
  132. package/dist/astro/routes/api/menus/index.mjs +8 -8
  133. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  134. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  135. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  136. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  137. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  138. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  139. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  140. package/dist/astro/routes/api/openapi.json.mjs +3 -3
  141. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  142. package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
  143. package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
  144. package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
  145. package/dist/astro/routes/api/redirects/index.mjs +9 -9
  146. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  147. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  148. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +31 -31
  149. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +31 -31
  150. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +31 -31
  151. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +31 -31
  152. package/dist/astro/routes/api/schema/collections/index.mjs +31 -31
  153. package/dist/astro/routes/api/schema/index.mjs +6 -6
  154. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +31 -31
  155. package/dist/astro/routes/api/schema/orphans/index.mjs +31 -31
  156. package/dist/astro/routes/api/search/enable.mjs +9 -9
  157. package/dist/astro/routes/api/search/index.mjs +8 -8
  158. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  159. package/dist/astro/routes/api/search/stats.mjs +6 -6
  160. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  161. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  162. package/dist/astro/routes/api/sections/index.mjs +8 -8
  163. package/dist/astro/routes/api/settings/email.mjs +4 -4
  164. package/dist/astro/routes/api/settings.mjs +11 -11
  165. package/dist/astro/routes/api/setup/admin-verify.mjs +5 -5
  166. package/dist/astro/routes/api/setup/admin.mjs +5 -5
  167. package/dist/astro/routes/api/setup/dev-bypass.mjs +17 -17
  168. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  169. package/dist/astro/routes/api/setup/index.mjs +20 -20
  170. package/dist/astro/routes/api/setup/status.mjs +3 -3
  171. package/dist/astro/routes/api/snapshot.mjs +3 -3
  172. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -11
  173. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -11
  174. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -11
  175. package/dist/astro/routes/api/taxonomies/index.mjs +11 -11
  176. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  177. package/dist/astro/routes/api/typegen.mjs +5 -5
  178. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  179. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  180. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  181. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  182. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  183. package/dist/astro/routes/api/widget-components.mjs +2 -2
  184. package/dist/astro/routes/robots.txt.mjs +5 -5
  185. package/dist/astro/routes/sitemap-_collection_.xml.mjs +11 -10
  186. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  187. package/dist/astro/routes/sitemap.xml.mjs +6 -6
  188. package/dist/astro/types.d.mts +10 -10
  189. package/dist/{authorize-B__u-iZJ.mjs → authorize-BZnOGJFo.mjs} +2 -2
  190. package/dist/{authorize-B__u-iZJ.mjs.map → authorize-BZnOGJFo.mjs.map} +1 -1
  191. package/dist/{byline-C-YajfV7.mjs → byline-LL8lyqdb.mjs} +6 -6
  192. package/dist/{byline-C-YajfV7.mjs.map → byline-LL8lyqdb.mjs.map} +1 -1
  193. package/dist/{byline-fields-Ca56gaT6.d.mts → byline-fields-KD3JFRuH.d.mts} +24 -6
  194. package/dist/byline-fields-KD3JFRuH.d.mts.map +1 -0
  195. package/dist/{byline-fields-B-KXkg5R.mjs → byline-fields-eyJMblSr.mjs} +2 -2
  196. package/dist/{byline-fields-B-KXkg5R.mjs.map → byline-fields-eyJMblSr.mjs.map} +1 -1
  197. package/dist/{bylines-D4KADDAi.mjs → bylines-BmJ7Vg7J.mjs} +4 -4
  198. package/dist/{bylines-D4KADDAi.mjs.map → bylines-BmJ7Vg7J.mjs.map} +1 -1
  199. package/dist/{bylines-BMtJnPnG.mjs → bylines-DE47vP8d.mjs} +6 -6
  200. package/dist/{bylines-BMtJnPnG.mjs.map → bylines-DE47vP8d.mjs.map} +1 -1
  201. package/dist/{cache-BUJe2w6Z.mjs → cache-D9jF5bYw.mjs} +2 -2
  202. package/dist/{cache-BUJe2w6Z.mjs.map → cache-D9jF5bYw.mjs.map} +1 -1
  203. package/dist/{chunks-BHgcHh2C.mjs → chunks-OCG163rN.mjs} +2 -2
  204. package/dist/{chunks-BHgcHh2C.mjs.map → chunks-OCG163rN.mjs.map} +1 -1
  205. package/dist/cli/index.mjs +17 -17
  206. package/dist/{comment-BDx50EX5.mjs → comment-D9BLDpZI.mjs} +3 -3
  207. package/dist/{comment-BDx50EX5.mjs.map → comment-D9BLDpZI.mjs.map} +1 -1
  208. package/dist/{comment-reaction-Gv_pgfI3.mjs → comment-reaction-Drl9SkNu.mjs} +2 -2
  209. package/dist/{comment-reaction-Gv_pgfI3.mjs.map → comment-reaction-Drl9SkNu.mjs.map} +1 -1
  210. package/dist/{comments-CXHuQ6jI.mjs → comments-BjJ9SKGU.mjs} +3 -3
  211. package/dist/{comments-CXHuQ6jI.mjs.map → comments-BjJ9SKGU.mjs.map} +1 -1
  212. package/dist/{content-PoVGKjp0.mjs → content-ncUWWjna.mjs} +10 -8
  213. package/dist/content-ncUWWjna.mjs.map +1 -0
  214. package/dist/{context-Dvws41_A.mjs → context-C_g-iJVA.mjs} +14 -8
  215. package/dist/context-C_g-iJVA.mjs.map +1 -0
  216. package/dist/{dashboard-DsYU8JKS.mjs → dashboard-CotyqQrW.mjs} +4 -4
  217. package/dist/{dashboard-DsYU8JKS.mjs.map → dashboard-CotyqQrW.mjs.map} +1 -1
  218. package/dist/db/index.d.mts +2 -2
  219. package/dist/db/index.mjs +1 -1
  220. package/dist/{db-errors-UXB1CK42.mjs → db-errors-CK46D-ly.mjs} +1 -1
  221. package/dist/{db-errors-UXB1CK42.mjs.map → db-errors-CK46D-ly.mjs.map} +1 -1
  222. package/dist/{placeholder-B0s2mWu5.mjs → enrich-BQ0mxJRs.mjs} +61 -10
  223. package/dist/enrich-BQ0mxJRs.mjs.map +1 -0
  224. package/dist/{error-lq_fL-RS.mjs → error-rVGiqWDt.mjs} +3 -2
  225. package/dist/error-rVGiqWDt.mjs.map +1 -0
  226. package/dist/{field-defs-cache-CYYx8xlG.mjs → field-defs-cache-ljXzJCfc.mjs} +2 -2
  227. package/dist/{field-defs-cache-CYYx8xlG.mjs.map → field-defs-cache-ljXzJCfc.mjs.map} +1 -1
  228. package/dist/{fts-manager-DW_NR-Fs.mjs → fts-manager-BbCCbEKA.mjs} +2 -2
  229. package/dist/{fts-manager-DW_NR-Fs.mjs.map → fts-manager-BbCCbEKA.mjs.map} +1 -1
  230. package/dist/{index-BroXT-vD.d.mts → index-BRNabKrx.d.mts} +53 -15
  231. package/dist/index-BRNabKrx.d.mts.map +1 -0
  232. package/dist/{index-qbL02uhs.d.mts → index-Dt0OKJB4.d.mts} +2 -2
  233. package/dist/{index-qbL02uhs.d.mts.map → index-Dt0OKJB4.d.mts.map} +1 -1
  234. package/dist/index.d.mts +12 -12
  235. package/dist/index.mjs +43 -43
  236. package/dist/{load-DMFi4n1p.mjs → load-DMjXghbZ.mjs} +2 -2
  237. package/dist/{load-DMFi4n1p.mjs.map → load-DMjXghbZ.mjs.map} +1 -1
  238. package/dist/{loader-BR2SgfEc.mjs → loader-BA_zhkS_.mjs} +82 -19
  239. package/dist/loader-BA_zhkS_.mjs.map +1 -0
  240. package/dist/{manifest-schema-BWKnQYQF.mjs → manifest-schema-kPGX7VS-.mjs} +4 -1
  241. package/dist/manifest-schema-kPGX7VS-.mjs.map +1 -0
  242. package/dist/media/image-endpoint.mjs +1 -1
  243. package/dist/media/index.d.mts +45 -2
  244. package/dist/media/index.d.mts.map +1 -1
  245. package/dist/media/index.mjs +5 -3
  246. package/dist/media/index.mjs.map +1 -1
  247. package/dist/media/local-runtime.d.mts +9 -9
  248. package/dist/media/local-runtime.d.mts.map +1 -1
  249. package/dist/media/local-runtime.mjs +17 -5
  250. package/dist/media/local-runtime.mjs.map +1 -1
  251. package/dist/{media-CTjJoJTh.mjs → media-C5t_eCnk.mjs} +4 -2
  252. package/dist/media-C5t_eCnk.mjs.map +1 -0
  253. package/dist/{media-allowlist-B77I5_bw.mjs → media-allowlist-Du8t1u6a.mjs} +2 -2
  254. package/dist/{media-allowlist-B77I5_bw.mjs.map → media-allowlist-Du8t1u6a.mjs.map} +1 -1
  255. package/dist/{menus-8O6xNcYW.mjs → menus-CBb6UrMU.mjs} +3 -3
  256. package/dist/{menus-8O6xNcYW.mjs.map → menus-CBb6UrMU.mjs.map} +1 -1
  257. package/dist/{menus-GvRSpgIA.mjs → menus-CYH09a41.mjs} +65 -41
  258. package/dist/menus-CYH09a41.mjs.map +1 -0
  259. package/dist/{mime-BTzvzJ8M.mjs → mime-CCEzze7W.mjs} +1 -1
  260. package/dist/{mime-BTzvzJ8M.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
  261. package/dist/{normalize-ClbNIenm.mjs → normalize-CeglrBT9.mjs} +11 -2
  262. package/dist/normalize-CeglrBT9.mjs.map +1 -0
  263. package/dist/{object-cache-SEb2IM4Z.mjs → object-cache-CHbHv83-.mjs} +8 -4
  264. package/dist/object-cache-CHbHv83-.mjs.map +1 -0
  265. package/dist/{options-CqECG0gM.d.mts → options-41nCWqi9.d.mts} +3 -3
  266. package/dist/{options-CqECG0gM.d.mts.map → options-41nCWqi9.d.mts.map} +1 -1
  267. package/dist/page/index.d.mts +5 -2
  268. package/dist/page/index.d.mts.map +1 -1
  269. package/dist/page/index.mjs +3 -2
  270. package/dist/page/index.mjs.map +1 -1
  271. package/dist/{parse-DHI873qU.mjs → parse-BwAdS7FZ.mjs} +2 -2
  272. package/dist/{parse-DHI873qU.mjs.map → parse-BwAdS7FZ.mjs.map} +1 -1
  273. package/dist/{placeholder-QoI4gEzP.d.mts → placeholder-Cuce9U-m.d.mts} +20 -5
  274. package/dist/placeholder-Cuce9U-m.d.mts.map +1 -0
  275. package/dist/plugin-types.d.mts +5 -2
  276. package/dist/plugin-types.d.mts.map +1 -1
  277. package/dist/plugin-utils.d.mts +8 -8
  278. package/dist/plugins/adapt-sandbox-entry.d.mts +8 -8
  279. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  280. package/dist/{query-DZdlS6Cu.mjs → query-BpQb-blG.mjs} +15 -15
  281. package/dist/{query-DZdlS6Cu.mjs.map → query-BpQb-blG.mjs.map} +1 -1
  282. package/dist/{rate-limit-D1Y6VFVP.mjs → rate-limit-Dap6HMNM.mjs} +2 -2
  283. package/dist/{rate-limit-D1Y6VFVP.mjs.map → rate-limit-Dap6HMNM.mjs.map} +1 -1
  284. package/dist/{redirect-C1KVGYWe.mjs → redirect-_M0trWoo.mjs} +2 -2
  285. package/dist/{redirect-C1KVGYWe.mjs.map → redirect-_M0trWoo.mjs.map} +1 -1
  286. package/dist/{redirects-x9eFPeyi.mjs → redirects-Dcbuisoj.mjs} +2 -1
  287. package/dist/redirects-Dcbuisoj.mjs.map +1 -0
  288. package/dist/{redirects-DgnypVir.mjs → redirects-w0V9vj_j.mjs} +3 -3
  289. package/dist/{redirects-DgnypVir.mjs.map → redirects-w0V9vj_j.mjs.map} +1 -1
  290. package/dist/{registry-CJkCubOJ.mjs → registry-g8yYt4cC.mjs} +14 -4
  291. package/dist/registry-g8yYt4cC.mjs.map +1 -0
  292. package/dist/{runner-CEtq-6Sm.d.mts → runner-BbR3DfrL.d.mts} +2 -2
  293. package/dist/{runner-CEtq-6Sm.d.mts.map → runner-BbR3DfrL.d.mts.map} +1 -1
  294. package/dist/{runner-BsquExlK.mjs → runner-DPdt_Wpa.mjs} +250 -178
  295. package/dist/runner-DPdt_Wpa.mjs.map +1 -0
  296. package/dist/runtime.d.mts +9 -9
  297. package/dist/runtime.mjs +2 -2
  298. package/dist/{schema-DMjXuvNU.mjs → schema-DoLd3j2e.mjs} +5 -5
  299. package/dist/{schema-DMjXuvNU.mjs.map → schema-DoLd3j2e.mjs.map} +1 -1
  300. package/dist/{search-D1wKk3lc.mjs → search-BR0ZeZmh.mjs} +4 -4
  301. package/dist/{search-D1wKk3lc.mjs.map → search-BR0ZeZmh.mjs.map} +1 -1
  302. package/dist/{sections-CCMic8dB.mjs → sections-5J2YJPqJ.mjs} +3 -3
  303. package/dist/{sections-CCMic8dB.mjs.map → sections-5J2YJPqJ.mjs.map} +1 -1
  304. package/dist/seed/index.d.mts +2 -2
  305. package/dist/seed/index.mjs +15 -15
  306. package/dist/seo/index.d.mts +1 -1
  307. package/dist/{seo-DCEF-AVr.mjs → seo-BkhuuaaE.mjs} +25 -3
  308. package/dist/seo-BkhuuaaE.mjs.map +1 -0
  309. package/dist/{seo-3QrZ1EPT.mjs → seo-DKeXHkzE.mjs} +3 -3
  310. package/dist/{seo-3QrZ1EPT.mjs.map → seo-DKeXHkzE.mjs.map} +1 -1
  311. package/dist/{service-CQHK_10l.mjs → service-BbO2s8gT.mjs} +2 -2
  312. package/dist/{service-CQHK_10l.mjs.map → service-BbO2s8gT.mjs.map} +1 -1
  313. package/dist/{settings-C3Yi5Co3.mjs → settings-BMNr9Dmi.mjs} +3 -3
  314. package/dist/{settings-C3Yi5Co3.mjs.map → settings-BMNr9Dmi.mjs.map} +1 -1
  315. package/dist/{settings-D9rvaLuY.mjs → settings-DJeki-97.mjs} +4 -4
  316. package/dist/{settings-D9rvaLuY.mjs.map → settings-DJeki-97.mjs.map} +1 -1
  317. package/dist/{taxonomies-DhLaNJji.mjs → taxonomies-CM_fUqLU.mjs} +5 -4
  318. package/dist/taxonomies-CM_fUqLU.mjs.map +1 -0
  319. package/dist/{taxonomies-CurpLtNC.mjs → taxonomies-D13i2ctt.mjs} +76 -21
  320. package/dist/taxonomies-D13i2ctt.mjs.map +1 -0
  321. package/dist/{taxonomy-BBUlGug5.mjs → taxonomy-BU0LjdVb.mjs} +4 -4
  322. package/dist/{taxonomy-BBUlGug5.mjs.map → taxonomy-BU0LjdVb.mjs.map} +1 -1
  323. package/dist/{types-TTx8uP7V.d.mts → types-BYnDBVwe.d.mts} +27 -5
  324. package/dist/{types-TTx8uP7V.d.mts.map → types-BYnDBVwe.d.mts.map} +1 -1
  325. package/dist/{types-CTLJYMhN.d.mts → types-CNlaBFzx.d.mts} +11 -1
  326. package/dist/{types-CTLJYMhN.d.mts.map → types-CNlaBFzx.d.mts.map} +1 -1
  327. package/dist/{types-CmI1OyQg.mjs → types-CZgK3EMW.mjs} +2 -2
  328. package/dist/{types-CmI1OyQg.mjs.map → types-CZgK3EMW.mjs.map} +1 -1
  329. package/dist/{types-BWh2MjmA.d.mts → types-ETmO_jQr.d.mts} +2 -2
  330. package/dist/{types-BWh2MjmA.d.mts.map → types-ETmO_jQr.d.mts.map} +1 -1
  331. package/dist/types-tM44hEcf.mjs.map +1 -1
  332. package/dist/{types-B-gIxFCE.d.mts → types-xIfVRNLp.d.mts} +38 -1
  333. package/dist/types-xIfVRNLp.d.mts.map +1 -0
  334. package/dist/{user-JAuc8VUf.mjs → user-DkG2jKCp.mjs} +3 -3
  335. package/dist/{user-JAuc8VUf.mjs.map → user-DkG2jKCp.mjs.map} +1 -1
  336. package/dist/{validate-DOFrIb-c.d.mts → validate-CG0V0wNR.d.mts} +3 -3
  337. package/dist/{validate-DOFrIb-c.d.mts.map → validate-CG0V0wNR.d.mts.map} +1 -1
  338. package/dist/{validate-BzXHWnkV.mjs → validate-CuJ1Ps9j.mjs} +2 -2
  339. package/dist/{validate-BzXHWnkV.mjs.map → validate-CuJ1Ps9j.mjs.map} +1 -1
  340. package/dist/{validation-CY1gk4Ar.mjs → validation-CwUfTdGF.mjs} +6 -6
  341. package/dist/{validation-CY1gk4Ar.mjs.map → validation-CwUfTdGF.mjs.map} +1 -1
  342. package/dist/version-BoNBItP_.mjs +7 -0
  343. package/dist/{version-C0JPFUG8.mjs.map → version-BoNBItP_.mjs.map} +1 -1
  344. package/dist/{widgets-Dri0spI4.mjs → widgets-BfOZ-ajY.mjs} +2 -2
  345. package/dist/{widgets-Dri0spI4.mjs.map → widgets-BfOZ-ajY.mjs.map} +1 -1
  346. package/dist/{zod-generator-xEHODMJf.mjs → zod-generator-gFsRuNNy.mjs} +6 -3
  347. package/dist/zod-generator-gFsRuNNy.mjs.map +1 -0
  348. package/package.json +6 -6
  349. package/src/api/errors.ts +1 -0
  350. package/src/api/handlers/content.ts +4 -4
  351. package/src/api/handlers/seo.ts +30 -2
  352. package/src/api/handlers/taxonomies.ts +5 -1
  353. package/src/api/schemas/schema.ts +1 -0
  354. package/src/astro/integration/virtual-modules.ts +15 -0
  355. package/src/astro/middleware.ts +14 -1
  356. package/src/astro/routes/api/import/wordpress/media.ts +9 -3
  357. package/src/astro/routes/api/media/[id]/confirm.ts +62 -2
  358. package/src/astro/routes/api/media.ts +17 -19
  359. package/src/astro/routes/sitemap-[collection].xml.ts +5 -4
  360. package/src/components/EmDashHead.astro +23 -3
  361. package/src/components/EmDashImage.astro +4 -7
  362. package/src/components/Image.astro +14 -4
  363. package/src/components/InlinePortableTextEditor.tsx +48 -1
  364. package/src/components/JsonLdScript.astro +9 -0
  365. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +16 -0
  366. package/src/database/migrations/046_media_usage_index.ts +115 -0
  367. package/src/database/migrations/047_restore_taxonomy_parent_index.ts +29 -0
  368. package/src/database/migrations/runner.ts +4 -0
  369. package/src/database/repositories/content.ts +8 -5
  370. package/src/database/repositories/media-usage.ts +444 -0
  371. package/src/database/repositories/media.ts +9 -1
  372. package/src/database/types.ts +39 -0
  373. package/src/emdash-runtime.ts +72 -25
  374. package/src/index.ts +3 -0
  375. package/src/loader.ts +110 -31
  376. package/src/media/enrich.ts +78 -0
  377. package/src/media/index.ts +1 -0
  378. package/src/media/local-runtime.ts +19 -0
  379. package/src/media/normalize.ts +20 -1
  380. package/src/media/placeholder.ts +29 -9
  381. package/src/media/types.ts +14 -0
  382. package/src/media/usage/extractor.ts +302 -0
  383. package/src/media/usage/types.ts +46 -0
  384. package/src/object-cache/index.ts +8 -2
  385. package/src/page/metadata.ts +23 -3
  386. package/src/plugin-types.ts +12 -0
  387. package/src/plugins/context.ts +8 -0
  388. package/src/plugins/define-plugin.ts +14 -5
  389. package/src/plugins/hooks.ts +66 -33
  390. package/src/plugins/index.ts +6 -0
  391. package/src/plugins/manager.ts +33 -0
  392. package/src/plugins/manifest-schema.ts +3 -0
  393. package/src/plugins/types.ts +41 -2
  394. package/src/schema/registry.ts +25 -0
  395. package/src/schema/types.ts +10 -0
  396. package/src/schema/zod-generator.ts +9 -1
  397. package/src/seed/apply.ts +7 -0
  398. package/src/taxonomies/index.ts +121 -18
  399. package/dist/api-d_0wi9fW.mjs.map +0 -1
  400. package/dist/apply-BE9UaWX0.mjs.map +0 -1
  401. package/dist/byline-fields-Ca56gaT6.d.mts.map +0 -1
  402. package/dist/content-PoVGKjp0.mjs.map +0 -1
  403. package/dist/context-Dvws41_A.mjs.map +0 -1
  404. package/dist/error-lq_fL-RS.mjs.map +0 -1
  405. package/dist/index-BroXT-vD.d.mts.map +0 -1
  406. package/dist/loader-BR2SgfEc.mjs.map +0 -1
  407. package/dist/manifest-schema-BWKnQYQF.mjs.map +0 -1
  408. package/dist/media-CTjJoJTh.mjs.map +0 -1
  409. package/dist/menus-GvRSpgIA.mjs.map +0 -1
  410. package/dist/normalize-ClbNIenm.mjs.map +0 -1
  411. package/dist/object-cache-SEb2IM4Z.mjs.map +0 -1
  412. package/dist/placeholder-B0s2mWu5.mjs.map +0 -1
  413. package/dist/placeholder-QoI4gEzP.d.mts.map +0 -1
  414. package/dist/redirects-x9eFPeyi.mjs.map +0 -1
  415. package/dist/registry-CJkCubOJ.mjs.map +0 -1
  416. package/dist/runner-BsquExlK.mjs.map +0 -1
  417. package/dist/seo-DCEF-AVr.mjs.map +0 -1
  418. package/dist/taxonomies-CurpLtNC.mjs.map +0 -1
  419. package/dist/taxonomies-DhLaNJji.mjs.map +0 -1
  420. package/dist/types-B-gIxFCE.d.mts.map +0 -1
  421. package/dist/version-C0JPFUG8.mjs +0 -7
  422. package/dist/zod-generator-xEHODMJf.mjs.map +0 -1
@@ -60,6 +60,43 @@ export interface MediaTable {
60
60
  author_id: string | null;
61
61
  }
62
62
 
63
+ export interface MediaUsageSourceTable {
64
+ source_key: string;
65
+ source_type: string;
66
+ collection_slug: string | null;
67
+ content_id: string | null;
68
+ source_variant: string;
69
+ locale: string | null;
70
+ translation_group: string | null;
71
+ content_slug: string | null;
72
+ content_title: string | null;
73
+ content_status: string | null;
74
+ content_scheduled_at: string | null;
75
+ content_deleted_at: string | null;
76
+ revision_id: string | null;
77
+ current_generation: string;
78
+ schema_version: Generated<number>;
79
+ indexed_at: Generated<string>;
80
+ created_at: Generated<string>;
81
+ updated_at: Generated<string>;
82
+ }
83
+
84
+ export interface MediaUsageTable {
85
+ id: string;
86
+ source_key: string;
87
+ generation: string;
88
+ field_slug: string;
89
+ field_path: string;
90
+ occurrence_index: Generated<number>;
91
+ reference_type: string;
92
+ media_id: string | null;
93
+ provider: Generated<string>;
94
+ provider_asset_id: string;
95
+ media_kind: string | null;
96
+ mime_type: string | null;
97
+ created_at: Generated<string>;
98
+ }
99
+
63
100
  export interface UserTable {
64
101
  id: string;
65
102
  email: string;
@@ -414,6 +451,8 @@ export interface Database {
414
451
  content_taxonomies: ContentTaxonomyTable;
415
452
  _emdash_taxonomy_defs: TaxonomyDefTable;
416
453
  media: MediaTable;
454
+ _emdash_media_usage_sources: MediaUsageSourceTable;
455
+ _emdash_media_usage: MediaUsageTable;
417
456
  users: UserTable;
418
457
  credentials: CredentialTable;
419
458
  auth_tokens: AuthTokenTable;
@@ -2797,7 +2797,14 @@ export class EmDashRuntime {
2797
2797
  }
2798
2798
 
2799
2799
  async handleContentRestore(collection: string, id: string) {
2800
- return handleContentRestore(this.db, collection, id);
2800
+ const result = await handleContentRestore(this.db, collection, id);
2801
+
2802
+ // Run afterRestore hooks (fire-and-forget)
2803
+ if (result.success) {
2804
+ this.runAfterRestoreHooks(contentItemToRecord(result.data.item), collection);
2805
+ }
2806
+
2807
+ return result;
2801
2808
  }
2802
2809
 
2803
2810
  async handleContentPermanentDelete(collection: string, id: string) {
@@ -2850,11 +2857,25 @@ export class EmDashRuntime {
2850
2857
  }
2851
2858
 
2852
2859
  async handleContentSchedule(collection: string, id: string, scheduledAt: string) {
2853
- return handleContentSchedule(this.db, collection, id, scheduledAt);
2860
+ const result = await handleContentSchedule(this.db, collection, id, scheduledAt);
2861
+
2862
+ // Run afterSchedule hooks (fire-and-forget)
2863
+ if (result.success && result.data) {
2864
+ this.runAfterScheduleHooks(contentItemToRecord(result.data.item), collection);
2865
+ }
2866
+
2867
+ return result;
2854
2868
  }
2855
2869
 
2856
2870
  async handleContentUnschedule(collection: string, id: string) {
2857
- return handleContentUnschedule(this.db, collection, id);
2871
+ const result = await handleContentUnschedule(this.db, collection, id);
2872
+
2873
+ // Run afterUnschedule hooks (fire-and-forget)
2874
+ if (result.success && result.data) {
2875
+ this.runAfterUnscheduleHooks(contentItemToRecord(result.data.item), collection);
2876
+ }
2877
+
2878
+ return result;
2858
2879
  }
2859
2880
 
2860
2881
  async handleContentCountScheduled(collection: string) {
@@ -3313,14 +3334,41 @@ export class EmDashRuntime {
3313
3334
  }
3314
3335
  }
3315
3336
 
3316
- private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
3337
+ private runDeferredContentHook(
3338
+ name:
3339
+ | "content:afterPublish"
3340
+ | "content:afterUnpublish"
3341
+ | "content:afterRestore"
3342
+ | "content:afterSchedule"
3343
+ | "content:afterUnschedule",
3344
+ content: Record<string, unknown>,
3345
+ collection: string,
3346
+ ): void {
3347
+ const label = name.slice("content:".length);
3348
+
3317
3349
  after(async () => {
3318
3350
  // Trusted plugins
3319
- if (this.hooks.hasHooks("content:afterPublish")) {
3351
+ if (this.hooks.hasHooks(name)) {
3320
3352
  try {
3321
- await this.hooks.runContentAfterPublish(content, collection);
3353
+ switch (name) {
3354
+ case "content:afterPublish":
3355
+ await this.hooks.runContentAfterPublish(content, collection);
3356
+ break;
3357
+ case "content:afterUnpublish":
3358
+ await this.hooks.runContentAfterUnpublish(content, collection);
3359
+ break;
3360
+ case "content:afterRestore":
3361
+ await this.hooks.runContentAfterRestore(content, collection);
3362
+ break;
3363
+ case "content:afterSchedule":
3364
+ await this.hooks.runContentAfterSchedule(content, collection);
3365
+ break;
3366
+ case "content:afterUnschedule":
3367
+ await this.hooks.runContentAfterUnschedule(content, collection);
3368
+ break;
3369
+ }
3322
3370
  } catch (err) {
3323
- console.error("EmDash afterPublish hook error:", err);
3371
+ console.error(`EmDash ${label} hook error:`, err);
3324
3372
  }
3325
3373
  }
3326
3374
 
@@ -3333,9 +3381,9 @@ export class EmDashRuntime {
3333
3381
  tasks.push(
3334
3382
  (async () => {
3335
3383
  try {
3336
- await plugin.invokeHook("content:afterPublish", { content, collection });
3384
+ await plugin.invokeHook(name, { content, collection });
3337
3385
  } catch (err) {
3338
- console.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish error:`, err);
3386
+ console.error(`EmDash: Sandboxed plugin ${pluginId} ${label} error:`, err);
3339
3387
  }
3340
3388
  })(),
3341
3389
  );
@@ -3344,25 +3392,24 @@ export class EmDashRuntime {
3344
3392
  });
3345
3393
  }
3346
3394
 
3395
+ private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
3396
+ this.runDeferredContentHook("content:afterPublish", content, collection);
3397
+ }
3398
+
3347
3399
  private runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {
3348
- // Trusted plugins
3349
- if (this.hooks.hasHooks("content:afterUnpublish")) {
3350
- this.hooks
3351
- .runContentAfterUnpublish(content, collection)
3352
- .catch((err) => console.error("EmDash afterUnpublish hook error:", err));
3353
- }
3400
+ this.runDeferredContentHook("content:afterUnpublish", content, collection);
3401
+ }
3354
3402
 
3355
- // Sandboxed plugins
3356
- for (const [pluginKey, plugin] of this.sandboxedPlugins) {
3357
- const [pluginId] = pluginKey.split(":");
3358
- if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
3403
+ private runAfterRestoreHooks(content: Record<string, unknown>, collection: string): void {
3404
+ this.runDeferredContentHook("content:afterRestore", content, collection);
3405
+ }
3359
3406
 
3360
- plugin
3361
- .invokeHook("content:afterUnpublish", { content, collection })
3362
- .catch((err) =>
3363
- console.error(`EmDash: Sandboxed plugin ${pluginId} afterUnpublish error:`, err),
3364
- );
3365
- }
3407
+ private runAfterScheduleHooks(content: Record<string, unknown>, collection: string): void {
3408
+ this.runDeferredContentHook("content:afterSchedule", content, collection);
3409
+ }
3410
+
3411
+ private runAfterUnscheduleHooks(content: Record<string, unknown>, collection: string): void {
3412
+ this.runDeferredContentHook("content:afterUnschedule", content, collection);
3366
3413
  }
3367
3414
 
3368
3415
  private async handleSandboxedRoute(
package/src/index.ts CHANGED
@@ -255,6 +255,9 @@ export type {
255
255
  ContentHookEvent,
256
256
  ContentDeleteEvent,
257
257
  ContentPublishStateChangeEvent,
258
+ ContentRestoreStateChangeEvent,
259
+ ContentScheduleStateChangeEvent,
260
+ ContentStateChangeEvent,
258
261
  MediaUploadEvent,
259
262
  HookResult,
260
263
  PluginRoute,
package/src/loader.ts CHANGED
@@ -25,15 +25,23 @@ import { isMissingColumnError, isMissingTableError } from "./utils/db-errors.js"
25
25
  const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
26
26
 
27
27
  /**
28
- * SEO columns joined in from `_emdash_seo` on the single-entry path, mapped to
29
- * aliased result keys. SEO lives in a side table, so a LEFT JOIN folds it into
30
- * the entry load at zero extra query cost; the result is surfaced as a nested
31
- * `data.seo` object (see extractSeo) rather than flat fields.
28
+ * SEO columns folded into the single-entry query as a single JSON column
29
+ * (`_emdash_seo` in the result set), then expanded onto the row under these
30
+ * aliases for `extractSeo()`. Surfacing SEO as one aggregated column keeps the
31
+ * result-set width bounded regardless of how many fields the collection has,
32
+ * which matters for D1: a flat `LEFT JOIN _emdash_seo` adds 5 alias columns to
33
+ * every row and pushes wide collections (common after WordPress / ACF imports)
34
+ * past D1's per-result-set column limit, surfacing as a silent null entry.
35
+ * One JSON column is one column, so the join stays safe at any schema width.
36
+ *
37
+ * The aliases mirror the strategy used by `foldedHydrationSelects` for byline
38
+ * and taxonomy hydration: aggregate in SQL, expand in JS. SEO is 1:1 with
39
+ * content, so the subquery uses `json_object` (not the array aggregator).
32
40
  *
33
41
  * The `_emdash_` prefix on the aliases guarantees they can never collide with
34
42
  * a content field. Field slugs must match `/^[a-z][a-z0-9_]*$/`, so a user can
35
- * legitimately define a `seo_title` field; selecting the joined column under
36
- * its bare name would shadow that field in the result set and drop the user's
43
+ * legitimately define a `seo_title` field; surfacing the SEO column under its
44
+ * bare name would shadow that field in the result set and drop the user's
37
45
  * value. The prefix (illegal as a leading slug char) sidesteps this entirely.
38
46
  */
39
47
  const SEO_COLUMN_ALIASES: Record<string, string> = {
@@ -47,6 +55,9 @@ const SEO_COLUMN_ALIASES: Record<string, string> = {
47
55
  /** Aliased SEO result keys — excluded from generic field mapping. */
48
56
  const SEO_ALIAS_COLUMNS = Object.values(SEO_COLUMN_ALIASES);
49
57
 
58
+ /** Folded SEO JSON column name in the result set (expanded onto aliases in JS). */
59
+ const SEO_FOLDED_COLUMN = "_emdash_seo";
60
+
50
61
  /**
51
62
  * System columns excluded from entry.data
52
63
  * Note: slug is intentionally NOT excluded - it's useful as data.slug in templates
@@ -67,15 +78,17 @@ const SYSTEM_COLUMNS = new Set([
67
78
  "draft_revision_id",
68
79
  "locale",
69
80
  "translation_group",
70
- // Aliased SEO columns joined from _emdash_seo on the single-entry path.
71
- // Surfaced as a nested data.seo object (see extractSeo), never as flat
72
- // fields. The aliases are _emdash_-prefixed so they can't shadow a user
73
- // field named e.g. `seo_title`.
81
+ // Aliased SEO columns expanded from the folded _emdash_seo JSON column on
82
+ // the single-entry path. Surfaced as a nested data.seo object (see
83
+ // extractSeo), never as flat fields. The aliases are _emdash_-prefixed so
84
+ // they can't shadow a user field named e.g. `seo_title`.
74
85
  ...SEO_ALIAS_COLUMNS,
75
- // Folded hydration JSON columns (see foldedHydrationSelects) — surfaced via
76
- // the FOLDED_* markers, never as flat fields.
86
+ // Folded hydration JSON columns (see foldedHydrationSelects and
87
+ // foldedSeoSelect) — surfaced via the FOLDED_* markers or expanded onto
88
+ // SEO_ALIAS_COLUMNS, never as flat fields.
77
89
  "_emdash_terms",
78
90
  "_emdash_bylines",
91
+ SEO_FOLDED_COLUMN,
79
92
  ]);
80
93
 
81
94
  /** Markers for byline/taxonomy hydration folded into the content query. */
@@ -123,6 +136,62 @@ function foldedHydrationSelects(db: Kysely<any>, type: string, outer: string) {
123
136
  return { terms, bylines };
124
137
  }
125
138
 
139
+ /**
140
+ * Correlated JSON-object subquery that folds per-entry SEO into the content
141
+ * query without widening the result set: 1 row of `_emdash_seo` becomes 1 JSON
142
+ * column rather than 5 flat columns. The JSON column is expanded onto the row
143
+ * via {@link expandFoldedSeo} after the query runs, preserving the alias keys
144
+ * that {@link extractSeo} reads. Missing SEO row (no entry in `_emdash_seo`)
145
+ * yields NULL, which {@link expandFoldedSeo} treats as "no SEO" - identical to
146
+ * the prior LEFT JOIN miss behavior.
147
+ *
148
+ * Dialect-specific aggregation mirrors {@link foldedHydrationSelects}: SQLite
149
+ * `json_object` returns a JSON *string*, Postgres `json_build_object` returns
150
+ * parsed JSON; both branches are handled in expansion.
151
+ */
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- any Kysely instance
153
+ function foldedSeoSelect(db: Kysely<any>, type: string, outer: string) {
154
+ const o = sql.ref(outer);
155
+ const pg = isPostgres(db);
156
+ // Use raw column names (not aliases) as JSON keys: the JSON is expanded back
157
+ // onto SEO_COLUMN_ALIASES in JS, and keeping the keys matched to the
158
+ // underlying columns makes the SQL readable and the expansion 1-to-1.
159
+ const pairs =
160
+ "'seo_title', s.seo_title, 'seo_description', s.seo_description, 'seo_image', s.seo_image, 'seo_canonical', s.seo_canonical, 'seo_no_index', s.seo_no_index";
161
+ const obj = pg ? sql.raw(`json_build_object(${pairs})`) : sql.raw(`json_object(${pairs})`);
162
+ return sql`(SELECT ${obj} FROM ${sql.ref("_emdash_seo")} AS s WHERE s.collection = ${type} AND s.content_id = ${o}.id LIMIT 1) AS ${sql.ref(SEO_FOLDED_COLUMN)}`;
163
+ }
164
+
165
+ /**
166
+ * Expand the folded `_emdash_seo` JSON column onto the row using SEO_COLUMN_ALIASES,
167
+ * so {@link extractSeo} reads it transparently. SQLite returns a JSON string
168
+ * (parse it); Postgres returns already-parsed JSON. Missing/malformed/null is
169
+ * a no-op: {@link extractSeo} returns null when the aliases are absent.
170
+ */
171
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
172
+ return !!value && typeof value === "object" && !Array.isArray(value);
173
+ }
174
+
175
+ function expandFoldedSeo(row: Record<string, unknown>): void {
176
+ const raw = row[SEO_FOLDED_COLUMN];
177
+ delete row[SEO_FOLDED_COLUMN];
178
+ let parsed: Record<string, unknown> | null = null;
179
+ if (typeof raw === "string") {
180
+ try {
181
+ const candidate: unknown = JSON.parse(raw);
182
+ if (isPlainObject(candidate)) parsed = candidate;
183
+ } catch {
184
+ return; // malformed JSON: leave the row without SEO aliases (extractSeo returns null)
185
+ }
186
+ } else if (isPlainObject(raw)) {
187
+ parsed = raw;
188
+ }
189
+ if (!parsed) return;
190
+ for (const [col, alias] of Object.entries(SEO_COLUMN_ALIASES)) {
191
+ row[alias] = parsed[col] ?? null;
192
+ }
193
+ }
194
+
126
195
  /**
127
196
  * Stash folded hydration JSON (non-enumerable) for the query.ts fast paths.
128
197
  * SQLite returns a JSON string (parse it); Postgres returns already-parsed JSON.
@@ -227,6 +296,18 @@ async function getTaxonomyNames(db: Kysely<Database>): Promise<Set<string>> {
227
296
  }
228
297
  }
229
298
 
299
+ /**
300
+ * Reset the module-scoped taxonomy-names cache.
301
+ *
302
+ * Called from `invalidateTaxonomyDefsCache()` so that creating or seeding a
303
+ * taxonomy definition is reflected within the current isolate instead of
304
+ * waiting for the isolate to recycle. Keeps this cache consistent with the
305
+ * isolate-wide taxonomy-defs cache in `taxonomies/index.ts`.
306
+ */
307
+ export function resetTaxonomyNamesCache(): void {
308
+ taxonomyNames = null;
309
+ }
310
+
230
311
  /**
231
312
  * System columns to include in data (mapped to camelCase where needed)
232
313
  */
@@ -879,11 +960,12 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
879
960
  taxonomyFilters.map(
880
961
  (f) => sql`AND EXISTS (
881
962
  SELECT 1 FROM content_taxonomies ct
882
- INNER JOIN taxonomies t ON t.id = ct.taxonomy_id
963
+ INNER JOIN taxonomies t ON t.translation_group = ct.taxonomy_id
883
964
  WHERE ct.collection = ${type}
884
965
  AND ct.entry_id = ${sql.ref(tableName)}.id
885
966
  AND t.name = ${f.name}
886
967
  AND t.slug IN (${sql.join(f.slugs.map((s) => sql`${s}`))})
968
+ ${locale ? sql`AND t.locale = ${locale}` : sql``}
887
969
  )`,
888
970
  ),
889
971
  sql` `,
@@ -1050,31 +1132,25 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
1050
1132
  // When locale is specified, prefer locale-scoped slug match,
1051
1133
  // but IDs are globally unique so always check id without locale scope.
1052
1134
  //
1053
- // LEFT JOIN _emdash_seo folds per-entry SEO (canonical, noindex,
1054
- // etc.) into this single query at zero extra round-trip cost. The
1055
- // joined columns are surfaced as a nested data.seo object via
1056
- // extractSeo() and excluded from the generic field mapping. SEO is
1057
- // 1:1 with content (PK on collection+content_id), so the join never
1058
- // multiplies rows.
1059
- const seoSelect = sql.join(
1060
- Object.entries(SEO_COLUMN_ALIASES).map(
1061
- ([col, alias]) => sql`${sql.ref(`s.${col}`)} AS ${sql.ref(alias)}`,
1062
- ),
1063
- );
1064
- // Fold byline + taxonomy hydration into the content query (see
1065
- // foldedHydrationSelects), removing the two separate hydration
1066
- // round trips per fetch.
1135
+ // Byline + taxonomy hydration (foldedHydrationSelects) and per-entry
1136
+ // SEO (foldedSeoSelect) are each surfaced as a single aggregated JSON
1137
+ // column rather than flat columns. This keeps the result-set width
1138
+ // bounded at any collection schema width: a flat `LEFT JOIN _emdash_seo`
1139
+ // adds 5 alias columns to every row and pushes wide flat-schema
1140
+ // collections (common after WordPress / ACF imports) past D1's
1141
+ // per-result-set column limit, surfacing as a silent null entry. One
1142
+ // JSON column is one column, so the join stays safe at any width and
1143
+ // we keep the single round trip.
1067
1144
  const { terms: termsSelect, bylines: bylinesSelect } = foldedHydrationSelects(
1068
1145
  db,
1069
1146
  type,
1070
1147
  "c",
1071
1148
  );
1149
+ const seoSelect = foldedSeoSelect(db, type, "c");
1072
1150
  const result = locale
1073
1151
  ? await sql<Record<string, unknown>>`
1074
1152
  SELECT c.*, ${seoSelect}, ${termsSelect}, ${bylinesSelect}
1075
1153
  FROM ${sql.ref(tableName)} AS c
1076
- LEFT JOIN ${sql.ref("_emdash_seo")} AS s
1077
- ON s.collection = ${type} AND s.content_id = c.id
1078
1154
  WHERE c.deleted_at IS NULL
1079
1155
  AND ((c.slug = ${id} AND c.locale = ${locale}) OR c.id = ${id})
1080
1156
  LIMIT 1
@@ -1082,8 +1158,6 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
1082
1158
  : await sql<Record<string, unknown>>`
1083
1159
  SELECT c.*, ${seoSelect}, ${termsSelect}, ${bylinesSelect}
1084
1160
  FROM ${sql.ref(tableName)} AS c
1085
- LEFT JOIN ${sql.ref("_emdash_seo")} AS s
1086
- ON s.collection = ${type} AND s.content_id = c.id
1087
1161
  WHERE c.deleted_at IS NULL
1088
1162
  AND (c.slug = ${id} OR c.id = ${id})
1089
1163
  LIMIT 1
@@ -1094,6 +1168,11 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
1094
1168
  return undefined;
1095
1169
  }
1096
1170
 
1171
+ // Expand the folded SEO JSON column onto SEO_COLUMN_ALIASES so
1172
+ // extractSeo() reads it transparently. Missing/null SEO is a
1173
+ // no-op: extractSeo() returns null when the aliases are absent.
1174
+ expandFoldedSeo(row);
1175
+
1097
1176
  const i18nConfig = virtualConfig?.i18n;
1098
1177
  const i18nEnabled = i18nConfig && i18nConfig.locales.length > 1;
1099
1178
  const entrySlug = rowStr(row, "slug") || rowStr(row, "id");
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Image Metadata Enrichment
3
+ *
4
+ * Single seam that derives image dimensions and LQIP placeholders (blurhash,
5
+ * dominant color) from raw image bytes. Every server-side media-creation path
6
+ * routes through this so records are populated consistently. Pure-JS and
7
+ * Workers-safe (image-size reads headers only; generatePlaceholder guards
8
+ * decode size).
9
+ */
10
+
11
+ import { normalizeMime } from "./mime.js";
12
+ import { generatePlaceholder, readDimensions } from "./placeholder.js";
13
+
14
+ export interface EnrichedImageMetadata {
15
+ width?: number;
16
+ height?: number;
17
+ blurhash?: string;
18
+ dominantColor?: string;
19
+ }
20
+
21
+ /**
22
+ * Derive dimensions + LQIP placeholders from image bytes.
23
+ *
24
+ * - Non-image content types return `{}`.
25
+ * - `knownDimensions` (e.g. browser `naturalWidth/Height`) win over `image-size`
26
+ * for the *stored record* because the browser applies EXIF orientation;
27
+ * `image-size` reports raw header dimensions, which are swapped for
28
+ * 90°/270°-rotated JPEGs. They are NOT used for the decode OOM guard — see below.
29
+ * - The placeholder OOM guard uses only header dimensions read from the bytes
30
+ * actually decoded. Caller-supplied `knownDimensions` are untrusted for the
31
+ * guard: a client could claim a tiny size for a huge image to bypass the cap.
32
+ * - `placeholder` lets a caller decode a smaller thumbnail for the blurhash to
33
+ * avoid OOM on large originals; dimensions still come from `bytes`.
34
+ * - Placeholders are jpeg/png only (the generator's supported formats); other
35
+ * image types still get dimensions.
36
+ */
37
+ export async function enrichImageMetadata(
38
+ bytes: Uint8Array,
39
+ contentType: string,
40
+ opts?: {
41
+ knownDimensions?: { width: number; height: number };
42
+ placeholder?: { bytes: Uint8Array; contentType: string };
43
+ },
44
+ ): Promise<EnrichedImageMetadata> {
45
+ const normalizedContentType = normalizeMime(contentType);
46
+ if (!normalizedContentType.startsWith("image/")) return {};
47
+
48
+ // Header dimensions are read once from the actual bytes. They feed the
49
+ // placeholder OOM guard, which must never trust caller-supplied dimensions:
50
+ // `knownDimensions` is decoupled from the buffer, so a client could claim a
51
+ // tiny size for a huge image and slip past the decoded-size cap, making the
52
+ // decoder allocate an unbounded RGBA buffer and OOM the runtime. Only dims
53
+ // read from the buffer that actually gets decoded can bound the decode.
54
+ const headerDims = readDimensions(bytes) ?? undefined;
55
+
56
+ // Dimensions published on the record prefer the caller's knownDimensions
57
+ // (e.g. browser naturalWidth/Height, which apply EXIF orientation) over the
58
+ // raw header dims, which are swapped for 90°/270°-rotated JPEGs.
59
+ const recordDims = opts?.knownDimensions ?? headerDims;
60
+
61
+ // When a smaller thumbnail override is supplied, decode that for the blurhash
62
+ // and let generatePlaceholder read the thumbnail's own header for the OOM
63
+ // guard (the override buffer is what actually gets decoded). On the common
64
+ // no-override path pass the header dims already read from this same buffer.
65
+ const override = opts?.placeholder;
66
+ const placeholder = await generatePlaceholder(
67
+ override ? override.bytes : bytes,
68
+ override ? normalizeMime(override.contentType) : normalizedContentType,
69
+ override ? undefined : headerDims,
70
+ );
71
+
72
+ return {
73
+ width: recordDims?.width,
74
+ height: recordDims?.height,
75
+ blurhash: placeholder?.blurhash,
76
+ dominantColor: placeholder?.dominantColor,
77
+ };
78
+ }
@@ -27,6 +27,7 @@ export type {
27
27
  export { mediaItemToValue } from "./types.js";
28
28
  export { normalizeMediaValue } from "./normalize.js";
29
29
  export { generatePlaceholder, type PlaceholderData } from "./placeholder.js";
30
+ export { enrichImageMetadata, type EnrichedImageMetadata } from "./enrich.js";
30
31
 
31
32
  // Built-in providers
32
33
  export { localMedia, type LocalMediaConfig } from "./local.js";
@@ -73,6 +73,8 @@ export const createMediaProvider: CreateMediaProviderFn<LocalMediaRuntimeConfig>
73
73
  size: item.size ?? undefined,
74
74
  width: item.width ?? undefined,
75
75
  height: item.height ?? undefined,
76
+ blurhash: item.blurhash ?? undefined,
77
+ dominantColor: item.dominantColor ?? undefined,
76
78
  alt: item.alt ?? undefined,
77
79
  previewUrl: `/_emdash/api/media/file/${item.storageKey}`,
78
80
  meta: {
@@ -97,6 +99,8 @@ export const createMediaProvider: CreateMediaProviderFn<LocalMediaRuntimeConfig>
97
99
  size: item.size ?? undefined,
98
100
  width: item.width ?? undefined,
99
101
  height: item.height ?? undefined,
102
+ blurhash: item.blurhash ?? undefined,
103
+ dominantColor: item.dominantColor ?? undefined,
100
104
  alt: item.alt ?? undefined,
101
105
  previewUrl: `/_emdash/api/media/file/${item.storageKey}`,
102
106
  meta: {
@@ -148,6 +152,15 @@ export const createMediaProvider: CreateMediaProviderFn<LocalMediaRuntimeConfig>
148
152
  const src = `/_emdash/api/media/file/${storageKey}`;
149
153
  const mimeType = value.mimeType || "";
150
154
 
155
+ // Prefer the first-class fields; fall back to `meta` for legacy snapshots
156
+ // stored before LQIP was promoted off the provider-specific `meta` bag.
157
+ const blurhash =
158
+ value.blurhash ??
159
+ (typeof value.meta?.blurhash === "string" ? value.meta.blurhash : undefined);
160
+ const dominantColor =
161
+ value.dominantColor ??
162
+ (typeof value.meta?.dominantColor === "string" ? value.meta.dominantColor : undefined);
163
+
151
164
  // Determine embed type based on MIME type
152
165
  if (mimeType.startsWith("image/")) {
153
166
  return {
@@ -155,6 +168,8 @@ export const createMediaProvider: CreateMediaProviderFn<LocalMediaRuntimeConfig>
155
168
  src,
156
169
  width: value.width,
157
170
  height: value.height,
171
+ blurhash,
172
+ dominantColor,
158
173
  alt: value.alt,
159
174
  };
160
175
  }
@@ -185,6 +200,8 @@ export const createMediaProvider: CreateMediaProviderFn<LocalMediaRuntimeConfig>
185
200
  src,
186
201
  width: value.width,
187
202
  height: value.height,
203
+ blurhash,
204
+ dominantColor,
188
205
  alt: value.alt,
189
206
  };
190
207
  },
@@ -221,6 +238,8 @@ export function repoItemToProviderItem(item: {
221
238
  size: item.size ?? undefined,
222
239
  width: item.width ?? undefined,
223
240
  height: item.height ?? undefined,
241
+ blurhash: item.blurhash ?? undefined,
242
+ dominantColor: item.dominantColor ?? undefined,
224
243
  alt: item.alt ?? undefined,
225
244
  previewUrl: `/_emdash/api/media/file/${item.storageKey}`,
226
245
  meta: {
@@ -60,7 +60,14 @@ export async function normalizeMediaValue(
60
60
  const needsDimensions = result.width == null || result.height == null;
61
61
  const needsStorageKey = provider === "local" && !result.meta?.storageKey;
62
62
  const needsFileInfo = !result.mimeType || !result.filename;
63
- const needsLookup = needsDimensions || needsStorageKey || needsFileInfo;
63
+ // LQIP placeholders are immutable facts of the bytes: if an image record is
64
+ // missing them, the provider may have gained them since (e.g. content saved
65
+ // before LQIP backfill ran, or the row's blurhash was populated later). Pull
66
+ // them on every image lookup so the LQIP backfill in mergeProviderData runs.
67
+ const needsLqip =
68
+ (result.mimeType ?? "").startsWith("image/") &&
69
+ (result.blurhash == null || result.dominantColor == null);
70
+ const needsLookup = needsDimensions || needsStorageKey || needsFileInfo || needsLqip;
64
71
 
65
72
  if (!needsLookup || !id) return result;
66
73
 
@@ -138,6 +145,8 @@ async function resolveInternalUrl(
138
145
  mimeType: item.mimeType,
139
146
  width: item.width,
140
147
  height: item.height,
148
+ blurhash: item.blurhash,
149
+ dominantColor: item.dominantColor,
141
150
  alt: item.alt,
142
151
  meta: item.meta,
143
152
  };
@@ -167,6 +176,8 @@ async function resolveLocalId(
167
176
  mimeType: item.mimeType,
168
177
  width: item.width,
169
178
  height: item.height,
179
+ blurhash: item.blurhash,
180
+ dominantColor: item.dominantColor,
170
181
  alt: item.alt,
171
182
  meta: item.meta,
172
183
  };
@@ -183,6 +194,12 @@ function mergeProviderData(existing: MediaValue, item: MediaProviderItem): Media
183
194
  if (result.width == null && item.width != null) result.width = item.width;
184
195
  if (result.height == null && item.height != null) result.height = item.height;
185
196
 
197
+ // Fill missing LQIP placeholders (immutable facts of the bytes; caller wins)
198
+ if (result.blurhash == null && item.blurhash != null) result.blurhash = item.blurhash;
199
+ if (result.dominantColor == null && item.dominantColor != null) {
200
+ result.dominantColor = item.dominantColor;
201
+ }
202
+
186
203
  // Fill missing file info
187
204
  if (!result.filename && item.filename) result.filename = item.filename;
188
205
  if (!result.mimeType && item.mimeType) result.mimeType = item.mimeType;
@@ -217,6 +234,8 @@ function recordToMediaValue(obj: Record<string, unknown>): MediaValue {
217
234
  if (typeof obj.mimeType === "string") result.mimeType = obj.mimeType;
218
235
  if (typeof obj.width === "number") result.width = obj.width;
219
236
  if (typeof obj.height === "number") result.height = obj.height;
237
+ if (typeof obj.blurhash === "string") result.blurhash = obj.blurhash;
238
+ if (typeof obj.dominantColor === "string") result.dominantColor = obj.dominantColor;
220
239
  if (typeof obj.alt === "string") result.alt = obj.alt;
221
240
  if (isRecord(obj.meta)) result.meta = obj.meta;
222
241
  return result;