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
@@ -56,6 +56,34 @@ export interface SitemapDataResponse {
56
56
  /** Maximum entries per sitemap (per spec) */
57
57
  const SITEMAP_MAX_ENTRIES = 50_000;
58
58
 
59
+ /** Matches a trailing timezone designator (`Z` or `±HH`, `±HHMM`, `±HH:MM`). */
60
+ const TZ_SUFFIX_RE = /([zZ]|[+-]\d{2}(:?\d{2})?)$/;
61
+
62
+ /**
63
+ * Normalize a stored timestamp to W3C Datetime (ISO 8601) for sitemaps.
64
+ *
65
+ * `updated_at` is not guaranteed to be ISO: the column default is
66
+ * `datetime('now')` on SQLite and `CURRENT_TIMESTAMP` on Postgres, both of
67
+ * which store a space-separated `YYYY-MM-DD HH:MM:SS` string (and imported
68
+ * content can carry other shapes). The sitemap `<lastmod>` field requires
69
+ * W3C Datetime, and Google Search Console rejects the space-separated form
70
+ * as "Invalid date". Normalize defensively, assuming UTC when no offset is
71
+ * present (matches SQLite's `datetime('now')`). Valid date strings are
72
+ * normalized to UTC ISO 8601; unparseable values are returned as-is.
73
+ */
74
+ function toW3CDate(value: string): string {
75
+ if (!value) return value;
76
+ let normalized = value.trim();
77
+ if (normalized.includes(" ") && !normalized.includes("T")) {
78
+ normalized = normalized.replace(" ", "T");
79
+ }
80
+ if (!TZ_SUFFIX_RE.test(normalized)) {
81
+ normalized += "Z";
82
+ }
83
+ const parsed = Date.parse(normalized);
84
+ return Number.isNaN(parsed) ? value : new Date(parsed).toISOString();
85
+ }
86
+
59
87
  /**
60
88
  * Collect all published, indexable content across SEO-enabled collections
61
89
  * for sitemap generation, grouped by collection.
@@ -133,7 +161,7 @@ export async function handleSitemapData(
133
161
  entries.push({
134
162
  id: row.id,
135
163
  slug: row.slug,
136
- updatedAt: row.updated_at,
164
+ updatedAt: toW3CDate(row.updated_at),
137
165
  locale: row.locale,
138
166
  translationGroup: row.translation_group,
139
167
  image: row.seo_image ?? null,
@@ -144,7 +172,7 @@ export async function handleSitemapData(
144
172
  collection: col.slug,
145
173
  urlPattern: col.url_pattern,
146
174
  // Rows are ordered by updated_at DESC, so first row is the latest
147
- lastmod: rows.rows[0].updated_at,
175
+ lastmod: toW3CDate(rows.rows[0].updated_at),
148
176
  entries,
149
177
  });
150
178
  } catch (err) {
@@ -12,7 +12,7 @@ import { ulid } from "ulidx";
12
12
 
13
13
  import { TaxonomyRepository } from "../../database/repositories/taxonomy.js";
14
14
  import type { Database, TaxonomyDefTable } from "../../database/types.js";
15
- import { invalidateTermCache } from "../../taxonomies/index.js";
15
+ import { invalidateTaxonomyDefsCache, invalidateTermCache } from "../../taxonomies/index.js";
16
16
  import type { ApiResult } from "../types.js";
17
17
 
18
18
  const NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
@@ -290,6 +290,10 @@ export async function handleTaxonomyCreate(
290
290
  })
291
291
  .execute();
292
292
 
293
+ // A new def changes which taxonomies exist — drop the isolate-wide
294
+ // defs/names caches so this isolate reflects it immediately.
295
+ invalidateTaxonomyDefsCache();
296
+
293
297
  const row = await db
294
298
  .selectFrom("_emdash_taxonomy_defs")
295
299
  .selectAll()
@@ -125,6 +125,7 @@ export const createFieldBody = z
125
125
  export const updateFieldBody = z
126
126
  .object({
127
127
  label: z.string().min(1).optional(),
128
+ type: fieldTypeValues.optional(),
128
129
  required: z.boolean().optional(),
129
130
  unique: z.boolean().optional(),
130
131
  defaultValue: z.unknown().optional(),
@@ -242,6 +242,21 @@ export function generatePluginsModule(descriptors: PluginDescriptor[]): string {
242
242
  let needsAdapter = false;
243
243
 
244
244
  descriptors.forEach((descriptor, index) => {
245
+ // Every `plugins: []` entry must resolve to a file/package entrypoint that
246
+ // can be statically imported and bundled at build time. An in-process
247
+ // `definePlugin({...})` result passed directly has no entrypoint; without
248
+ // this guard the generator emitted `import pluginDefN from "undefined";`,
249
+ // which failed deep in Rollup with `failed to resolve import "undefined"`
250
+ // (#1416). Fail fast with an actionable message instead.
251
+ if (!descriptor.entrypoint) {
252
+ throw new Error(
253
+ `[emdash] Plugin "${descriptor.id}" has no \`entrypoint\`. The astro integration's ` +
254
+ `\`plugins: []\` requires plugins that resolve to a file/package entrypoint so they can be ` +
255
+ `bundled at build time; an in-process \`definePlugin({...})\` result passed directly is not ` +
256
+ `supported. Move the plugin into its own module and reference it via a factory that returns ` +
257
+ `a descriptor with an \`entrypoint\` (e.g. \`plugins: [myPlugin()]\`).`,
258
+ );
259
+ }
245
260
  if (descriptor.format === "standard") {
246
261
  // Standard format: import default export, wrap with adaptSandboxEntry
247
262
  needsAdapter = true;
@@ -490,6 +490,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
490
490
  const sessionUser =
491
491
  context.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);
492
492
 
493
+ // Credentialed API requests (API tokens `ec_pat_*`, OAuth tokens
494
+ // `ec_oat_*`, and other Bearer credentials) carry no `astro-session`
495
+ // cookie, so `sessionUser` is null for them -- yet they still expect
496
+ // read-your-writes. The auth middleware that resolves the token runs
497
+ // *after* this one, so `locals.user` isn't populated here; detect the
498
+ // credential directly on the request. Request-scoped adapters use this to
499
+ // keep such requests on the primary/uncached connection (not a lagging
500
+ // read replica or the Hyperdrive query cache).
501
+ const hasBearerAuth = (request.headers.get("authorization") ?? "")
502
+ .toLowerCase()
503
+ .startsWith("bearer ");
504
+
493
505
  if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
494
506
  if (!sessionUser && !playgroundDb) {
495
507
  const timings: Array<{ name: string; dur: number; desc?: string }> = [];
@@ -668,6 +680,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
668
680
  // Content handlers
669
681
  handleContentList: runtime.handleContentList.bind(runtime),
670
682
  handleContentGet: runtime.handleContentGet.bind(runtime),
683
+ handleContentAuthors: runtime.handleContentAuthors.bind(runtime),
671
684
  handleContentCreate: runtime.handleContentCreate.bind(runtime),
672
685
  handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
673
686
  handleContentDelete: runtime.handleContentDelete.bind(runtime),
@@ -775,7 +788,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
775
788
  // per-request state (e.g. a D1 bookmark cookie for read-your-writes).
776
789
  const scoped = createRequestScopedDb({
777
790
  config: config?.database?.config,
778
- isAuthenticated: !!sessionUser,
791
+ isAuthenticated: !!sessionUser || hasBearerAuth,
779
792
  isWrite: request.method !== "GET" && request.method !== "HEAD",
780
793
  cookies: context.cookies,
781
794
  url,
@@ -20,6 +20,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
20
20
  import { isParseError, parseBody } from "#api/parse.js";
21
21
  import { wpMediaImportBody } from "#api/schemas.js";
22
22
  import { validateExternalUrl, ssrfSafeFetch, SsrfError } from "#import/ssrf.js";
23
+ import { enrichImageMetadata } from "#media/enrich.js";
23
24
  import type { EmDashHandlers } from "#types";
24
25
 
25
26
  import type { AttachmentInfo } from "./analyze.js";
@@ -125,7 +126,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
125
126
  }
126
127
  };
127
128
 
128
- async function importMediaWithProgress(
129
+ export async function importMediaWithProgress(
129
130
  attachments: AttachmentInfo[],
130
131
  db: NonNullable<EmDashHandlers["db"]>,
131
132
  storage: NonNullable<EmDashHandlers["storage"]>,
@@ -273,6 +274,9 @@ async function importMediaWithProgress(
273
274
  contentType,
274
275
  });
275
276
 
277
+ // Derive dimensions + LQIP placeholders (no-op for non-images).
278
+ const enriched = await enrichImageMetadata(new Uint8Array(buffer), contentType);
279
+
276
280
  // Create media record with content hash
277
281
  const mediaItem = await repo.create({
278
282
  filename: attachment.filename || `media-${attachment.id}${ext}`,
@@ -280,8 +284,10 @@ async function importMediaWithProgress(
280
284
  size,
281
285
  storageKey,
282
286
  contentHash,
283
- width: undefined,
284
- height: undefined,
287
+ width: enriched.width,
288
+ height: enriched.height,
289
+ blurhash: enriched.blurhash,
290
+ dominantColor: enriched.dominantColor,
285
291
  });
286
292
 
287
293
  // Build the new URL
@@ -14,10 +14,20 @@ import { requireOwnerPerm, requirePerm } from "#api/authorize.js";
14
14
  import { apiError, apiSuccess, handleError } from "#api/error.js";
15
15
  import { isParseError, parseOptionalBody } from "#api/parse.js";
16
16
  import { mediaConfirmBody } from "#api/schemas.js";
17
+ import { enrichImageMetadata } from "#media/enrich.js";
17
18
  import type { MediaItem } from "#types";
18
19
 
19
20
  export const prerender = false;
20
21
 
22
+ /**
23
+ * Max raw bytes to buffer for server-side LQIP generation at confirm time. The
24
+ * signed-URL upload flow exists so large files bypass server buffering — re-reading
25
+ * the whole object into a Worker's 128 MB heap to compute a blurhash would OOM
26
+ * on the very uploads that flow was designed for. LQIP is progressive
27
+ * enhancement: large images simply ship without a server-generated placeholder.
28
+ */
29
+ const MAX_PLACEHOLDER_DOWNLOAD_BYTES = 8 * 1024 * 1024;
30
+
21
31
  /**
22
32
  * Add URL to media item (relative URL for portability)
23
33
  */
@@ -81,11 +91,61 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
81
91
  }
82
92
  }
83
93
 
94
+ // For images, read the just-uploaded bytes back from storage once to
95
+ // generate LQIP placeholders (and server-side dimensions as a fallback).
96
+ // The signed-URL flow uploads directly to storage, so this confirm is the
97
+ // only point at which the server sees the bytes. Best-effort: a decode
98
+ // failure must not block the upload from being marked ready. We also cap
99
+ // the download size — buffering a large original into a Worker heap to
100
+ // compute a 32px blurhash would OOM on the uploads the signed-URL path
101
+ // exists to support, so oversized files skip the server-side placeholder.
102
+ let blurhash: string | undefined;
103
+ let dominantColor: string | undefined;
104
+ let width = body.width;
105
+ let height = body.height;
106
+ if (emdash.storage && existing.mimeType.startsWith("image/")) {
107
+ const knownSize = body.size ?? existing.size ?? undefined;
108
+ const tooLarge = knownSize != null && knownSize > MAX_PLACEHOLDER_DOWNLOAD_BYTES;
109
+ if (!tooLarge) {
110
+ try {
111
+ const { body: stream } = await emdash.storage.download(existing.storageKey);
112
+ const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
113
+ // Defense-in-depth for the unknown-size case: even though we
114
+ // already buffered it, refuse the decode so we don't also pay
115
+ // the (larger) RGBA allocation.
116
+ if (bytes.byteLength > MAX_PLACEHOLDER_DOWNLOAD_BYTES) {
117
+ console.warn(
118
+ `[media] confirm skipping placeholder: object ${existing.storageKey} is ${bytes.byteLength} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
119
+ );
120
+ } else {
121
+ const enriched = await enrichImageMetadata(bytes, existing.mimeType, {
122
+ knownDimensions:
123
+ body.width != null && body.height != null
124
+ ? { width: body.width, height: body.height }
125
+ : undefined,
126
+ });
127
+ blurhash = enriched.blurhash;
128
+ dominantColor = enriched.dominantColor;
129
+ width = width ?? enriched.width;
130
+ height = height ?? enriched.height;
131
+ }
132
+ } catch (error) {
133
+ console.error("[media] confirm placeholder generation failed:", error);
134
+ }
135
+ } else {
136
+ console.warn(
137
+ `[media] confirm skipping placeholder: object ${existing.storageKey} reported size ${knownSize} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
138
+ );
139
+ }
140
+ }
141
+
84
142
  // Confirm the upload
85
143
  const item = await repo.confirmUpload(id, {
86
144
  size: body.size,
87
- width: body.width,
88
- height: body.height,
145
+ width,
146
+ height,
147
+ blurhash,
148
+ dominantColor,
89
149
  });
90
150
 
91
151
  if (!item) {
@@ -16,8 +16,8 @@ import { GLOBAL_UPLOAD_ALLOWLIST, resolveFieldAllowlist } from "#api/handlers/me
16
16
  import { isParseError, parseQuery } from "#api/parse.js";
17
17
  import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
18
18
  import { MediaRepository } from "#db/repositories/media.js";
19
+ import { enrichImageMetadata } from "#media/enrich.js";
19
20
  import { matchesMimeAllowlist, normalizeMime } from "#media/mime.js";
20
- import { generatePlaceholder } from "#media/placeholder.js";
21
21
  import { computeContentHash } from "#utils/hash.js";
22
22
 
23
23
  import type { MediaItem } from "../../types.js";
@@ -163,34 +163,32 @@ export const POST: APIRoute = async ({ request, locals }) => {
163
163
  const width = widthStr ? parseInt(widthStr, 10) : undefined;
164
164
  const height = heightStr ? parseInt(heightStr, 10) : undefined;
165
165
 
166
- // Generate placeholder data for images.
167
- // If the client sent a thumbnail (small pre-resized image), use that
168
- // instead of the full buffer to avoid OOM on memory-constrained runtimes.
166
+ // Derive dimensions + LQIP placeholders via the shared helper.
167
+ // If the client sent a downscaled thumbnail, decode that for the blurhash
168
+ // (avoids OOM on large originals on memory-constrained runtimes).
169
169
  const thumbnailEntry = formData.get("thumbnail");
170
170
  const thumbnail = thumbnailEntry instanceof File ? thumbnailEntry : null;
171
-
172
- let placeholder: Awaited<ReturnType<typeof generatePlaceholder>> = null;
173
- if (file.type.startsWith("image/")) {
174
- if (thumbnail) {
175
- const thumbBuffer = new Uint8Array(await thumbnail.arrayBuffer());
176
- placeholder = await generatePlaceholder(thumbBuffer, thumbnail.type);
177
- } else {
178
- const clientDims = width && height ? { width, height } : undefined;
179
- placeholder = await generatePlaceholder(buffer, file.type, clientDims);
180
- }
181
- }
171
+ const enriched = await enrichImageMetadata(buffer, file.type, {
172
+ knownDimensions: width != null && height != null ? { width, height } : undefined,
173
+ placeholder: thumbnail
174
+ ? { bytes: new Uint8Array(await thumbnail.arrayBuffer()), contentType: thumbnail.type }
175
+ : undefined,
176
+ });
182
177
 
183
178
  // Create media record
184
179
  const result = await emdash.handleMediaCreate({
185
180
  filename: file.name,
186
181
  mimeType: normalizeMime(file.type),
187
182
  size: file.size,
188
- width,
189
- height,
183
+ // Client dimensions win over server header dimensions: the browser's
184
+ // naturalWidth/Height apply EXIF orientation, while image-size reports
185
+ // raw (pre-orientation) header dims — swapped for 90°/270° JPEGs.
186
+ width: width ?? enriched.width,
187
+ height: height ?? enriched.height,
190
188
  storageKey,
191
189
  contentHash,
192
- blurhash: placeholder?.blurhash,
193
- dominantColor: placeholder?.dominantColor,
190
+ blurhash: enriched.blurhash,
191
+ dominantColor: enriched.dominantColor,
194
192
  authorId: user?.id,
195
193
  });
196
194
 
@@ -138,11 +138,12 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
138
138
  lines.push(" </image:image>");
139
139
  }
140
140
 
141
- if (useXhtml && siblings && siblings.length > 1) {
141
+ const alternateEntries = siblings ?? (useXhtml ? [entry] : null);
142
+ if (useXhtml && alternateEntries) {
142
143
  // Emit one xhtml:link per sibling (including self -- Google
143
144
  // recommends including the page's own hreflang annotation).
144
145
  // Siblings with unroutable locales are skipped here too.
145
- for (const sib of siblings) {
146
+ for (const sib of alternateEntries) {
146
147
  const sibLoc = await resolveEntryUrl(sib);
147
148
  if (sibLoc === null) continue;
148
149
  lines.push(
@@ -154,13 +155,13 @@ export const GET: APIRoute = async ({ params, locals, url }) => {
154
155
  // the first sibling with a routable URL. Stable order:
155
156
  // rows arrive sorted by updated_at DESC from the handler.
156
157
  const defaultSibling =
157
- i18nConfig && siblings.find((s) => s.locale === i18nConfig.defaultLocale);
158
+ i18nConfig && alternateEntries.find((s) => s.locale === i18nConfig.defaultLocale);
158
159
  let xDefaultLoc: string | null = null;
159
160
  if (defaultSibling) {
160
161
  xDefaultLoc = await resolveEntryUrl(defaultSibling);
161
162
  }
162
163
  if (xDefaultLoc === null) {
163
- for (const sib of siblings) {
164
+ for (const sib of alternateEntries) {
164
165
  const sibLoc = await resolveEntryUrl(sib);
165
166
  if (sibLoc !== null) {
166
167
  xDefaultLoc = sibLoc;
@@ -17,8 +17,14 @@
17
17
  * ```
18
18
  */
19
19
  import type { PublicPageContext, PageMetadataContribution } from "../plugins/types.js";
20
- import { resolvePageMetadata, renderPageMetadata } from "../page/metadata.js";
20
+ import {
21
+ createSha256CspHash,
22
+ resolvePageMetadata,
23
+ renderPageMetadata,
24
+ type ResolvedPageMetadata,
25
+ } from "../page/metadata.js";
21
26
  import { renderFragments } from "../page/fragments.js";
27
+ import JsonLdScript from "./JsonLdScript.astro";
22
28
  import {
23
29
  generateBaseSeoContributions,
24
30
  generateSiteSeoContributions,
@@ -38,6 +44,7 @@ const runtime = getPageRuntime(Astro.locals as Record<string, unknown>);
38
44
  let metadataHtml = "";
39
45
  let siteIdentityHtml = "";
40
46
  let fragmentsHtml = "";
47
+ let jsonLdScripts: ResolvedPageMetadata["jsonld"] = [];
41
48
 
42
49
  if (runtime) {
43
50
  // Run independent async loads in parallel: site settings (SEO meta
@@ -79,17 +86,30 @@ if (runtime) {
79
86
  const siteContributions = generateSiteSeoContributions(siteSettings.seo);
80
87
  const allContributions = [...pluginContributions, ...siteContributions, ...baseContributions];
81
88
  const resolved = resolvePageMetadata(allContributions);
82
- metadataHtml = renderPageMetadata(resolved);
89
+ jsonLdScripts = resolved.jsonld;
90
+ metadataHtml = renderPageMetadata(resolved, { includeJsonLd: false });
83
91
  siteIdentityHtml = renderSiteIdentity({ favicon: siteSettings.favicon });
84
92
  fragmentsHtml = renderFragments(fragments, "head");
85
93
  } else {
86
94
  // No runtime (EmDash not initialized) — still render base SEO
87
95
  const baseContributions: PageMetadataContribution[] = generateBaseSeoContributions(page);
88
96
  const resolved = resolvePageMetadata(baseContributions);
89
- metadataHtml = renderPageMetadata(resolved);
97
+ jsonLdScripts = resolved.jsonld;
98
+ metadataHtml = renderPageMetadata(resolved, { includeJsonLd: false });
99
+ }
100
+
101
+ if (Astro.csp && jsonLdScripts.length > 0) {
102
+ await Promise.all(
103
+ jsonLdScripts.map(async ({ json }) => {
104
+ Astro.csp?.insertScriptHash(await createSha256CspHash(json));
105
+ }),
106
+ );
90
107
  }
91
108
  ---
92
109
 
93
110
  <Fragment set:html={metadataHtml} />
111
+ {
112
+ jsonLdScripts.map(({ json }) => <JsonLdScript json={json} />)
113
+ }
94
114
  <Fragment set:html={siteIdentityHtml} />
95
115
  <Fragment set:html={fragmentsHtml} />
@@ -158,15 +158,12 @@ if (img) {
158
158
  }
159
159
  }
160
160
 
161
- // Build placeholder background style
161
+ // Build placeholder background style. Prefer the first-class MediaValue fields;
162
+ // fall back to `meta` for snapshots stored before LQIP was promoted off `meta`.
162
163
  const blurhash =
163
- typeof image === "object"
164
- ? (image?.meta?.blurhash as string | undefined)
165
- : undefined;
164
+ img?.blurhash ?? (img?.meta?.blurhash as string | undefined);
166
165
  const dominantColor =
167
- typeof image === "object"
168
- ? (image?.meta?.dominantColor as string | undefined)
169
- : undefined;
166
+ img?.dominantColor ?? (img?.meta?.dominantColor as string | undefined);
170
167
 
171
168
  let placeholderStyle = "";
172
169
  if (blurhash) {
@@ -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);
@@ -0,0 +1,9 @@
1
+ ---
2
+ interface Props {
3
+ json: string;
4
+ }
5
+
6
+ const { json } = Astro.props;
7
+ ---
8
+
9
+ <script type="application/ld+json" set:html={json}></script>
@@ -199,6 +199,15 @@ async function rebuildTaxonomies(db: Kysely<unknown>, defaultLocale: string): Pr
199
199
  .on("taxonomies")
200
200
  .column("translation_group")
201
201
  .execute();
202
+ // Dropping the old table dropped idx_taxonomies_parent (from 015); recreate
203
+ // it here for parity with rebuildMenuItems/rebuildContentTaxonomies. Existing
204
+ // installs recover it via migration 047.
205
+ await db.schema
206
+ .createIndex("idx_taxonomies_parent")
207
+ .ifNotExists()
208
+ .on("taxonomies")
209
+ .column("parent_id")
210
+ .execute();
202
211
  }
203
212
 
204
213
  async function rebuildTaxonomyDefs(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
@@ -569,6 +578,13 @@ async function rebuildTaxonomiesDown(db: Kysely<unknown>): Promise<void> {
569
578
  await db.schema.dropTable("taxonomies").execute();
570
579
  await sql`ALTER TABLE taxonomies_old RENAME TO taxonomies`.execute(db);
571
580
  await db.schema.createIndex("idx_taxonomies_name").on("taxonomies").column("name").execute();
581
+ // Restore the pre-036 (migration 015) parent index so the rollback is faithful.
582
+ await db.schema
583
+ .createIndex("idx_taxonomies_parent")
584
+ .ifNotExists()
585
+ .on("taxonomies")
586
+ .column("parent_id")
587
+ .execute();
572
588
  }
573
589
 
574
590
  async function rebuildTaxonomyDefsDown(db: Kysely<unknown>): Promise<void> {