emdash 0.24.1 → 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.
- package/dist/api/route-utils.d.mts +2 -2
- package/dist/api/route-utils.mjs +9 -8
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +2 -2
- package/dist/{api-d_0wi9fW.mjs → api-D2_sbavn.mjs} +16 -12
- package/dist/api-D2_sbavn.mjs.map +1 -0
- package/dist/{apply-BE9UaWX0.mjs → apply-Bk2mAuLn.mjs} +11 -10
- package/dist/apply-Bk2mAuLn.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +2 -2
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +66 -37
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +4 -4
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +1 -1
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +5 -5
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/index.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/bulk.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/index.mjs +5 -5
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +3 -3
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +2 -2
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +21 -21
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +21 -21
- package/dist/astro/routes/api/admin/plugins/updates.mjs +20 -20
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +20 -20
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +4 -4
- package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/index.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/register-options.mjs +4 -4
- package/dist/astro/routes/api/auth/logout.mjs +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +5 -5
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
- package/dist/astro/routes/api/auth/me.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/complete.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/request.mjs +5 -5
- package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +4 -4
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/index.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/trash.mjs +5 -5
- package/dist/astro/routes/api/dashboard.mjs +5 -5
- package/dist/astro/routes/api/dev/emails.mjs +1 -1
- package/dist/astro/routes/api/import/probe.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/media.d.mts +13 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.mjs +12 -8
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
- package/dist/astro/routes/api/manifest.mjs +2 -2
- package/dist/astro/routes/api/mcp.mjs +20 -20
- package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +43 -7
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +5 -5
- package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
- package/dist/astro/routes/api/media/providers/index.mjs +2 -2
- package/dist/astro/routes/api/media/upload-url.mjs +7 -7
- package/dist/astro/routes/api/media.mjs +21 -20
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_.mjs +5 -5
- package/dist/astro/routes/api/menus/index.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs +2 -2
- package/dist/astro/routes/api/oauth/device/code.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/token.mjs +3 -3
- package/dist/astro/routes/api/oauth/register.mjs +1 -1
- package/dist/astro/routes/api/oauth/token/refresh.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/revoke.mjs +2 -2
- package/dist/astro/routes/api/oauth/token.mjs +1 -1
- package/dist/astro/routes/api/openapi.json.mjs +2 -2
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +5 -5
- package/dist/astro/routes/api/redirects/404s/summary.mjs +5 -5
- package/dist/astro/routes/api/redirects/_id_.mjs +6 -6
- package/dist/astro/routes/api/redirects/index.mjs +6 -6
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +2 -2
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/index.mjs +20 -20
- package/dist/astro/routes/api/schema/index.mjs +3 -3
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +20 -20
- package/dist/astro/routes/api/schema/orphans/index.mjs +20 -20
- package/dist/astro/routes/api/search/enable.mjs +7 -7
- package/dist/astro/routes/api/search/index.mjs +6 -6
- package/dist/astro/routes/api/search/rebuild.mjs +7 -7
- package/dist/astro/routes/api/search/stats.mjs +4 -4
- package/dist/astro/routes/api/search/suggest.mjs +6 -6
- package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
- package/dist/astro/routes/api/sections/index.mjs +7 -7
- package/dist/astro/routes/api/settings/email.mjs +3 -3
- package/dist/astro/routes/api/settings.mjs +9 -9
- package/dist/astro/routes/api/setup/admin-verify.mjs +4 -4
- package/dist/astro/routes/api/setup/admin.mjs +4 -4
- package/dist/astro/routes/api/setup/dev-bypass.mjs +7 -7
- package/dist/astro/routes/api/setup/dev-reset.mjs +1 -1
- package/dist/astro/routes/api/setup/index.mjs +10 -10
- package/dist/astro/routes/api/setup/status.mjs +1 -1
- package/dist/astro/routes/api/snapshot.mjs +2 -2
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
- package/dist/astro/routes/api/themes/preview.mjs +2 -2
- package/dist/astro/routes/api/typegen.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
- package/dist/astro/routes/api/widget-components.mjs +1 -1
- package/dist/astro/routes/robots.txt.mjs +3 -3
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
- package/dist/astro/routes/sitemap.xml.mjs +4 -4
- package/dist/astro/types.d.mts +7 -7
- package/dist/{authorize-B__u-iZJ.mjs → authorize-CuxZ0i2t.mjs} +2 -2
- package/dist/{authorize-B__u-iZJ.mjs.map → authorize-CuxZ0i2t.mjs.map} +1 -1
- package/dist/{byline-fields-Ca56gaT6.d.mts → byline-fields-KD3JFRuH.d.mts} +24 -6
- package/dist/byline-fields-KD3JFRuH.d.mts.map +1 -0
- package/dist/{byline-fields-B-KXkg5R.mjs → byline-fields-eyJMblSr.mjs} +2 -2
- package/dist/{byline-fields-B-KXkg5R.mjs.map → byline-fields-eyJMblSr.mjs.map} +1 -1
- package/dist/{bylines-BMtJnPnG.mjs → bylines-CPc_q2Vd.mjs} +4 -4
- package/dist/{bylines-BMtJnPnG.mjs.map → bylines-CPc_q2Vd.mjs.map} +1 -1
- package/dist/{cache-BUJe2w6Z.mjs → cache-C35VWD5D.mjs} +1 -1
- package/dist/{cache-BUJe2w6Z.mjs.map → cache-C35VWD5D.mjs.map} +1 -1
- package/dist/cli/index.mjs +8 -8
- package/dist/{content-PoVGKjp0.mjs → content-B43-YrIq.mjs} +7 -5
- package/dist/content-B43-YrIq.mjs.map +1 -0
- package/dist/{context-Dvws41_A.mjs → context-BW3N7wyt.mjs} +12 -6
- package/dist/context-BW3N7wyt.mjs.map +1 -0
- package/dist/{dashboard-DsYU8JKS.mjs → dashboard-FIe0S512.mjs} +3 -3
- package/dist/{dashboard-DsYU8JKS.mjs.map → dashboard-FIe0S512.mjs.map} +1 -1
- package/dist/{db-errors-UXB1CK42.mjs → db-errors-CK46D-ly.mjs} +1 -1
- package/dist/{db-errors-UXB1CK42.mjs.map → db-errors-CK46D-ly.mjs.map} +1 -1
- package/dist/{placeholder-B0s2mWu5.mjs → enrich-BQ0mxJRs.mjs} +61 -10
- package/dist/enrich-BQ0mxJRs.mjs.map +1 -0
- package/dist/{error-lq_fL-RS.mjs → error-DXnK2wF4.mjs} +2 -1
- package/dist/error-DXnK2wF4.mjs.map +1 -0
- package/dist/{index-BroXT-vD.d.mts → index-Cz6_JBL3.d.mts} +21 -8
- package/dist/index-Cz6_JBL3.d.mts.map +1 -0
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +28 -28
- package/dist/{loader-BR2SgfEc.mjs → loader-DrpofRon.mjs} +2 -2
- package/dist/{loader-BR2SgfEc.mjs.map → loader-DrpofRon.mjs.map} +1 -1
- package/dist/{manifest-schema-BWKnQYQF.mjs → manifest-schema-BOfPXse0.mjs} +2 -1
- package/dist/manifest-schema-BOfPXse0.mjs.map +1 -0
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +45 -2
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +5 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +6 -6
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +15 -3
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-CTjJoJTh.mjs → media-BjKxjzk8.mjs} +3 -1
- package/dist/media-BjKxjzk8.mjs.map +1 -0
- package/dist/{media-allowlist-B77I5_bw.mjs → media-allowlist-Du8t1u6a.mjs} +2 -2
- package/dist/{media-allowlist-B77I5_bw.mjs.map → media-allowlist-Du8t1u6a.mjs.map} +1 -1
- package/dist/{menus-GvRSpgIA.mjs → menus-CxqJ-z-N.mjs} +46 -4
- package/dist/menus-CxqJ-z-N.mjs.map +1 -0
- package/dist/{mime-BTzvzJ8M.mjs → mime-CCEzze7W.mjs} +1 -1
- package/dist/{mime-BTzvzJ8M.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
- package/dist/{normalize-ClbNIenm.mjs → normalize-CeglrBT9.mjs} +11 -2
- package/dist/normalize-CeglrBT9.mjs.map +1 -0
- package/dist/{options-CqECG0gM.d.mts → options-BvClpP5Q.d.mts} +2 -2
- package/dist/{options-CqECG0gM.d.mts.map → options-BvClpP5Q.d.mts.map} +1 -1
- package/dist/page/index.d.mts +1 -1
- package/dist/{parse-DHI873qU.mjs → parse-DqMWHQ8x.mjs} +2 -2
- package/dist/{parse-DHI873qU.mjs.map → parse-DqMWHQ8x.mjs.map} +1 -1
- package/dist/{placeholder-QoI4gEzP.d.mts → placeholder-Cuce9U-m.d.mts} +20 -5
- package/dist/placeholder-Cuce9U-m.d.mts.map +1 -0
- package/dist/plugin-types.d.mts +2 -1
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-utils.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-DZdlS6Cu.mjs → query-34pfm02Q.mjs} +11 -11
- package/dist/{query-DZdlS6Cu.mjs.map → query-34pfm02Q.mjs.map} +1 -1
- package/dist/{rate-limit-D1Y6VFVP.mjs → rate-limit-CBQK4ygJ.mjs} +2 -2
- package/dist/{rate-limit-D1Y6VFVP.mjs.map → rate-limit-CBQK4ygJ.mjs.map} +1 -1
- package/dist/{redirects-x9eFPeyi.mjs → redirects-Dcbuisoj.mjs} +2 -1
- package/dist/redirects-Dcbuisoj.mjs.map +1 -0
- package/dist/{registry-CJkCubOJ.mjs → registry-3aOAS4My.mjs} +11 -1
- package/dist/registry-3aOAS4My.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-DMjXuvNU.mjs → schema-BGwx8AQY.mjs} +3 -3
- package/dist/{schema-DMjXuvNU.mjs.map → schema-BGwx8AQY.mjs.map} +1 -1
- package/dist/{search-D1wKk3lc.mjs → search-Gl747zMP.mjs} +2 -2
- package/dist/{search-D1wKk3lc.mjs.map → search-Gl747zMP.mjs.map} +1 -1
- package/dist/{sections-CCMic8dB.mjs → sections-C-umuuLj.mjs} +2 -2
- package/dist/{sections-CCMic8dB.mjs.map → sections-C-umuuLj.mjs.map} +1 -1
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +6 -6
- package/dist/seo/index.d.mts +1 -1
- package/dist/{seo-3QrZ1EPT.mjs → seo-BNzkiK_W.mjs} +1 -1
- package/dist/{seo-3QrZ1EPT.mjs.map → seo-BNzkiK_W.mjs.map} +1 -1
- package/dist/{seo-DCEF-AVr.mjs → seo-BkhuuaaE.mjs} +25 -3
- package/dist/seo-BkhuuaaE.mjs.map +1 -0
- package/dist/{settings-D9rvaLuY.mjs → settings-C4tXRYiG.mjs} +3 -3
- package/dist/{settings-D9rvaLuY.mjs.map → settings-C4tXRYiG.mjs.map} +1 -1
- package/dist/{settings-C3Yi5Co3.mjs → settings-fPSR6dJa.mjs} +2 -2
- package/dist/{settings-C3Yi5Co3.mjs.map → settings-fPSR6dJa.mjs.map} +1 -1
- package/dist/{taxonomies-DhLaNJji.mjs → taxonomies-DbAtL6m4.mjs} +2 -2
- package/dist/{taxonomies-DhLaNJji.mjs.map → taxonomies-DbAtL6m4.mjs.map} +1 -1
- package/dist/{taxonomies-CurpLtNC.mjs → taxonomies-n8w72lLQ.mjs} +4 -4
- package/dist/{taxonomies-CurpLtNC.mjs.map → taxonomies-n8w72lLQ.mjs.map} +1 -1
- package/dist/{types-CTLJYMhN.d.mts → types-CNlaBFzx.d.mts} +11 -1
- package/dist/{types-CTLJYMhN.d.mts.map → types-CNlaBFzx.d.mts.map} +1 -1
- package/dist/{types-BWh2MjmA.d.mts → types-ETmO_jQr.d.mts} +2 -2
- package/dist/{types-BWh2MjmA.d.mts.map → types-ETmO_jQr.d.mts.map} +1 -1
- package/dist/{types-TTx8uP7V.d.mts → types-h2Cwbzi2.d.mts} +6 -3
- package/dist/{types-TTx8uP7V.d.mts.map → types-h2Cwbzi2.d.mts.map} +1 -1
- package/dist/types-tM44hEcf.mjs.map +1 -1
- package/dist/{validate-DOFrIb-c.d.mts → validate-Va_An3qb.d.mts} +2 -2
- package/dist/{validate-DOFrIb-c.d.mts.map → validate-Va_An3qb.d.mts.map} +1 -1
- package/dist/{validation-CY1gk4Ar.mjs → validation-B6DKPTtU.mjs} +3 -3
- package/dist/{validation-CY1gk4Ar.mjs.map → validation-B6DKPTtU.mjs.map} +1 -1
- package/dist/version-DHdCA24u.mjs +7 -0
- package/dist/{version-C0JPFUG8.mjs.map → version-DHdCA24u.mjs.map} +1 -1
- package/dist/{widgets-Dri0spI4.mjs → widgets-C2RbFN0G.mjs} +2 -2
- package/dist/{widgets-Dri0spI4.mjs.map → widgets-C2RbFN0G.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/errors.ts +1 -0
- package/src/api/handlers/content.ts +4 -4
- package/src/api/handlers/seo.ts +30 -2
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/middleware.ts +14 -1
- package/src/astro/routes/api/import/wordpress/media.ts +9 -3
- package/src/astro/routes/api/media/[id]/confirm.ts +62 -2
- package/src/astro/routes/api/media.ts +17 -19
- package/src/components/EmDashImage.astro +4 -7
- package/src/components/Image.astro +14 -4
- package/src/components/InlinePortableTextEditor.tsx +48 -1
- package/src/database/repositories/content.ts +8 -5
- package/src/database/repositories/media.ts +9 -1
- package/src/emdash-runtime.ts +39 -1
- package/src/media/enrich.ts +78 -0
- package/src/media/index.ts +1 -0
- package/src/media/local-runtime.ts +19 -0
- package/src/media/normalize.ts +20 -1
- package/src/media/placeholder.ts +29 -9
- package/src/media/types.ts +14 -0
- package/src/plugin-types.ts +2 -0
- package/src/plugins/context.ts +8 -0
- package/src/plugins/hooks.ts +45 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/manager.ts +11 -0
- package/src/plugins/manifest-schema.ts +1 -0
- package/src/plugins/types.ts +7 -0
- package/src/schema/registry.ts +25 -0
- package/src/schema/types.ts +10 -0
- package/src/seed/apply.ts +1 -0
- package/dist/api-d_0wi9fW.mjs.map +0 -1
- package/dist/apply-BE9UaWX0.mjs.map +0 -1
- package/dist/byline-fields-Ca56gaT6.d.mts.map +0 -1
- package/dist/content-PoVGKjp0.mjs.map +0 -1
- package/dist/context-Dvws41_A.mjs.map +0 -1
- package/dist/error-lq_fL-RS.mjs.map +0 -1
- package/dist/index-BroXT-vD.d.mts.map +0 -1
- package/dist/manifest-schema-BWKnQYQF.mjs.map +0 -1
- package/dist/media-CTjJoJTh.mjs.map +0 -1
- package/dist/menus-GvRSpgIA.mjs.map +0 -1
- package/dist/normalize-ClbNIenm.mjs.map +0 -1
- package/dist/placeholder-B0s2mWu5.mjs.map +0 -1
- package/dist/placeholder-QoI4gEzP.d.mts.map +0 -1
- package/dist/redirects-x9eFPeyi.mjs.map +0 -1
- package/dist/registry-CJkCubOJ.mjs.map +0 -1
- package/dist/seo-DCEF-AVr.mjs.map +0 -1
- package/dist/version-C0JPFUG8.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
|
|
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
|
-
|
|
157
|
-
const
|
|
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?: {
|
|
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<
|
|
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
|
|
670
|
-
if (
|
|
671
|
-
|
|
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?: {
|
|
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
|
|
package/src/emdash-runtime.ts
CHANGED
|
@@ -2797,7 +2797,14 @@ export class EmDashRuntime {
|
|
|
2797
2797
|
}
|
|
2798
2798
|
|
|
2799
2799
|
async handleContentRestore(collection: string, id: string) {
|
|
2800
|
-
|
|
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
|
+
}
|
package/src/media/index.ts
CHANGED
|
@@ -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: {
|
package/src/media/normalize.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/media/placeholder.ts
CHANGED
|
@@ -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
|
|
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.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
package/src/media/types.ts
CHANGED
|
@@ -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
|
};
|
package/src/plugin-types.ts
CHANGED
|
@@ -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;
|
package/src/plugins/context.ts
CHANGED
|
@@ -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 {
|