emdash 0.24.0 → 0.25.1

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 (340) hide show
  1. package/dist/api/route-utils.d.mts +2 -2
  2. package/dist/api/route-utils.mjs +9 -8
  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-D2_sbavn.mjs} +16 -12
  7. package/dist/api-D2_sbavn.mjs.map +1 -0
  8. package/dist/{apply-BE9UaWX0.mjs → apply-Bk2mAuLn.mjs} +11 -10
  9. package/dist/apply-Bk2mAuLn.mjs.map +1 -0
  10. package/dist/astro/index.d.mts +6 -6
  11. package/dist/astro/index.mjs +2 -2
  12. package/dist/astro/middleware/auth.d.mts +5 -5
  13. package/dist/astro/middleware/auth.mjs +1 -1
  14. package/dist/astro/middleware/redirect.mjs +2 -2
  15. package/dist/astro/middleware.d.mts.map +1 -1
  16. package/dist/astro/middleware.mjs +66 -37
  17. package/dist/astro/middleware.mjs.map +1 -1
  18. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +4 -4
  19. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +4 -4
  20. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +1 -1
  21. package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
  22. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
  23. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +5 -5
  24. package/dist/astro/routes/api/admin/byline-fields/index.mjs +5 -5
  25. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +5 -5
  26. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +7 -7
  27. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +7 -7
  28. package/dist/astro/routes/api/admin/bylines/index.mjs +7 -7
  29. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +5 -5
  30. package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
  31. package/dist/astro/routes/api/admin/comments/bulk.mjs +5 -5
  32. package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
  33. package/dist/astro/routes/api/admin/comments/index.mjs +5 -5
  34. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +3 -3
  35. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +2 -2
  36. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +2 -2
  37. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +2 -2
  38. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +20 -20
  39. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +20 -20
  40. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +20 -20
  41. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +20 -20
  42. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +20 -20
  43. package/dist/astro/routes/api/admin/plugins/index.mjs +20 -20
  44. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
  45. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +20 -20
  46. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +20 -20
  47. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +20 -20
  48. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +20 -20
  49. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +21 -21
  50. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +20 -20
  51. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +21 -21
  52. package/dist/astro/routes/api/admin/plugins/updates.mjs +20 -20
  53. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +20 -20
  54. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
  55. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +20 -20
  56. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
  57. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
  58. package/dist/astro/routes/api/admin/users/_id_/index.mjs +4 -4
  59. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
  60. package/dist/astro/routes/api/admin/users/index.mjs +4 -4
  61. package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
  62. package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
  63. package/dist/astro/routes/api/auth/invite/complete.mjs +4 -4
  64. package/dist/astro/routes/api/auth/invite/index.mjs +4 -4
  65. package/dist/astro/routes/api/auth/invite/register-options.mjs +4 -4
  66. package/dist/astro/routes/api/auth/logout.mjs +1 -1
  67. package/dist/astro/routes/api/auth/magic-link/send.mjs +5 -5
  68. package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
  69. package/dist/astro/routes/api/auth/me.mjs +4 -4
  70. package/dist/astro/routes/api/auth/passkey/_id_.mjs +4 -4
  71. package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
  72. package/dist/astro/routes/api/auth/passkey/options.mjs +5 -5
  73. package/dist/astro/routes/api/auth/passkey/register/options.mjs +4 -4
  74. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +4 -4
  75. package/dist/astro/routes/api/auth/passkey/verify.mjs +4 -4
  76. package/dist/astro/routes/api/auth/signup/complete.mjs +4 -4
  77. package/dist/astro/routes/api/auth/signup/request.mjs +5 -5
  78. package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
  79. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +4 -4
  80. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +4 -4
  81. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
  82. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +2 -2
  83. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +2 -2
  84. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
  85. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +5 -5
  86. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +5 -5
  87. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +2 -2
  88. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
  89. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +5 -5
  90. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
  91. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
  92. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +2 -2
  93. package/dist/astro/routes/api/content/_collection_/_id_.mjs +5 -5
  94. package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
  95. package/dist/astro/routes/api/content/_collection_/index.mjs +5 -5
  96. package/dist/astro/routes/api/content/_collection_/trash.mjs +5 -5
  97. package/dist/astro/routes/api/dashboard.mjs +5 -5
  98. package/dist/astro/routes/api/dev/emails.mjs +1 -1
  99. package/dist/astro/routes/api/import/probe.mjs +5 -5
  100. package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
  101. package/dist/astro/routes/api/import/wordpress/execute.d.mts +5 -5
  102. package/dist/astro/routes/api/import/wordpress/execute.mjs +4 -4
  103. package/dist/astro/routes/api/import/wordpress/media.d.mts +13 -1
  104. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  105. package/dist/astro/routes/api/import/wordpress/media.mjs +12 -8
  106. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  107. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  108. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
  109. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
  110. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
  111. package/dist/astro/routes/api/manifest.mjs +2 -2
  112. package/dist/astro/routes/api/mcp.mjs +20 -20
  113. package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
  114. package/dist/astro/routes/api/media/_id_/confirm.mjs +43 -7
  115. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  116. package/dist/astro/routes/api/media/_id_.mjs +5 -5
  117. package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
  118. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
  119. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
  120. package/dist/astro/routes/api/media/providers/index.mjs +2 -2
  121. package/dist/astro/routes/api/media/upload-url.mjs +7 -7
  122. package/dist/astro/routes/api/media.mjs +21 -20
  123. package/dist/astro/routes/api/media.mjs.map +1 -1
  124. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  125. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  126. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  127. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  128. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  129. package/dist/astro/routes/api/menus/index.mjs +5 -5
  130. package/dist/astro/routes/api/oauth/device/authorize.mjs +2 -2
  131. package/dist/astro/routes/api/oauth/device/code.mjs +3 -3
  132. package/dist/astro/routes/api/oauth/device/token.mjs +3 -3
  133. package/dist/astro/routes/api/oauth/register.mjs +1 -1
  134. package/dist/astro/routes/api/oauth/token/refresh.mjs +2 -2
  135. package/dist/astro/routes/api/oauth/token/revoke.mjs +2 -2
  136. package/dist/astro/routes/api/oauth/token.mjs +1 -1
  137. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  138. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
  139. package/dist/astro/routes/api/redirects/404s/index.mjs +5 -5
  140. package/dist/astro/routes/api/redirects/404s/summary.mjs +5 -5
  141. package/dist/astro/routes/api/redirects/_id_.mjs +6 -6
  142. package/dist/astro/routes/api/redirects/index.mjs +6 -6
  143. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
  144. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +2 -2
  145. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +20 -20
  146. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +20 -20
  147. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +20 -20
  148. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +20 -20
  149. package/dist/astro/routes/api/schema/collections/index.mjs +20 -20
  150. package/dist/astro/routes/api/schema/index.mjs +3 -3
  151. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +20 -20
  152. package/dist/astro/routes/api/schema/orphans/index.mjs +20 -20
  153. package/dist/astro/routes/api/search/enable.mjs +7 -7
  154. package/dist/astro/routes/api/search/index.mjs +6 -6
  155. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  156. package/dist/astro/routes/api/search/stats.mjs +4 -4
  157. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  158. package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
  159. package/dist/astro/routes/api/sections/index.mjs +7 -7
  160. package/dist/astro/routes/api/settings/email.mjs +3 -3
  161. package/dist/astro/routes/api/settings.mjs +9 -9
  162. package/dist/astro/routes/api/setup/admin-verify.mjs +4 -4
  163. package/dist/astro/routes/api/setup/admin.mjs +4 -4
  164. package/dist/astro/routes/api/setup/dev-bypass.mjs +7 -7
  165. package/dist/astro/routes/api/setup/dev-reset.mjs +1 -1
  166. package/dist/astro/routes/api/setup/index.mjs +10 -10
  167. package/dist/astro/routes/api/setup/status.mjs +1 -1
  168. package/dist/astro/routes/api/snapshot.mjs +2 -2
  169. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
  170. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
  171. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
  172. package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
  173. package/dist/astro/routes/api/themes/preview.mjs +2 -2
  174. package/dist/astro/routes/api/typegen.mjs +3 -3
  175. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +5 -5
  176. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
  177. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
  178. package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -4
  179. package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
  180. package/dist/astro/routes/api/widget-components.mjs +1 -1
  181. package/dist/astro/routes/robots.txt.mjs +3 -3
  182. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  183. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  184. package/dist/astro/types.d.mts +7 -7
  185. package/dist/{authorize-B__u-iZJ.mjs → authorize-CuxZ0i2t.mjs} +2 -2
  186. package/dist/{authorize-B__u-iZJ.mjs.map → authorize-CuxZ0i2t.mjs.map} +1 -1
  187. package/dist/{byline-fields-RFPkDLRw.d.mts → byline-fields-KD3JFRuH.d.mts} +19 -1
  188. package/dist/byline-fields-KD3JFRuH.d.mts.map +1 -0
  189. package/dist/{byline-fields-B-KXkg5R.mjs → byline-fields-eyJMblSr.mjs} +2 -2
  190. package/dist/{byline-fields-B-KXkg5R.mjs.map → byline-fields-eyJMblSr.mjs.map} +1 -1
  191. package/dist/{bylines-BMtJnPnG.mjs → bylines-CPc_q2Vd.mjs} +4 -4
  192. package/dist/{bylines-BMtJnPnG.mjs.map → bylines-CPc_q2Vd.mjs.map} +1 -1
  193. package/dist/{cache-BUJe2w6Z.mjs → cache-C35VWD5D.mjs} +1 -1
  194. package/dist/{cache-BUJe2w6Z.mjs.map → cache-C35VWD5D.mjs.map} +1 -1
  195. package/dist/cli/index.mjs +8 -8
  196. package/dist/{content-PoVGKjp0.mjs → content-B43-YrIq.mjs} +7 -5
  197. package/dist/content-B43-YrIq.mjs.map +1 -0
  198. package/dist/{context-Dvws41_A.mjs → context-BW3N7wyt.mjs} +12 -6
  199. package/dist/context-BW3N7wyt.mjs.map +1 -0
  200. package/dist/{dashboard-DsYU8JKS.mjs → dashboard-FIe0S512.mjs} +3 -3
  201. package/dist/{dashboard-DsYU8JKS.mjs.map → dashboard-FIe0S512.mjs.map} +1 -1
  202. package/dist/{db-errors-UXB1CK42.mjs → db-errors-CK46D-ly.mjs} +1 -1
  203. package/dist/{db-errors-UXB1CK42.mjs.map → db-errors-CK46D-ly.mjs.map} +1 -1
  204. package/dist/{placeholder-B0s2mWu5.mjs → enrich-BQ0mxJRs.mjs} +61 -10
  205. package/dist/enrich-BQ0mxJRs.mjs.map +1 -0
  206. package/dist/{error-lq_fL-RS.mjs → error-DXnK2wF4.mjs} +2 -1
  207. package/dist/error-DXnK2wF4.mjs.map +1 -0
  208. package/dist/{index-BroXT-vD.d.mts → index-Cz6_JBL3.d.mts} +21 -8
  209. package/dist/index-Cz6_JBL3.d.mts.map +1 -0
  210. package/dist/index.d.mts +8 -8
  211. package/dist/index.mjs +28 -28
  212. package/dist/{loader-BR2SgfEc.mjs → loader-DrpofRon.mjs} +2 -2
  213. package/dist/{loader-BR2SgfEc.mjs.map → loader-DrpofRon.mjs.map} +1 -1
  214. package/dist/{manifest-schema-BWKnQYQF.mjs → manifest-schema-BOfPXse0.mjs} +2 -1
  215. package/dist/manifest-schema-BOfPXse0.mjs.map +1 -0
  216. package/dist/media/image-endpoint.mjs +1 -1
  217. package/dist/media/index.d.mts +45 -2
  218. package/dist/media/index.d.mts.map +1 -1
  219. package/dist/media/index.mjs +5 -3
  220. package/dist/media/index.mjs.map +1 -1
  221. package/dist/media/local-runtime.d.mts +6 -6
  222. package/dist/media/local-runtime.d.mts.map +1 -1
  223. package/dist/media/local-runtime.mjs +15 -3
  224. package/dist/media/local-runtime.mjs.map +1 -1
  225. package/dist/{media-CTjJoJTh.mjs → media-BjKxjzk8.mjs} +3 -1
  226. package/dist/media-BjKxjzk8.mjs.map +1 -0
  227. package/dist/{media-allowlist-B77I5_bw.mjs → media-allowlist-Du8t1u6a.mjs} +2 -2
  228. package/dist/{media-allowlist-B77I5_bw.mjs.map → media-allowlist-Du8t1u6a.mjs.map} +1 -1
  229. package/dist/{menus-GvRSpgIA.mjs → menus-CxqJ-z-N.mjs} +46 -4
  230. package/dist/menus-CxqJ-z-N.mjs.map +1 -0
  231. package/dist/{mime-BTzvzJ8M.mjs → mime-CCEzze7W.mjs} +1 -1
  232. package/dist/{mime-BTzvzJ8M.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
  233. package/dist/{normalize-ClbNIenm.mjs → normalize-CeglrBT9.mjs} +11 -2
  234. package/dist/normalize-CeglrBT9.mjs.map +1 -0
  235. package/dist/{options-CqECG0gM.d.mts → options-BvClpP5Q.d.mts} +2 -2
  236. package/dist/{options-CqECG0gM.d.mts.map → options-BvClpP5Q.d.mts.map} +1 -1
  237. package/dist/page/index.d.mts +1 -1
  238. package/dist/{parse-DHI873qU.mjs → parse-DqMWHQ8x.mjs} +2 -2
  239. package/dist/{parse-DHI873qU.mjs.map → parse-DqMWHQ8x.mjs.map} +1 -1
  240. package/dist/{placeholder-QoI4gEzP.d.mts → placeholder-Cuce9U-m.d.mts} +20 -5
  241. package/dist/placeholder-Cuce9U-m.d.mts.map +1 -0
  242. package/dist/plugin-types.d.mts +2 -1
  243. package/dist/plugin-types.d.mts.map +1 -1
  244. package/dist/plugin-utils.d.mts +5 -5
  245. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  246. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  247. package/dist/{query-DZdlS6Cu.mjs → query-34pfm02Q.mjs} +11 -11
  248. package/dist/{query-DZdlS6Cu.mjs.map → query-34pfm02Q.mjs.map} +1 -1
  249. package/dist/{rate-limit-D1Y6VFVP.mjs → rate-limit-CBQK4ygJ.mjs} +2 -2
  250. package/dist/{rate-limit-D1Y6VFVP.mjs.map → rate-limit-CBQK4ygJ.mjs.map} +1 -1
  251. package/dist/{redirects-x9eFPeyi.mjs → redirects-Dcbuisoj.mjs} +2 -1
  252. package/dist/redirects-Dcbuisoj.mjs.map +1 -0
  253. package/dist/{registry-CJkCubOJ.mjs → registry-3aOAS4My.mjs} +11 -1
  254. package/dist/registry-3aOAS4My.mjs.map +1 -0
  255. package/dist/runtime.d.mts +6 -6
  256. package/dist/runtime.mjs +1 -1
  257. package/dist/{schema-DMjXuvNU.mjs → schema-BGwx8AQY.mjs} +3 -3
  258. package/dist/{schema-DMjXuvNU.mjs.map → schema-BGwx8AQY.mjs.map} +1 -1
  259. package/dist/{search-D1wKk3lc.mjs → search-Gl747zMP.mjs} +2 -2
  260. package/dist/{search-D1wKk3lc.mjs.map → search-Gl747zMP.mjs.map} +1 -1
  261. package/dist/{sections-CCMic8dB.mjs → sections-C-umuuLj.mjs} +2 -2
  262. package/dist/{sections-CCMic8dB.mjs.map → sections-C-umuuLj.mjs.map} +1 -1
  263. package/dist/seed/index.d.mts +1 -1
  264. package/dist/seed/index.mjs +6 -6
  265. package/dist/seo/index.d.mts +1 -1
  266. package/dist/{seo-3QrZ1EPT.mjs → seo-BNzkiK_W.mjs} +1 -1
  267. package/dist/{seo-3QrZ1EPT.mjs.map → seo-BNzkiK_W.mjs.map} +1 -1
  268. package/dist/{seo-DCEF-AVr.mjs → seo-BkhuuaaE.mjs} +25 -3
  269. package/dist/seo-BkhuuaaE.mjs.map +1 -0
  270. package/dist/{settings-D9rvaLuY.mjs → settings-C4tXRYiG.mjs} +3 -3
  271. package/dist/{settings-D9rvaLuY.mjs.map → settings-C4tXRYiG.mjs.map} +1 -1
  272. package/dist/{settings-C3Yi5Co3.mjs → settings-fPSR6dJa.mjs} +2 -2
  273. package/dist/{settings-C3Yi5Co3.mjs.map → settings-fPSR6dJa.mjs.map} +1 -1
  274. package/dist/{taxonomies-DhLaNJji.mjs → taxonomies-DbAtL6m4.mjs} +2 -2
  275. package/dist/{taxonomies-DhLaNJji.mjs.map → taxonomies-DbAtL6m4.mjs.map} +1 -1
  276. package/dist/{taxonomies-CurpLtNC.mjs → taxonomies-n8w72lLQ.mjs} +4 -4
  277. package/dist/{taxonomies-CurpLtNC.mjs.map → taxonomies-n8w72lLQ.mjs.map} +1 -1
  278. package/dist/{types-CTLJYMhN.d.mts → types-CNlaBFzx.d.mts} +11 -1
  279. package/dist/{types-CTLJYMhN.d.mts.map → types-CNlaBFzx.d.mts.map} +1 -1
  280. package/dist/{types-BWh2MjmA.d.mts → types-ETmO_jQr.d.mts} +2 -2
  281. package/dist/{types-BWh2MjmA.d.mts.map → types-ETmO_jQr.d.mts.map} +1 -1
  282. package/dist/{types-TTx8uP7V.d.mts → types-h2Cwbzi2.d.mts} +6 -3
  283. package/dist/{types-TTx8uP7V.d.mts.map → types-h2Cwbzi2.d.mts.map} +1 -1
  284. package/dist/types-tM44hEcf.mjs.map +1 -1
  285. package/dist/{validate-DOFrIb-c.d.mts → validate-Va_An3qb.d.mts} +2 -2
  286. package/dist/{validate-DOFrIb-c.d.mts.map → validate-Va_An3qb.d.mts.map} +1 -1
  287. package/dist/{validation-CY1gk4Ar.mjs → validation-B6DKPTtU.mjs} +3 -3
  288. package/dist/{validation-CY1gk4Ar.mjs.map → validation-B6DKPTtU.mjs.map} +1 -1
  289. package/dist/version-DHdCA24u.mjs +7 -0
  290. package/dist/{version-B5nHxI5Y.mjs.map → version-DHdCA24u.mjs.map} +1 -1
  291. package/dist/{widgets-Dri0spI4.mjs → widgets-C2RbFN0G.mjs} +2 -2
  292. package/dist/{widgets-Dri0spI4.mjs.map → widgets-C2RbFN0G.mjs.map} +1 -1
  293. package/package.json +5 -5
  294. package/src/api/errors.ts +1 -0
  295. package/src/api/handlers/content.ts +4 -4
  296. package/src/api/handlers/seo.ts +30 -2
  297. package/src/api/schemas/schema.ts +1 -0
  298. package/src/astro/middleware.ts +14 -1
  299. package/src/astro/routes/api/import/wordpress/media.ts +9 -3
  300. package/src/astro/routes/api/media/[id]/confirm.ts +62 -2
  301. package/src/astro/routes/api/media.ts +17 -19
  302. package/src/components/EmDashImage.astro +4 -7
  303. package/src/components/Image.astro +14 -4
  304. package/src/components/InlinePortableTextEditor.tsx +48 -1
  305. package/src/database/repositories/content.ts +8 -5
  306. package/src/database/repositories/media.ts +9 -1
  307. package/src/emdash-runtime.ts +39 -1
  308. package/src/media/enrich.ts +78 -0
  309. package/src/media/index.ts +1 -0
  310. package/src/media/local-runtime.ts +19 -0
  311. package/src/media/normalize.ts +20 -1
  312. package/src/media/placeholder.ts +29 -9
  313. package/src/media/types.ts +14 -0
  314. package/src/plugin-types.ts +2 -0
  315. package/src/plugins/context.ts +8 -0
  316. package/src/plugins/hooks.ts +45 -0
  317. package/src/plugins/index.ts +1 -0
  318. package/src/plugins/manager.ts +11 -0
  319. package/src/plugins/manifest-schema.ts +1 -0
  320. package/src/plugins/types.ts +7 -0
  321. package/src/schema/registry.ts +25 -0
  322. package/src/schema/types.ts +10 -0
  323. package/src/seed/apply.ts +1 -0
  324. package/dist/api-d_0wi9fW.mjs.map +0 -1
  325. package/dist/apply-BE9UaWX0.mjs.map +0 -1
  326. package/dist/byline-fields-RFPkDLRw.d.mts.map +0 -1
  327. package/dist/content-PoVGKjp0.mjs.map +0 -1
  328. package/dist/context-Dvws41_A.mjs.map +0 -1
  329. package/dist/error-lq_fL-RS.mjs.map +0 -1
  330. package/dist/index-BroXT-vD.d.mts.map +0 -1
  331. package/dist/manifest-schema-BWKnQYQF.mjs.map +0 -1
  332. package/dist/media-CTjJoJTh.mjs.map +0 -1
  333. package/dist/menus-GvRSpgIA.mjs.map +0 -1
  334. package/dist/normalize-ClbNIenm.mjs.map +0 -1
  335. package/dist/placeholder-B0s2mWu5.mjs.map +0 -1
  336. package/dist/placeholder-QoI4gEzP.d.mts.map +0 -1
  337. package/dist/redirects-x9eFPeyi.mjs.map +0 -1
  338. package/dist/registry-CJkCubOJ.mjs.map +0 -1
  339. package/dist/seo-DCEF-AVr.mjs.map +0 -1
  340. package/dist/version-B5nHxI5Y.mjs +0 -7
@@ -25,7 +25,8 @@ export interface Props {
25
25
  url?: string;
26
26
  /** Provider ID for external media (e.g., "cloudflare-images") */
27
27
  provider?: string;
28
- /** Provider metadata (blurhash, dominantColor, etc.) */
28
+ /** Provider metadata. LQIP is read from first-class fields below;
29
+ * `meta` only remains as a fallback for legacy snapshots. */
29
30
  meta?: Record<string, unknown>;
30
31
  };
31
32
  alt?: string;
@@ -34,6 +35,10 @@ export interface Props {
34
35
  width?: number;
35
36
  /** Original image height */
36
37
  height?: number;
38
+ /** LQIP blurhash — first-class field (legacy snapshots store it in `asset.meta`). */
39
+ blurhash?: string;
40
+ /** LQIP dominant color — first-class field (legacy snapshots store it in `asset.meta`). */
41
+ dominantColor?: string;
37
42
  /** Display width for this instance (overrides original) */
38
43
  displayWidth?: number;
39
44
  /** Display height for this instance (overrides original) */
@@ -152,9 +157,14 @@ if (!src) {
152
157
  }
153
158
  }
154
159
 
155
- // Build placeholder background style
156
- const blurhash = asset.meta?.blurhash as string | undefined;
157
- const dominantColor = asset.meta?.dominantColor as string | undefined;
160
+ // Build placeholder background style. Prefer first-class LQIP fields; fall
161
+ // back to `asset.meta` for legacy snapshots persisted before LQIP was promoted.
162
+ const blurhash = (node.blurhash ?? (asset.meta?.blurhash as string | undefined)) as
163
+ | string
164
+ | undefined;
165
+ const dominantColor = (node.dominantColor ?? (asset.meta?.dominantColor as string | undefined)) as
166
+ | string
167
+ | undefined;
158
168
 
159
169
  let placeholderStyle = "";
160
170
  if (blurhash) {
@@ -200,6 +200,13 @@ function convertPMNode(node: PMNode): PTBlock | PTBlock[] | null {
200
200
  }
201
201
  case "image": {
202
202
  const provider = attrStrOpt(node.attrs, "provider");
203
+ const blurhash = attrStrOpt(node.attrs, "blurhash");
204
+ const dominantColor = attrStrOpt(node.attrs, "dominantColor");
205
+ // Persist LQIP as first-class block fields (matching the image-field
206
+ // MediaValue path) rather than nesting in `asset.meta`, so read sites
207
+ // and normalize don't need a dual-shape fallback. `asset.meta` is left
208
+ // to carry only provider-specific data — it isn't reconstructed here,
209
+ // so non-LQIP meta keys are never silently dropped on editor round-trip.
203
210
  return {
204
211
  _type: "image",
205
212
  _key: k(),
@@ -212,6 +219,8 @@ function convertPMNode(node: PMNode): PTBlock | PTBlock[] | null {
212
219
  caption: attrStrOpt(node.attrs, "caption") ?? attrStrOpt(node.attrs, "title"),
213
220
  width: attrNum(node.attrs, "width"),
214
221
  height: attrNum(node.attrs, "height"),
222
+ ...(blurhash ? { blurhash } : {}),
223
+ ...(dominantColor ? { dominantColor } : {}),
215
224
  displayWidth: attrNum(node.attrs, "displayWidth"),
216
225
  displayHeight: attrNum(node.attrs, "displayHeight"),
217
226
  };
@@ -416,16 +425,38 @@ function convertPTBlock(block: PTBlock): JSONContent | null {
416
425
  }
417
426
  if (block._type === "image") {
418
427
  const ib = block as PTBlock & {
419
- asset?: { _ref?: string; url?: string; provider?: string };
428
+ asset?: {
429
+ _ref?: string;
430
+ url?: string;
431
+ provider?: string;
432
+ meta?: Record<string, unknown>;
433
+ };
420
434
  url?: string;
421
435
  alt?: string;
422
436
  caption?: string;
423
437
  width?: number;
424
438
  height?: number;
439
+ /** LQIP — first-class field (legacy snapshots keep it in `asset.meta`). */
440
+ blurhash?: string;
441
+ dominantColor?: string;
425
442
  displayWidth?: number;
426
443
  displayHeight?: number;
427
444
  };
428
445
  const asset = ib.asset;
446
+ const meta = asset?.meta;
447
+ // Prefer first-class LQIP fields; fall back to `asset.meta` for legacy.
448
+ const blurhash =
449
+ typeof ib.blurhash === "string"
450
+ ? ib.blurhash
451
+ : typeof meta?.blurhash === "string"
452
+ ? meta.blurhash
453
+ : null;
454
+ const dominantColor =
455
+ typeof ib.dominantColor === "string"
456
+ ? ib.dominantColor
457
+ : typeof meta?.dominantColor === "string"
458
+ ? meta.dominantColor
459
+ : null;
429
460
  return {
430
461
  type: "image",
431
462
  attrs: {
@@ -437,6 +468,8 @@ function convertPTBlock(block: PTBlock): JSONContent | null {
437
468
  provider: asset?.provider,
438
469
  width: ib.width,
439
470
  height: ib.height,
471
+ blurhash,
472
+ dominantColor,
440
473
  displayWidth: ib.displayWidth,
441
474
  displayHeight: ib.displayHeight,
442
475
  },
@@ -1145,6 +1178,8 @@ interface MediaItemData {
1145
1178
  storageKey?: string;
1146
1179
  width?: number;
1147
1180
  height?: number;
1181
+ blurhash?: string;
1182
+ dominantColor?: string;
1148
1183
  alt?: string;
1149
1184
  provider?: string;
1150
1185
  previewUrl?: string;
@@ -1224,6 +1259,8 @@ function InlineMediaPicker({
1224
1259
  storageKey?: string;
1225
1260
  width?: number;
1226
1261
  height?: number;
1262
+ blurhash?: string;
1263
+ dominantColor?: string;
1227
1264
  alt?: string;
1228
1265
  meta?: Record<string, unknown>;
1229
1266
  }>;
@@ -1239,6 +1276,8 @@ function InlineMediaPicker({
1239
1276
  storageKey: item.storageKey,
1240
1277
  width: item.width,
1241
1278
  height: item.height,
1279
+ blurhash: item.blurhash,
1280
+ dominantColor: item.dominantColor,
1242
1281
  alt: item.alt,
1243
1282
  provider: activeProvider === "local" ? undefined : activeProvider,
1244
1283
  previewUrl: item.previewUrl,
@@ -1319,6 +1358,8 @@ function InlineMediaPicker({
1319
1358
  storageKey: raw.storageKey,
1320
1359
  width: raw.width || dims.width,
1321
1360
  height: raw.height || dims.height,
1361
+ blurhash: raw.blurhash,
1362
+ dominantColor: raw.dominantColor,
1322
1363
  alt: raw.alt,
1323
1364
  };
1324
1365
  } else {
@@ -1339,6 +1380,8 @@ function InlineMediaPicker({
1339
1380
  url: raw.previewUrl || "",
1340
1381
  width: raw.width || dims.width,
1341
1382
  height: raw.height || dims.height,
1383
+ blurhash: raw.blurhash,
1384
+ dominantColor: raw.dominantColor,
1342
1385
  alt: raw.alt,
1343
1386
  provider: activeProvider,
1344
1387
  previewUrl: raw.previewUrl,
@@ -1854,6 +1897,8 @@ export function InlinePortableTextEditor({
1854
1897
  provider: { default: null },
1855
1898
  width: { default: null },
1856
1899
  height: { default: null },
1900
+ blurhash: { default: null },
1901
+ dominantColor: { default: null },
1857
1902
  };
1858
1903
  },
1859
1904
  }),
@@ -1924,6 +1969,8 @@ export function InlinePortableTextEditor({
1924
1969
  mediaId: item.id,
1925
1970
  width: item.width,
1926
1971
  height: item.height,
1972
+ blurhash: item.blurhash,
1973
+ dominantColor: item.dominantColor,
1927
1974
  })
1928
1975
  .run();
1929
1976
  setMediaPickerOpen(false);
@@ -656,19 +656,22 @@ export class ContentRepository {
656
656
  /**
657
657
  * Restore content from trash
658
658
  */
659
- async restore(type: string, id: string): Promise<boolean> {
659
+ async restore(type: string, id: string): Promise<ContentItem | null> {
660
660
  const tableName = getTableName(type);
661
661
 
662
- const result = await sql`
662
+ const result = await sql<Record<string, unknown>>`
663
663
  UPDATE ${sql.ref(tableName)}
664
664
  SET deleted_at = NULL
665
665
  WHERE id = ${id}
666
666
  AND deleted_at IS NOT NULL
667
+ RETURNING *
667
668
  `.execute(this.db);
668
669
 
669
- const changed = (result.numAffectedRows ?? 0n) > 0n;
670
- if (changed) invalidateCollectionCache(type);
671
- return changed;
670
+ const restored = result.rows[0];
671
+ if (!restored) return null;
672
+
673
+ invalidateCollectionCache(type);
674
+ return this.mapRow(type, restored);
672
675
  }
673
676
 
674
677
  /**
@@ -143,7 +143,13 @@ export class MediaRepository {
143
143
  */
144
144
  async confirmUpload(
145
145
  id: string,
146
- metadata?: { width?: number; height?: number; size?: number },
146
+ metadata?: {
147
+ width?: number;
148
+ height?: number;
149
+ size?: number;
150
+ blurhash?: string;
151
+ dominantColor?: string;
152
+ },
147
153
  ): Promise<MediaItem | null> {
148
154
  const existing = await this.findById(id);
149
155
  if (!existing) {
@@ -156,6 +162,8 @@ export class MediaRepository {
156
162
  if (metadata?.width !== undefined) updates.width = metadata.width;
157
163
  if (metadata?.height !== undefined) updates.height = metadata.height;
158
164
  if (metadata?.size !== undefined) updates.size = metadata.size;
165
+ if (metadata?.blurhash !== undefined) updates.blurhash = metadata.blurhash;
166
+ if (metadata?.dominantColor !== undefined) updates.dominant_color = metadata.dominantColor;
159
167
 
160
168
  await this.db.updateTable("media").set(updates).where("id", "=", id).execute();
161
169
 
@@ -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) {
@@ -3365,6 +3372,37 @@ export class EmDashRuntime {
3365
3372
  }
3366
3373
  }
3367
3374
 
3375
+ private runAfterRestoreHooks(content: Record<string, unknown>, collection: string): void {
3376
+ after(async () => {
3377
+ // Trusted plugins
3378
+ if (this.hooks.hasHooks("content:afterRestore")) {
3379
+ try {
3380
+ await this.hooks.runContentAfterRestore(content, collection);
3381
+ } catch (err) {
3382
+ console.error("EmDash afterRestore hook error:", err);
3383
+ }
3384
+ }
3385
+
3386
+ // Sandboxed plugins
3387
+ const tasks: Promise<void>[] = [];
3388
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
3389
+ const [pluginId] = pluginKey.split(":");
3390
+ if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
3391
+
3392
+ tasks.push(
3393
+ (async () => {
3394
+ try {
3395
+ await plugin.invokeHook("content:afterRestore", { content, collection });
3396
+ } catch (err) {
3397
+ console.error(`EmDash: Sandboxed plugin ${pluginId} afterRestore error:`, err);
3398
+ }
3399
+ })(),
3400
+ );
3401
+ }
3402
+ await Promise.allSettled(tasks);
3403
+ });
3404
+ }
3405
+
3368
3406
  private async handleSandboxedRoute(
3369
3407
  plugin: SandboxedPluginInstance,
3370
3408
  path: string,
@@ -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;
@@ -9,6 +9,8 @@
9
9
  import { encode } from "blurhash";
10
10
  import { imageSize } from "image-size";
11
11
 
12
+ import { normalizeMime } from "./mime.js";
13
+
12
14
  export interface PlaceholderData {
13
15
  blurhash: string;
14
16
  dominantColor: string;
@@ -85,8 +87,12 @@ function extractDominantColor(data: Uint8Array, width: number, height: number):
85
87
 
86
88
  /**
87
89
  * Read image dimensions from headers without decoding pixel data.
90
+ * Returns null when the header cannot be parsed.
91
+ *
92
+ * Shared by every caller that needs pixel dimensions so the header is parsed
93
+ * once per buffer, not re-read inside generatePlaceholder.
88
94
  */
89
- function getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {
95
+ export function readDimensions(buffer: Uint8Array): { width: number; height: number } | null {
90
96
  try {
91
97
  const result = imageSize(buffer);
92
98
  if (result.width != null && result.height != null) {
@@ -102,23 +108,37 @@ function getImageDimensions(buffer: Uint8Array): { width: number; height: number
102
108
  * Generate blurhash and dominant color from an image buffer.
103
109
  * Returns null for non-image MIME types or on failure.
104
110
  *
105
- * @param dimensions - Optional pre-known dimensions. Used as a fallback when
106
- * image-size cannot parse the buffer (e.g. truncated headers). When the
107
- * decoded size (width * height * 4) exceeds MAX_DECODED_BYTES, placeholder
108
- * generation is skipped to avoid OOM on memory-constrained runtimes.
111
+ * @param dimensions - Optional pre-known dimensions. When present they are
112
+ * trusted verbatim (the caller has typically already read them via
113
+ * readDimensions); otherwise dimensions are read from this buffer's header.
114
+ * Generation is skipped (returns null) when no dimensions are available at
115
+ * all, or when the decoded size (width * height * 4) exceeds
116
+ * MAX_DECODED_BYTES — both guards avoid OOM from unbounded decodes on
117
+ * memory-constrained runtimes.
109
118
  */
110
119
  export async function generatePlaceholder(
111
120
  buffer: Uint8Array,
112
121
  mimeType: string,
113
122
  dimensions?: { width: number; height: number },
114
123
  ): Promise<PlaceholderData | null> {
115
- const format = SUPPORTED_TYPES[mimeType];
124
+ const format = SUPPORTED_TYPES[normalizeMime(mimeType)];
116
125
  if (!format) return null;
117
126
 
118
127
  try {
119
- // Safety net: skip decode if the image would exceed the memory budget
120
- const dims = getImageDimensions(buffer) ?? dimensions;
121
- if (dims && dims.width * dims.height * 4 > MAX_DECODED_BYTES) {
128
+ // Trust caller-supplied dimensions when present (the caller has usually
129
+ // already read them via readDimensions); otherwise read them from this
130
+ // buffer. The header is parsed at most once per call.
131
+ const dims = dimensions ?? readDimensions(buffer);
132
+
133
+ // Safety net: the decoders allocate the full RGBA buffer with no internal
134
+ // cap, so refuse to decode unless we can bound the output size. When we
135
+ // have no parseable header AND no known dimensions, the decoded size is
136
+ // unbounded — a crafted/truncated PNG whose header image-size can't read
137
+ // could still be decodable by upng-js, so we must bail to avoid OOM on
138
+ // memory-constrained runtimes. LQIP is progressive enhancement; missing
139
+ // it is preferable to crashing the request.
140
+ if (!dims) return null;
141
+ if (dims.width * dims.height * 4 > MAX_DECODED_BYTES) {
122
142
  return null;
123
143
  }
124
144
 
@@ -85,6 +85,10 @@ export interface MediaProviderItem {
85
85
  /** Dimensions (for images/video) */
86
86
  width?: number;
87
87
  height?: number;
88
+ /** LQIP blurhash placeholder (images only) */
89
+ blurhash?: string;
90
+ /** LQIP dominant-color placeholder, as a CSS color (images only) */
91
+ dominantColor?: string;
88
92
  /** Accessibility text */
89
93
  alt?: string;
90
94
  /** Preview URL for admin UI thumbnail */
@@ -126,6 +130,10 @@ export interface ImageEmbed {
126
130
  sizes?: string;
127
131
  width?: number;
128
132
  height?: number;
133
+ /** LQIP blurhash placeholder for rendering before the image loads */
134
+ blurhash?: string;
135
+ /** LQIP dominant-color placeholder, as a CSS color */
136
+ dominantColor?: string;
129
137
  alt?: string;
130
138
  /** Base URL without transforms, for responsive image generation */
131
139
  cdnBaseUrl?: string;
@@ -256,6 +264,10 @@ export interface MediaValue {
256
264
  mimeType?: string;
257
265
  width?: number;
258
266
  height?: number;
267
+ /** Cached LQIP blurhash placeholder (images only) */
268
+ blurhash?: string;
269
+ /** Cached LQIP dominant-color placeholder, as a CSS color (images only) */
270
+ dominantColor?: string;
259
271
  alt?: string;
260
272
 
261
273
  /** Provider-specific data needed for embedding */
@@ -273,6 +285,8 @@ export function mediaItemToValue(providerId: string, item: MediaProviderItem): M
273
285
  mimeType: item.mimeType,
274
286
  width: item.width,
275
287
  height: item.height,
288
+ blurhash: item.blurhash,
289
+ dominantColor: item.dominantColor,
276
290
  alt: item.alt,
277
291
  meta: item.meta,
278
292
  };
@@ -49,6 +49,7 @@ import type {
49
49
  CommentModerateHandler,
50
50
  ContentAfterDeleteHandler,
51
51
  ContentAfterPublishHandler,
52
+ ContentAfterRestoreHandler,
52
53
  ContentAfterSaveHandler,
53
54
  ContentAfterUnpublishHandler,
54
55
  ContentBeforeDeleteHandler,
@@ -97,6 +98,7 @@ export interface HookHandlers {
97
98
  "content:afterDelete": ContentAfterDeleteHandler;
98
99
  "content:afterPublish": ContentAfterPublishHandler;
99
100
  "content:afterUnpublish": ContentAfterUnpublishHandler;
101
+ "content:afterRestore": ContentAfterRestoreHandler;
100
102
  "media:beforeUpload": MediaBeforeUploadHandler;
101
103
  "media:afterUpload": MediaAfterUploadHandler;
102
104
  cron: CronHandler;
@@ -21,6 +21,7 @@ import {
21
21
  SsrfError,
22
22
  stripCredentialHeaders,
23
23
  } from "../import/ssrf.js";
24
+ import { enrichImageMetadata } from "../media/enrich.js";
24
25
  import { invalidateSiteSettingsCache } from "../settings/index.js";
25
26
  import type { Storage } from "../storage/types.js";
26
27
  import { CronAccessImpl } from "./cron.js";
@@ -508,6 +509,9 @@ export function createMediaAccessWithWrite(
508
509
  contentType,
509
510
  });
510
511
 
512
+ // Derive dimensions + LQIP placeholders (no-op for non-images).
513
+ const enriched = await enrichImageMetadata(new Uint8Array(bytes), contentType);
514
+
511
515
  // Create DB record — clean up storage on failure
512
516
  let media;
513
517
  try {
@@ -517,6 +521,10 @@ export function createMediaAccessWithWrite(
517
521
  size: bytes.byteLength,
518
522
  storageKey,
519
523
  status: "ready",
524
+ width: enriched.width,
525
+ height: enriched.height,
526
+ blurhash: enriched.blurhash,
527
+ dominantColor: enriched.dominantColor,
520
528
  });
521
529
  } catch (error) {
522
530
  try {