emdash 0.18.0 → 0.20.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.
- package/dist/{adapters-C5AWLJSD.d.mts → adapters-BzIHV3sw.d.mts} +1 -1
- package/dist/{adapters-C5AWLJSD.d.mts.map → adapters-BzIHV3sw.d.mts.map} +1 -1
- package/dist/{allowed-origins-CyYLEJkp.mjs → allowed-origins-B1u7Qnvg.mjs} +2 -2
- package/dist/{allowed-origins-CyYLEJkp.mjs.map → allowed-origins-B1u7Qnvg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +15 -15
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +3 -3
- package/dist/{api-Cs7DAACP.mjs → api-DStv36ik.mjs} +123 -20
- package/dist/api-DStv36ik.mjs.map +1 -0
- package/dist/{api-tokens-VrXNiNvV.mjs → api-tokens-DPfhPu5V.mjs} +2 -2
- package/dist/{api-tokens-VrXNiNvV.mjs.map → api-tokens-DPfhPu5V.mjs.map} +1 -1
- package/dist/{apply-BWMV4Zmw.mjs → apply-Dr7snAMT.mjs} +23 -23
- package/dist/apply-Dr7snAMT.mjs.map +1 -0
- package/dist/astro/index.d.mts +10 -10
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +115 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +9 -9
- package/dist/astro/middleware/auth.mjs +6 -6
- package/dist/astro/middleware/redirect.mjs +4 -4
- package/dist/astro/middleware/request-context.mjs +2 -2
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +26 -4
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +242 -259
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +12 -12
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +12 -12
- package/dist/astro/routes/api/admin/bylines/index.mjs +12 -12
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/index.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +33 -33
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +34 -34
- package/dist/astro/routes/api/admin/plugins/updates.mjs +33 -33
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +33 -33
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +33 -33
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
- package/dist/astro/routes/api/admin/users/index.mjs +5 -5
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +10 -10
- package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
- package/dist/astro/routes/api/auth/invite/register-options.mjs +9 -9
- package/dist/astro/routes/api/auth/logout.mjs +3 -3
- package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/me.mjs +6 -6
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +9 -9
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/verify.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/complete.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +6 -5
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +9 -8
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +12 -10
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +6 -5
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -8
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/authors.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/authors.mjs +19 -0
- package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/media.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +9 -9
- package/dist/astro/routes/api/manifest.mjs +4 -4
- package/dist/astro/routes/api/mcp.mjs +29 -29
- package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
- package/dist/astro/routes/api/media/_id_.mjs +6 -6
- package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
- package/dist/astro/routes/api/media/providers/index.mjs +3 -3
- package/dist/astro/routes/api/media/upload-url.mjs +7 -7
- package/dist/astro/routes/api/media.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_.mjs +7 -7
- package/dist/astro/routes/api/menus/index.mjs +7 -7
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
- package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
- package/dist/astro/routes/api/oauth/register.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
- package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
- package/dist/astro/routes/api/oauth/token.mjs +6 -6
- package/dist/astro/routes/api/openapi.json.mjs +17 -3
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
- package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
- package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
- package/dist/astro/routes/api/redirects/index.mjs +10 -10
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +33 -33
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +33 -33
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +33 -33
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +33 -33
- package/dist/astro/routes/api/schema/collections/index.mjs +33 -33
- package/dist/astro/routes/api/schema/index.mjs +9 -14
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +33 -33
- package/dist/astro/routes/api/schema/orphans/index.mjs +33 -33
- package/dist/astro/routes/api/search/enable.mjs +9 -9
- package/dist/astro/routes/api/search/index.mjs +8 -8
- package/dist/astro/routes/api/search/rebuild.mjs +9 -9
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +8 -8
- package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
- package/dist/astro/routes/api/sections/index.mjs +8 -8
- package/dist/astro/routes/api/settings/email.mjs +5 -5
- package/dist/astro/routes/api/settings.mjs +12 -12
- package/dist/astro/routes/api/setup/admin-verify.mjs +11 -11
- package/dist/astro/routes/api/setup/admin.mjs +10 -10
- package/dist/astro/routes/api/setup/dev-bypass.mjs +23 -23
- package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
- package/dist/astro/routes/api/setup/index.mjs +23 -23
- package/dist/astro/routes/api/setup/status.mjs +4 -4
- package/dist/astro/routes/api/snapshot.mjs +6 -6
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/index.mjs +11 -11
- package/dist/astro/routes/api/themes/preview.mjs +6 -6
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +9 -8
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +9 -8
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +6 -5
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +9 -8
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +3 -3
- package/dist/astro/routes/robots.txt.mjs +7 -7
- package/dist/astro/routes/sitemap-_collection_.xml.d.mts.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +16 -9
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +8 -8
- package/dist/astro/types.d.mts +19 -12
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-CotM4Yiu.mjs → authorize-DsMSVSaY.mjs} +2 -2
- package/dist/{authorize-CotM4Yiu.mjs.map → authorize-DsMSVSaY.mjs.map} +1 -1
- package/dist/{byline-CWQ9aSoz.mjs → byline-DUx48sJp.mjs} +6 -6
- package/dist/{byline-CWQ9aSoz.mjs.map → byline-DUx48sJp.mjs.map} +1 -1
- package/dist/{byline-fields-DC3Wkk-U.mjs → byline-fields--WxSNS79.mjs} +2 -2
- package/dist/{byline-fields-DC3Wkk-U.mjs.map → byline-fields--WxSNS79.mjs.map} +1 -1
- package/dist/{byline-fields-Dr-xcb6S.mjs → byline-fields-8TMtkBnH.mjs} +3 -3
- package/dist/{byline-fields-Dr-xcb6S.mjs.map → byline-fields-8TMtkBnH.mjs.map} +1 -1
- package/dist/{byline-fields-BNy7Ng1U.d.mts → byline-fields-DbibsvTl.d.mts} +30 -2
- package/dist/byline-fields-DbibsvTl.d.mts.map +1 -0
- package/dist/{byline-registry-CxK5g559.mjs → byline-registry-CWP7I71B.mjs} +3 -3
- package/dist/{byline-registry-CxK5g559.mjs.map → byline-registry-CWP7I71B.mjs.map} +1 -1
- package/dist/{bylines-LJMgENMI.mjs → bylines-BdxWCnPL.mjs} +3 -3
- package/dist/{bylines-LJMgENMI.mjs.map → bylines-BdxWCnPL.mjs.map} +1 -1
- package/dist/{bylines-BJSva1Un.mjs → bylines-s8c2DXbH.mjs} +50 -6
- package/dist/{bylines-BJSva1Un.mjs.map → bylines-s8c2DXbH.mjs.map} +1 -1
- package/dist/{cache-lZL7SgVb.mjs → cache-B_HzASVT.mjs} +3 -3
- package/dist/{cache-lZL7SgVb.mjs.map → cache-B_HzASVT.mjs.map} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs → challenge-store-DXX3rfdI.mjs} +1 -1
- package/dist/{challenge-store-DGwuCc4R.mjs.map → challenge-store-DXX3rfdI.mjs.map} +1 -1
- package/dist/{chunks-BU-vP9Dh.mjs → chunks-BerYVuve.mjs} +2 -2
- package/dist/{chunks-BU-vP9Dh.mjs.map → chunks-BerYVuve.mjs.map} +1 -1
- package/dist/cli/index.mjs +46 -32
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{comment-C4jVbCM8.mjs → comment-sqQxNpN3.mjs} +2 -2
- package/dist/{comment-C4jVbCM8.mjs.map → comment-sqQxNpN3.mjs.map} +1 -1
- package/dist/{comments-BTAbC0Ek.mjs → comments-Vkivawyl.mjs} +3 -3
- package/dist/{comments-BTAbC0Ek.mjs.map → comments-Vkivawyl.mjs.map} +1 -1
- package/dist/{components-CTfpu3PZ.mjs → components-CK0cuUoH.mjs} +1 -1
- package/dist/{components-CTfpu3PZ.mjs.map → components-CK0cuUoH.mjs.map} +1 -1
- package/dist/{content-CyqOmOzm.mjs → content-BIlVx-RX.mjs} +132 -43
- package/dist/content-BIlVx-RX.mjs.map +1 -0
- package/dist/{context-DZ7bEh5-.mjs → context-Y7BRkWes.mjs} +10 -10
- package/dist/{context-DZ7bEh5-.mjs.map → context-Y7BRkWes.mjs.map} +1 -1
- package/dist/{cron-DZovZUnC.mjs → cron-BJ2ClIlj.mjs} +4 -3
- package/dist/cron-BJ2ClIlj.mjs.map +1 -0
- package/dist/{dashboard-B5WQpNTP.mjs → dashboard-2JgAMWxK.mjs} +4 -4
- package/dist/{dashboard-B5WQpNTP.mjs.map → dashboard-2JgAMWxK.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +10 -1
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +13 -1
- package/dist/database/instrumentation.mjs.map +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-xLFNSsZ9.mjs → default-IlBaTFxM.mjs} +1 -1
- package/dist/{default-xLFNSsZ9.mjs.map → default-IlBaTFxM.mjs.map} +1 -1
- package/dist/{device-flow-ptLrVINd.mjs → device-flow-R23SIbQ2.mjs} +5 -5
- package/dist/{device-flow-ptLrVINd.mjs.map → device-flow-R23SIbQ2.mjs.map} +1 -1
- package/dist/{error-DJOsMVSt.mjs → error-RwM4dD35.mjs} +2 -2
- package/dist/{error-DJOsMVSt.mjs.map → error-RwM4dD35.mjs.map} +1 -1
- package/dist/{escape-bIyGoW5W.mjs → escape-Ds07EEyu.mjs} +1 -1
- package/dist/{escape-bIyGoW5W.mjs.map → escape-Ds07EEyu.mjs.map} +1 -1
- package/dist/{fts-manager-DR1ERA0c.mjs → fts-manager-1RgHmopc.mjs} +2 -2
- package/dist/{fts-manager-DR1ERA0c.mjs.map → fts-manager-1RgHmopc.mjs.map} +1 -1
- package/dist/{index-CjKdMZ3U.d.mts → index-B1keaX5Y.d.mts} +237 -24
- package/dist/index-B1keaX5Y.d.mts.map +1 -0
- package/dist/{index-D60_SzHG.d.mts → index-DR56od45.d.mts} +3 -3
- package/dist/{index-D60_SzHG.d.mts.map → index-DR56od45.d.mts.map} +1 -1
- package/dist/index.d.mts +17 -17
- package/dist/index.mjs +46 -46
- package/dist/{load-6ZrRhepW.mjs → load-BBetCvLC.mjs} +2 -2
- package/dist/{load-6ZrRhepW.mjs.map → load-BBetCvLC.mjs.map} +1 -1
- package/dist/{loader-Dyx8dhFV.mjs → loader-ZN1ll-d-.mjs} +36 -37
- package/dist/loader-ZN1ll-d-.mjs.map +1 -0
- package/dist/{manifest-schema-Cj-YrzrF.mjs → manifest-schema-BtwbL_vj.mjs} +55 -2
- package/dist/manifest-schema-BtwbL_vj.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +11 -11
- package/dist/media/local-runtime.mjs +6 -6
- package/dist/{media-C-oovGCG.mjs → media-JOf3pNkw.mjs} +2 -2
- package/dist/{media-C-oovGCG.mjs.map → media-JOf3pNkw.mjs.map} +1 -1
- package/dist/{media-allowlist-CMcoYIjQ.mjs → media-allowlist-Dknq-OFY.mjs} +1 -1
- package/dist/{media-allowlist-CMcoYIjQ.mjs.map → media-allowlist-Dknq-OFY.mjs.map} +1 -1
- package/dist/media-url-VClf8glU.mjs +26 -0
- package/dist/media-url-VClf8glU.mjs.map +1 -0
- package/dist/{menus-DugoYwTX.mjs → menus-DX4_E01q.mjs} +3 -3
- package/dist/{menus-DugoYwTX.mjs.map → menus-DX4_E01q.mjs.map} +1 -1
- package/dist/{menus-BKkxXCmd.mjs → menus-DrQLusqj.mjs} +87 -37
- package/dist/menus-DrQLusqj.mjs.map +1 -0
- package/dist/{mode-BjlXswIw.mjs → mode-CO2vQHfq.mjs} +1 -1
- package/dist/{mode-BjlXswIw.mjs.map → mode-CO2vQHfq.mjs.map} +1 -1
- package/dist/{normalize-DVV8nbrL.mjs → normalize-CK5o04zr.mjs} +2 -2
- package/dist/{normalize-DVV8nbrL.mjs.map → normalize-CK5o04zr.mjs.map} +1 -1
- package/dist/{oauth-authorization-DvBAL75d.mjs → oauth-authorization-Bw4NdF_S.mjs} +5 -5
- package/dist/{oauth-authorization-DvBAL75d.mjs.map → oauth-authorization-Bw4NdF_S.mjs.map} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs → oauth-clients-BGGFp57s.mjs} +1 -1
- package/dist/{oauth-clients-8mPDStMv.mjs.map → oauth-clients-BGGFp57s.mjs.map} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs → oauth-state-store-97x0xtN2.mjs} +1 -1
- package/dist/{oauth-state-store-BJ7YtrfD.mjs.map → oauth-state-store-97x0xtN2.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs → oauth-user-lookup-B_vnZHKO.mjs} +1 -1
- package/dist/{oauth-user-lookup-BdDSDvjF.mjs.map → oauth-user-lookup-B_vnZHKO.mjs.map} +1 -1
- package/dist/{options-BL4X94qY.mjs → options-BPCVnesz.mjs} +1 -1
- package/dist/{options-BL4X94qY.mjs.map → options-BPCVnesz.mjs.map} +1 -1
- package/dist/{options-tb7DJROi.d.mts → options-DyYIYpPd.d.mts} +3 -3
- package/dist/{options-tb7DJROi.d.mts.map → options-DyYIYpPd.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-BBkFmLVr.mjs → parse-CrGndy1A.mjs} +2 -2
- package/dist/{parse-BBkFmLVr.mjs.map → parse-CrGndy1A.mjs.map} +1 -1
- package/dist/{passkey-config-BDVM86Tj.mjs → passkey-config-C3QgnQnU.mjs} +1 -1
- package/dist/{passkey-config-BDVM86Tj.mjs.map → passkey-config-C3QgnQnU.mjs.map} +1 -1
- package/dist/{patterns-CqG5Ya3i.mjs → patterns-p-RBdTbM.mjs} +1 -1
- package/dist/{patterns-CqG5Ya3i.mjs.map → patterns-p-RBdTbM.mjs.map} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts → placeholder-CVBv5z8k.d.mts} +1 -1
- package/dist/{placeholder-B9lUUEmj.d.mts.map → placeholder-CVBv5z8k.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-egRHCy1m.mjs → public-url-BFVC2OTJ.mjs} +1 -1
- package/dist/{public-url-egRHCy1m.mjs.map → public-url-BFVC2OTJ.mjs.map} +1 -1
- package/dist/{query-Ctlq1aOk.mjs → query-CbUcI4Xk.mjs} +33 -17
- package/dist/query-CbUcI4Xk.mjs.map +1 -0
- package/dist/{rate-limit-CH6W6ikK.mjs → rate-limit-C7hjdkS5.mjs} +2 -2
- package/dist/{rate-limit-CH6W6ikK.mjs.map → rate-limit-C7hjdkS5.mjs.map} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs → redirect-B_q19j4v.mjs} +1 -1
- package/dist/{redirect-Cw3JTlmj.mjs.map → redirect-B_q19j4v.mjs.map} +1 -1
- package/dist/{redirect-C6tJA7tk.mjs → redirect-CRWIt8Zj.mjs} +3 -3
- package/dist/{redirect-C6tJA7tk.mjs.map → redirect-CRWIt8Zj.mjs.map} +1 -1
- package/dist/{redirects-C0L9JUk4.mjs → redirects-CCbCqCCd.mjs} +28 -4
- package/dist/redirects-CCbCqCCd.mjs.map +1 -0
- package/dist/{redirects-CacE9eQa.mjs → redirects-DxVoR7PI.mjs} +5 -5
- package/dist/{redirects-CacE9eQa.mjs.map → redirects-DxVoR7PI.mjs.map} +1 -1
- package/dist/{registry-CIDxZbhh.mjs → registry-brYh-rAT.mjs} +6 -6
- package/dist/{registry-CIDxZbhh.mjs.map → registry-brYh-rAT.mjs.map} +1 -1
- package/dist/{request-cache-BYMs-BGX.mjs → request-cache-D32LpnmI.mjs} +1 -1
- package/dist/{request-cache-BYMs-BGX.mjs.map → request-cache-D32LpnmI.mjs.map} +1 -1
- package/dist/request-context.d.mts +7 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +2 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-pt6Wl-l-.mjs → runner--4wMWwKM.mjs} +217 -166
- package/dist/runner--4wMWwKM.mjs.map +1 -0
- package/dist/{runner-DM1yR5qd.d.mts → runner-DTdhuI9i.d.mts} +2 -2
- package/dist/{runner-DM1yR5qd.d.mts.map → runner-DTdhuI9i.d.mts.map} +1 -1
- package/dist/runtime.d.mts +10 -10
- package/dist/runtime.mjs +2 -2
- package/dist/{schema-B4tk0HAG.mjs → schema-C1E70ug_.mjs} +5 -5
- package/dist/{schema-B4tk0HAG.mjs.map → schema-C1E70ug_.mjs.map} +1 -1
- package/dist/{search-f-fNfwab.mjs → search-B3SGZw91.mjs} +4 -4
- package/dist/{search-f-fNfwab.mjs.map → search-B3SGZw91.mjs.map} +1 -1
- package/dist/{secrets-YYbTgB1w.mjs → secrets-ChPTmy9x.mjs} +2 -2
- package/dist/{secrets-YYbTgB1w.mjs.map → secrets-ChPTmy9x.mjs.map} +1 -1
- package/dist/{sections-biElLfT9.mjs → sections-D_lVzwRZ.mjs} +3 -3
- package/dist/{sections-biElLfT9.mjs.map → sections-D_lVzwRZ.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +17 -17
- package/dist/seo/index.d.mts +1 -1
- package/dist/seo/index.d.mts.map +1 -1
- package/dist/seo/index.mjs +3 -12
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-BR39kvTF.mjs → seo-B5e6y9Wk.mjs} +2 -2
- package/dist/{seo-BR39kvTF.mjs.map → seo-B5e6y9Wk.mjs.map} +1 -1
- package/dist/{seo-DfjLvu8i.mjs → seo-D_LPkOtu.mjs} +4 -3
- package/dist/seo-D_LPkOtu.mjs.map +1 -0
- package/dist/{service-BhR2acnc.mjs → service-ChDcsTBs.mjs} +3 -3
- package/dist/{service-BhR2acnc.mjs.map → service-ChDcsTBs.mjs.map} +1 -1
- package/dist/{settings-D_NJvjgN.mjs → settings-Cv47v9u8.mjs} +3 -3
- package/dist/{settings-D_NJvjgN.mjs.map → settings-Cv47v9u8.mjs.map} +1 -1
- package/dist/settings-DfxiWY_s.mjs +411 -0
- package/dist/settings-DfxiWY_s.mjs.map +1 -0
- package/dist/{setup-complete-VoEZfasi.mjs → setup-complete-yvPE4OsP.mjs} +2 -2
- package/dist/{setup-complete-VoEZfasi.mjs.map → setup-complete-yvPE4OsP.mjs.map} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs → setup-nonce-C9aFzb94.mjs} +1 -1
- package/dist/{setup-nonce-Bm0uKqmf.mjs.map → setup-nonce-C9aFzb94.mjs.map} +1 -1
- package/dist/{site-url-Cm8-sJy7.mjs → site-url-CnHlmAs9.mjs} +2 -2
- package/dist/{site-url-Cm8-sJy7.mjs.map → site-url-CnHlmAs9.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/{taxonomies-Mhn9rjTQ.mjs → taxonomies-BILwiyGk.mjs} +4 -4
- package/dist/{taxonomies-Mhn9rjTQ.mjs.map → taxonomies-BILwiyGk.mjs.map} +1 -1
- package/dist/{taxonomies-Crtzy4MT.mjs → taxonomies-BdAmbOwx.mjs} +50 -12
- package/dist/taxonomies-BdAmbOwx.mjs.map +1 -0
- package/dist/{taxonomy-DTZrIQpi.mjs → taxonomy-CdllE4oq.mjs} +3 -3
- package/dist/{taxonomy-DTZrIQpi.mjs.map → taxonomy-CdllE4oq.mjs.map} +1 -1
- package/dist/{transaction-NQj4VJ7Z.mjs → transaction-x2tJQ-A1.mjs} +1 -1
- package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-x2tJQ-A1.mjs.map} +1 -1
- package/dist/{transport-OnMNbsIA.d.mts → transport-B7PPP2CC.d.mts} +1 -1
- package/dist/{transport-OnMNbsIA.d.mts.map → transport-B7PPP2CC.d.mts.map} +1 -1
- package/dist/{transport--Ck3RBin.mjs → transport-CmpLD7W3.mjs} +1 -1
- package/dist/{transport--Ck3RBin.mjs.map → transport-CmpLD7W3.mjs.map} +1 -1
- package/dist/{types-DWnN7weG.d.mts → types-BFgrqwSk.d.mts} +1 -1
- package/dist/{types-DWnN7weG.d.mts.map → types-BFgrqwSk.d.mts.map} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts → types-BH8-30hc.d.mts} +1 -1
- package/dist/{types-Qa7-HJJC.d.mts.map → types-BH8-30hc.d.mts.map} +1 -1
- package/dist/{types-DawhLFwy.d.mts → types-BPzXTV9x.d.mts} +26 -1
- package/dist/{types-DawhLFwy.d.mts.map → types-BPzXTV9x.d.mts.map} +1 -1
- package/dist/{types-DbCWhHet.d.mts → types-BUUVn1zr.d.mts} +2 -2
- package/dist/types-BUUVn1zr.d.mts.map +1 -0
- package/dist/{types-K3MDsxpy.mjs → types-BXSUSAjt.mjs} +16 -3
- package/dist/{types-K3MDsxpy.mjs.map → types-BXSUSAjt.mjs.map} +1 -1
- package/dist/{types-DMwSpvcw.d.mts → types-CPAPl93j.d.mts} +9 -3
- package/dist/{types-DMwSpvcw.d.mts.map → types-CPAPl93j.d.mts.map} +1 -1
- package/dist/types-CZI4E3qG.mjs +3 -0
- package/dist/{types-kwqCOUxj.d.mts → types-D4kUqbHh.d.mts} +1 -1
- package/dist/{types-kwqCOUxj.d.mts.map → types-D4kUqbHh.d.mts.map} +1 -1
- package/dist/{types-i8_uzhMD.d.mts → types-DTniiNto.d.mts} +19 -4
- package/dist/types-DTniiNto.d.mts.map +1 -0
- package/dist/{types-D8bhH891.mjs → types-DZk_y-MU.mjs} +1 -1
- package/dist/types-DZk_y-MU.mjs.map +1 -0
- package/dist/{types-DX6v9KzJ.d.mts → types-S15DXXNi.d.mts} +1 -1
- package/dist/{types-DX6v9KzJ.d.mts.map → types-S15DXXNi.d.mts.map} +1 -1
- package/dist/{user-DzEUl5zA.mjs → user-C0um7wrg.mjs} +18 -2
- package/dist/user-C0um7wrg.mjs.map +1 -0
- package/dist/{validate-JCXcsqiY.mjs → validate-Bz4vqcX1.mjs} +6 -3
- package/dist/validate-Bz4vqcX1.mjs.map +1 -0
- package/dist/{validate-Dy6nkNls.d.mts → validate-CNwkPWzz.d.mts} +13 -5
- package/dist/validate-CNwkPWzz.d.mts.map +1 -0
- package/dist/{validation-Bq-VyKJg.mjs → validation-DgGTJm3u.mjs} +5 -5
- package/dist/{validation-Bq-VyKJg.mjs.map → validation-DgGTJm3u.mjs.map} +1 -1
- package/dist/version-D-5txk2m.mjs +7 -0
- package/dist/{version-CnS-Cr8A.mjs.map → version-D-5txk2m.mjs.map} +1 -1
- package/dist/{widgets-Bap1eS1X.mjs → widgets-DZfmAbE4.mjs} +47 -44
- package/dist/widgets-DZfmAbE4.mjs.map +1 -0
- package/dist/{zod-generator-BSDpkqSH.mjs → zod-generator-Djo_VHCt.mjs} +2 -2
- package/dist/{zod-generator-BSDpkqSH.mjs.map → zod-generator-Djo_VHCt.mjs.map} +1 -1
- package/package.json +10 -10
- package/src/api/handlers/content.ts +107 -8
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/marketplace.ts +2 -5
- package/src/api/handlers/registry.ts +70 -0
- package/src/api/handlers/seo.ts +9 -1
- package/src/api/openapi/document.ts +25 -0
- package/src/api/schemas/content.ts +33 -0
- package/src/api/schemas/schema.ts +13 -1
- package/src/astro/integration/index.ts +98 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/virtual-modules.ts +39 -0
- package/src/astro/integration/vite-config.ts +12 -0
- package/src/astro/middleware.ts +48 -6
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id].ts +4 -2
- package/src/astro/routes/api/content/[collection]/authors.ts +34 -0
- package/src/astro/routes/api/schema/index.ts +7 -15
- package/src/astro/routes/sitemap-[collection].xml.ts +13 -2
- package/src/astro/types.ts +8 -1
- package/src/bylines/index.ts +57 -0
- package/src/cli/commands/bundle-utils.ts +2 -0
- package/src/cli/commands/export-seed.ts +28 -12
- package/src/cli/commands/secrets.ts +2 -2
- package/src/components/EmDashImage.astro +22 -4
- package/src/components/Image.astro +20 -3
- package/src/database/instrumentation.ts +13 -0
- package/src/database/migrations/043_content_references.ts +121 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +225 -67
- package/src/database/repositories/index.ts +7 -0
- package/src/database/repositories/relation.ts +467 -0
- package/src/database/repositories/types.ts +31 -0
- package/src/database/repositories/user.ts +18 -0
- package/src/database/types.ts +34 -0
- package/src/emdash-runtime.ts +172 -67
- package/src/index.ts +8 -1
- package/src/loader.ts +81 -39
- package/src/media/responsive.ts +125 -0
- package/src/plugins/cron.ts +3 -2
- package/src/plugins/index.ts +5 -0
- package/src/plugins/manifest-schema.ts +75 -0
- package/src/plugins/marketplace.ts +2 -5
- package/src/plugins/scheduler/node.ts +9 -2
- package/src/plugins/types.ts +12 -0
- package/src/query.ts +45 -7
- package/src/request-context.ts +8 -0
- package/src/scheduled-publish.ts +153 -0
- package/src/schema/types.ts +11 -1
- package/src/seed/apply.ts +16 -6
- package/src/seed/types.ts +9 -0
- package/src/seed/validate.ts +15 -0
- package/src/seo/index.ts +2 -28
- package/src/seo/media-url.ts +32 -0
- package/src/settings/index.ts +32 -40
- package/src/taxonomies/index.ts +79 -12
- package/src/utils/isolate-cache.ts +189 -0
- package/src/virtual-modules.d.ts +11 -0
- package/src/widgets/index.ts +57 -54
- package/dist/api-Cs7DAACP.mjs.map +0 -1
- package/dist/apply-BWMV4Zmw.mjs.map +0 -1
- package/dist/byline-fields-BNy7Ng1U.d.mts.map +0 -1
- package/dist/content-CyqOmOzm.mjs.map +0 -1
- package/dist/cron-DZovZUnC.mjs.map +0 -1
- package/dist/index-CjKdMZ3U.d.mts.map +0 -1
- package/dist/loader-Dyx8dhFV.mjs.map +0 -1
- package/dist/manifest-schema-Cj-YrzrF.mjs.map +0 -1
- package/dist/menus-BKkxXCmd.mjs.map +0 -1
- package/dist/query-Ctlq1aOk.mjs.map +0 -1
- package/dist/redirects-C0L9JUk4.mjs.map +0 -1
- package/dist/runner-pt6Wl-l-.mjs.map +0 -1
- package/dist/seo-DfjLvu8i.mjs.map +0 -1
- package/dist/settings-b5zW1R1T.mjs +0 -235
- package/dist/settings-b5zW1R1T.mjs.map +0 -1
- package/dist/taxonomies-Crtzy4MT.mjs.map +0 -1
- package/dist/types-Cj2S6FuC.mjs +0 -3
- package/dist/types-D8bhH891.mjs.map +0 -1
- package/dist/types-DbCWhHet.d.mts.map +0 -1
- package/dist/types-i8_uzhMD.d.mts.map +0 -1
- package/dist/user-DzEUl5zA.mjs.map +0 -1
- package/dist/validate-Dy6nkNls.d.mts.map +0 -1
- package/dist/validate-JCXcsqiY.mjs.map +0 -1
- package/dist/version-CnS-Cr8A.mjs +0 -7
- package/dist/widgets-Bap1eS1X.mjs.map +0 -1
- package/src/plugins/scheduler/piggyback.ts +0 -71
- /package/dist/{api-tokens-B6VgoE6M.mjs → api-tokens-Oq39ba-Z.mjs} +0 -0
package/dist/seo/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/seo/index.ts"],"sourcesContent":["/**\n * SEO Helpers\n *\n * Public API functions for generating SEO meta tags in Astro templates.\n *\n * @example\n * ```astro\n * ---\n * import { getEmDashEntry } from \"emdash\";\n * import { getSeoMeta } from \"emdash/seo\";\n *\n * const post = await getEmDashEntry(\"posts\", Astro.params.slug);\n * const meta = await getSeoMeta(post, {\n * siteTitle: \"My Blog\",\n * siteUrl: Astro.url.origin,\n * });\n * ---\n * <html>\n * <head>\n * <title>{meta.title}</title>\n * <meta name=\"description\" content={meta.description} />\n * <meta property=\"og:title\" content={meta.ogTitle} />\n * <meta property=\"og:description\" content={meta.ogDescription} />\n * {meta.ogImage && <meta property=\"og:image\" content={meta.ogImage} />}\n * <link rel=\"canonical\" href={meta.canonical} />\n * {meta.robots && <meta name=\"robots\" content={meta.robots} />}\n * </head>\n * </html>\n * ```\n */\n\nimport type { ContentSeo } from \"../database/repositories/types.js\";\n\nconst TRAILING_SLASH_RE = /\\/$/;\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i;\n\n/**\n * Content input for SEO functions.\n * Accepts both ContentEntry<T> (from query functions) and ContentItem (internal).\n */\nexport interface SeoContentInput<T = Record<string, unknown>> {\n\t/** Content data object */\n\tdata: T & {\n\t\ttitle?: unknown;\n\t\texcerpt?: unknown;\n\t\tseo?: ContentSeo;\n\t};\n\t/** SEO metadata (legacy location, prefer data.seo) */\n\tseo?: ContentSeo;\n}\n\n/** Resolved SEO meta tags ready for use in templates */\nexport interface SeoMeta {\n\t/** Full <title> tag content (e.g., \"Post Title | Site Name\") */\n\ttitle: string;\n\t/** Meta description */\n\tdescription: string | null;\n\t/** OG title (same as title by default) */\n\togTitle: string;\n\t/** OG description */\n\togDescription: string | null;\n\t/** OG image URL (absolute) */\n\togImage: string | null;\n\t/** Canonical URL */\n\tcanonical: string | null;\n\t/** Robots directive (e.g., \"noindex, nofollow\") or null if default */\n\trobots: string | null;\n}\n\n/** Options for generating SEO meta from a content item */\nexport interface SeoMetaOptions {\n\t/** Site title for the suffix (e.g., \"My Blog\") */\n\tsiteTitle?: string;\n\t/** Site URL origin for building absolute URLs (e.g., \"https://example.com\") */\n\tsiteUrl?: string;\n\t/** Title separator between page title and site title */\n\ttitleSeparator?: string;\n\t/** Path to this content (e.g., \"/posts/my-post\") for canonical fallback */\n\tpath?: string;\n\t/** Default OG image URL if content has none */\n\tdefaultOgImage?: string;\n}\n\n/**\n * Generate resolved SEO meta tags from a content item.\n *\n * Uses the content item's SEO fields, falling back to content data\n * (title from `data.title`, description from `data.excerpt`).\n *\n * @param content - The content item (from getEmDashEntry, etc.)\n * @param options - Configuration for title construction, canonical URLs, etc.\n * @returns Resolved meta tags ready for template use\n */\nexport function getSeoMeta<T>(content: SeoContentInput<T>, options: SeoMetaOptions = {}): SeoMeta {\n\tconst { siteTitle, siteUrl, path, defaultOgImage } = options;\n\tconst separator = options.titleSeparator || \" | \";\n\t// SEO can be in content.seo (ContentItem) or content.data.seo (ContentEntry)\n\tconst seo = content.seo ??\n\t\tcontent.data.seo ?? {\n\t\t\ttitle: null,\n\t\t\tdescription: null,\n\t\t\timage: null,\n\t\t\tcanonical: null,\n\t\t\tnoIndex: false,\n\t\t};\n\n\t// Title: SEO title > content title > fallback\n\tconst pageTitle =\n\t\tseo.title || (typeof content.data.title === \"string\" ? content.data.title : null) || \"\";\n\n\tconst fullTitle = siteTitle && pageTitle ? `${pageTitle}${separator}${siteTitle}` : pageTitle;\n\n\t// Description: SEO description > excerpt\n\tconst description =\n\t\tseo.description ||\n\t\t(typeof content.data.excerpt === \"string\" ? content.data.excerpt : null) ||\n\t\tnull;\n\n\t// OG image: SEO image > default\n\tconst ogImage = seo.image ?
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/seo/index.ts"],"sourcesContent":["/**\n * SEO Helpers\n *\n * Public API functions for generating SEO meta tags in Astro templates.\n *\n * @example\n * ```astro\n * ---\n * import { getEmDashEntry } from \"emdash\";\n * import { getSeoMeta } from \"emdash/seo\";\n *\n * const post = await getEmDashEntry(\"posts\", Astro.params.slug);\n * const meta = await getSeoMeta(post, {\n * siteTitle: \"My Blog\",\n * siteUrl: Astro.url.origin,\n * });\n * ---\n * <html>\n * <head>\n * <title>{meta.title}</title>\n * <meta name=\"description\" content={meta.description} />\n * <meta property=\"og:title\" content={meta.ogTitle} />\n * <meta property=\"og:description\" content={meta.ogDescription} />\n * {meta.ogImage && <meta property=\"og:image\" content={meta.ogImage} />}\n * <link rel=\"canonical\" href={meta.canonical} />\n * {meta.robots && <meta name=\"robots\" content={meta.robots} />}\n * </head>\n * </html>\n * ```\n */\n\nimport type { ContentSeo } from \"../database/repositories/types.js\";\nimport { buildSeoImageUrl } from \"./media-url.js\";\n\nconst TRAILING_SLASH_RE = /\\/$/;\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i;\n\n/**\n * Content input for SEO functions.\n * Accepts both ContentEntry<T> (from query functions) and ContentItem (internal).\n */\nexport interface SeoContentInput<T = Record<string, unknown>> {\n\t/** Content data object */\n\tdata: T & {\n\t\ttitle?: unknown;\n\t\texcerpt?: unknown;\n\t\tseo?: ContentSeo;\n\t};\n\t/** SEO metadata (legacy location, prefer data.seo) */\n\tseo?: ContentSeo;\n}\n\n/** Resolved SEO meta tags ready for use in templates */\nexport interface SeoMeta {\n\t/** Full <title> tag content (e.g., \"Post Title | Site Name\") */\n\ttitle: string;\n\t/** Meta description */\n\tdescription: string | null;\n\t/** OG title (same as title by default) */\n\togTitle: string;\n\t/** OG description */\n\togDescription: string | null;\n\t/** OG image URL (absolute) */\n\togImage: string | null;\n\t/** Canonical URL */\n\tcanonical: string | null;\n\t/** Robots directive (e.g., \"noindex, nofollow\") or null if default */\n\trobots: string | null;\n}\n\n/** Options for generating SEO meta from a content item */\nexport interface SeoMetaOptions {\n\t/** Site title for the suffix (e.g., \"My Blog\") */\n\tsiteTitle?: string;\n\t/** Site URL origin for building absolute URLs (e.g., \"https://example.com\") */\n\tsiteUrl?: string;\n\t/** Title separator between page title and site title */\n\ttitleSeparator?: string;\n\t/** Path to this content (e.g., \"/posts/my-post\") for canonical fallback */\n\tpath?: string;\n\t/** Default OG image URL if content has none */\n\tdefaultOgImage?: string;\n}\n\n/**\n * Generate resolved SEO meta tags from a content item.\n *\n * Uses the content item's SEO fields, falling back to content data\n * (title from `data.title`, description from `data.excerpt`).\n *\n * @param content - The content item (from getEmDashEntry, etc.)\n * @param options - Configuration for title construction, canonical URLs, etc.\n * @returns Resolved meta tags ready for template use\n */\nexport function getSeoMeta<T>(content: SeoContentInput<T>, options: SeoMetaOptions = {}): SeoMeta {\n\tconst { siteTitle, siteUrl, path, defaultOgImage } = options;\n\tconst separator = options.titleSeparator || \" | \";\n\t// SEO can be in content.seo (ContentItem) or content.data.seo (ContentEntry)\n\tconst seo = content.seo ??\n\t\tcontent.data.seo ?? {\n\t\t\ttitle: null,\n\t\t\tdescription: null,\n\t\t\timage: null,\n\t\t\tcanonical: null,\n\t\t\tnoIndex: false,\n\t\t};\n\n\t// Title: SEO title > content title > fallback\n\tconst pageTitle =\n\t\tseo.title || (typeof content.data.title === \"string\" ? content.data.title : null) || \"\";\n\n\tconst fullTitle = siteTitle && pageTitle ? `${pageTitle}${separator}${siteTitle}` : pageTitle;\n\n\t// Description: SEO description > excerpt\n\tconst description =\n\t\tseo.description ||\n\t\t(typeof content.data.excerpt === \"string\" ? content.data.excerpt : null) ||\n\t\tnull;\n\n\t// OG image: SEO image > default\n\tconst ogImage = seo.image ? buildSeoImageUrl(seo.image, siteUrl) : (defaultOgImage ?? null);\n\n\t// Canonical: explicit > path-based > null\n\tlet canonical: string | null = null;\n\tif (seo.canonical) {\n\t\t// Ensure relative canonical paths get a leading slash so we don't\n\t\t// produce \"https://example.composts/x\" when joined with siteUrl\n\t\tif (siteUrl && !seo.canonical.startsWith(\"/\") && !ABSOLUTE_URL_RE.test(seo.canonical)) {\n\t\t\tcanonical = `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}/${seo.canonical}`;\n\t\t} else {\n\t\t\tcanonical = seo.canonical;\n\t\t}\n\t} else if (siteUrl && path) {\n\t\tconst safePath = path.startsWith(\"/\") ? path : `/${path}`;\n\t\tcanonical = `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}${safePath}`;\n\t}\n\n\t// Robots\n\tconst robots = seo.noIndex ? \"noindex, nofollow\" : null;\n\n\treturn {\n\t\ttitle: fullTitle,\n\t\tdescription,\n\t\togTitle: pageTitle || fullTitle,\n\t\togDescription: description,\n\t\togImage,\n\t\tcanonical,\n\t\trobots,\n\t};\n}\n\n/**\n * Extract SEO data from a content item.\n *\n * Convenience accessor for the raw SEO fields without template resolution.\n *\n * @param content - The content item\n * @returns The content's SEO fields\n */\nexport function getContentSeo<T>(content: SeoContentInput<T>): ContentSeo | undefined {\n\treturn content.seo ?? content.data.seo;\n}\n"],"mappings":";;;AAkCA,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;;;;;;;;;;;AA2DxB,SAAgB,WAAc,SAA6B,UAA0B,EAAE,EAAW;CACjG,MAAM,EAAE,WAAW,SAAS,MAAM,mBAAmB;CACrD,MAAM,YAAY,QAAQ,kBAAkB;CAE5C,MAAM,MAAM,QAAQ,OACnB,QAAQ,KAAK,OAAO;EACnB,OAAO;EACP,aAAa;EACb,OAAO;EACP,WAAW;EACX,SAAS;EACT;CAGF,MAAM,YACL,IAAI,UAAU,OAAO,QAAQ,KAAK,UAAU,WAAW,QAAQ,KAAK,QAAQ,SAAS;CAEtF,MAAM,YAAY,aAAa,YAAY,GAAG,YAAY,YAAY,cAAc;CAGpF,MAAM,cACL,IAAI,gBACH,OAAO,QAAQ,KAAK,YAAY,WAAW,QAAQ,KAAK,UAAU,SACnE;CAGD,MAAM,UAAU,IAAI,QAAQ,iBAAiB,IAAI,OAAO,QAAQ,GAAI,kBAAkB;CAGtF,IAAI,YAA2B;AAC/B,KAAI,IAAI,UAGP,KAAI,WAAW,CAAC,IAAI,UAAU,WAAW,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI,UAAU,CACpF,aAAY,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,CAAC,GAAG,IAAI;KAE7D,aAAY,IAAI;UAEP,WAAW,MAAM;EAC3B,MAAM,WAAW,KAAK,WAAW,IAAI,GAAG,OAAO,IAAI;AACnD,cAAY,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,GAAG;;CAIzD,MAAM,SAAS,IAAI,UAAU,sBAAsB;AAEnD,QAAO;EACN,OAAO;EACP;EACA,SAAS,aAAa;EACtB,eAAe;EACf;EACA;EACA;EACA;;;;;;;;;;AAWF,SAAgB,cAAiB,SAAqD;AACrF,QAAO,QAAQ,OAAO,QAAQ,KAAK"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-
|
|
1
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BerYVuve.mjs";
|
|
2
2
|
import { sql } from "kysely";
|
|
3
3
|
|
|
4
4
|
//#region src/database/repositories/seo.ts
|
|
@@ -127,4 +127,4 @@ var SeoRepository = class {
|
|
|
127
127
|
|
|
128
128
|
//#endregion
|
|
129
129
|
export { SeoRepository as t };
|
|
130
|
-
//# sourceMappingURL=seo-
|
|
130
|
+
//# sourceMappingURL=seo-B5e6y9Wk.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-
|
|
1
|
+
{"version":3,"file":"seo-B5e6y9Wk.mjs","names":[],"sources":["../src/database/repositories/seo.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\n\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\nimport type { ContentSeo, ContentSeoInput } from \"./types.js\";\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Returns true if the input has at least one explicitly-set SEO field.\n * Used to skip no-op upserts when callers pass `{ seo: {} }`.\n */\nfunction hasAnyField(input: ContentSeoInput): boolean {\n\treturn (\n\t\tinput.title !== undefined ||\n\t\tinput.description !== undefined ||\n\t\tinput.image !== undefined ||\n\t\tinput.canonical !== undefined ||\n\t\tinput.noIndex !== undefined\n\t);\n}\n\n/**\n * Repository for SEO metadata stored in `_emdash_seo`.\n *\n * SEO data lives in a separate table keyed by (collection, content_id).\n * Only collections with `has_seo = 1` should use this — callers are\n * responsible for checking the flag before reading/writing.\n */\nexport class SeoRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Check whether a collection has SEO enabled (`has_seo = 1`).\n\t * Returns `false` if the collection does not exist.\n\t */\n\tasync isEnabled(collection: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"has_seo\")\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\t\treturn row?.has_seo === 1;\n\t}\n\n\t/**\n\t * Get SEO data for a content item. Returns null defaults if no row exists.\n\t */\n\tasync get(collection: string, contentId: string): Promise<ContentSeo> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn { ...SEO_DEFAULTS };\n\t\t}\n\n\t\treturn {\n\t\t\ttitle: row.seo_title ?? null,\n\t\t\tdescription: row.seo_description ?? null,\n\t\t\timage: row.seo_image ?? null,\n\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t};\n\t}\n\n\t/**\n\t * Get SEO data for multiple content items.\n\t * Returns a Map keyed by content_id. Items without SEO rows get defaults.\n\t *\n\t * Chunks the `content_id IN (…)` clause so the total bound-parameter count\n\t * per statement (ids + the `collection = ?` filter) stays within Cloudflare\n\t * D1's 100-variable limit regardless of how many content items are passed.\n\t */\n\tasync getMany(collection: string, contentIds: string[]): Promise<Map<string, ContentSeo>> {\n\t\tconst result = new Map<string, ContentSeo>();\n\n\t\tif (contentIds.length === 0) return result;\n\n\t\t// Pre-fill with defaults so every input id has an entry even if no row exists.\n\t\tfor (const id of contentIds) {\n\t\t\tresult.set(id, { ...SEO_DEFAULTS });\n\t\t}\n\n\t\tconst uniqueContentIds = [...new Set(contentIds)];\n\t\tfor (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t\t.where(\"content_id\", \"in\", chunk)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tresult.set(row.content_id, {\n\t\t\t\t\ttitle: row.seo_title ?? null,\n\t\t\t\t\tdescription: row.seo_description ?? null,\n\t\t\t\t\timage: row.seo_image ?? null,\n\t\t\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Upsert SEO data for a content item using INSERT ON CONFLICT DO UPDATE\n\t * for atomicity. Skips no-op writes when input has no fields set.\n\t */\n\tasync upsert(collection: string, contentId: string, input: ContentSeoInput): Promise<ContentSeo> {\n\t\t// Skip no-op: empty input (e.g., `{ seo: {} }` from form libs)\n\t\tif (!hasAnyField(input)) {\n\t\t\treturn this.get(collection, contentId);\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// Use INSERT ON CONFLICT for atomic upsert — avoids TOCTOU race\n\t\t// where two concurrent requests both see \"no row\" and both try INSERT.\n\t\t//\n\t\t// On conflict, we use COALESCE(excluded.col, current.col) so that\n\t\t// only explicitly-provided fields overwrite existing values.\n\t\tawait sql`\n\t\t\tINSERT INTO _emdash_seo (\n\t\t\t\tcollection, content_id,\n\t\t\t\tseo_title, seo_description, seo_image, seo_canonical, seo_no_index,\n\t\t\t\tcreated_at, updated_at\n\t\t\t) VALUES (\n\t\t\t\t${collection}, ${contentId},\n\t\t\t\t${input.title ?? null}, ${input.description ?? null},\n\t\t\t\t${input.image ?? null}, ${input.canonical ?? null},\n\t\t\t\t${input.noIndex ? 1 : 0},\n\t\t\t\t${now}, ${now}\n\t\t\t)\n\t\t\tON CONFLICT (collection, content_id) DO UPDATE SET\n\t\t\t\tseo_title = ${input.title !== undefined ? sql`${input.title}` : sql`_emdash_seo.seo_title`},\n\t\t\t\tseo_description = ${input.description !== undefined ? sql`${input.description}` : sql`_emdash_seo.seo_description`},\n\t\t\t\tseo_image = ${input.image !== undefined ? sql`${input.image}` : sql`_emdash_seo.seo_image`},\n\t\t\t\tseo_canonical = ${input.canonical !== undefined ? sql`${input.canonical}` : sql`_emdash_seo.seo_canonical`},\n\t\t\t\tseo_no_index = ${input.noIndex !== undefined ? sql`${input.noIndex ? 1 : 0}` : sql`_emdash_seo.seo_no_index`},\n\t\t\t\tupdated_at = ${now}\n\t\t`.execute(this.db);\n\n\t\treturn this.get(collection, contentId);\n\t}\n\n\t/**\n\t * Delete SEO data for a content item.\n\t */\n\tasync delete(collection: string, contentId: string): Promise<void> {\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_seo\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Copy SEO data from one content item to another.\n\t * Used by duplicate. Clears canonical (it pointed to the original).\n\t */\n\tasync copyForDuplicate(collection: string, sourceId: string, targetId: string): Promise<void> {\n\t\tconst source = await this.get(collection, sourceId);\n\n\t\t// Only write if there's actual SEO data worth copying\n\t\tif (\n\t\t\tsource.title !== null ||\n\t\t\tsource.description !== null ||\n\t\t\tsource.image !== null ||\n\t\t\tsource.noIndex\n\t\t) {\n\t\t\tawait this.upsert(collection, targetId, {\n\t\t\t\ttitle: source.title,\n\t\t\t\tdescription: source.description,\n\t\t\t\timage: source.image,\n\t\t\t\tcanonical: null, // Don't copy canonical — it pointed to the original\n\t\t\t\tnoIndex: source.noIndex,\n\t\t\t});\n\t\t}\n\t}\n}\n"],"mappings":";;;;;AAOA,MAAM,eAA2B;CAChC,OAAO;CACP,aAAa;CACb,OAAO;CACP,WAAW;CACX,SAAS;CACT;;;;;AAMD,SAAS,YAAY,OAAiC;AACrD,QACC,MAAM,UAAU,UAChB,MAAM,gBAAgB,UACtB,MAAM,UAAU,UAChB,MAAM,cAAc,UACpB,MAAM,YAAY;;;;;;;;;AAWpB,IAAa,gBAAb,MAA2B;CAC1B,YAAY,AAAQ,IAAsB;EAAtB;;;;;;CAMpB,MAAM,UAAU,YAAsC;AAMrD,UALY,MAAM,KAAK,GACrB,WAAW,sBAAsB,CACjC,OAAO,UAAU,CACjB,MAAM,QAAQ,KAAK,WAAW,CAC9B,kBAAkB,GACR,YAAY;;;;;CAMzB,MAAM,IAAI,YAAoB,WAAwC;EACrE,MAAM,MAAM,MAAM,KAAK,GACrB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,KAAK,UAAU,CACnC,kBAAkB;AAEpB,MAAI,CAAC,IACJ,QAAO,EAAE,GAAG,cAAc;AAG3B,SAAO;GACN,OAAO,IAAI,aAAa;GACxB,aAAa,IAAI,mBAAmB;GACpC,OAAO,IAAI,aAAa;GACxB,WAAW,IAAI,iBAAiB;GAChC,SAAS,IAAI,iBAAiB;GAC9B;;;;;;;;;;CAWF,MAAM,QAAQ,YAAoB,YAAwD;EACzF,MAAM,yBAAS,IAAI,KAAyB;AAE5C,MAAI,WAAW,WAAW,EAAG,QAAO;AAGpC,OAAK,MAAM,MAAM,WAChB,QAAO,IAAI,IAAI,EAAE,GAAG,cAAc,CAAC;EAGpC,MAAM,mBAAmB,CAAC,GAAG,IAAI,IAAI,WAAW,CAAC;AACjD,OAAK,MAAM,SAAS,OAAO,kBAAkB,eAAe,EAAE;GAC7D,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,MAAM,MAAM,CAChC,SAAS;AAEX,QAAK,MAAM,OAAO,KACjB,QAAO,IAAI,IAAI,YAAY;IAC1B,OAAO,IAAI,aAAa;IACxB,aAAa,IAAI,mBAAmB;IACpC,OAAO,IAAI,aAAa;IACxB,WAAW,IAAI,iBAAiB;IAChC,SAAS,IAAI,iBAAiB;IAC9B,CAAC;;AAIJ,SAAO;;;;;;CAOR,MAAM,OAAO,YAAoB,WAAmB,OAA6C;AAEhG,MAAI,CAAC,YAAY,MAAM,CACtB,QAAO,KAAK,IAAI,YAAY,UAAU;EAGvC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAOpC,QAAM,GAAG;;;;;;MAML,WAAW,IAAI,UAAU;MACzB,MAAM,SAAS,KAAK,IAAI,MAAM,eAAe,KAAK;MAClD,MAAM,SAAS,KAAK,IAAI,MAAM,aAAa,KAAK;MAChD,MAAM,UAAU,IAAI,EAAE;MACtB,IAAI,IAAI,IAAI;;;kBAGA,MAAM,UAAU,SAAY,GAAG,GAAG,MAAM,UAAU,GAAG,wBAAwB;wBACvE,MAAM,gBAAgB,SAAY,GAAG,GAAG,MAAM,gBAAgB,GAAG,8BAA8B;kBACrG,MAAM,UAAU,SAAY,GAAG,GAAG,MAAM,UAAU,GAAG,wBAAwB;sBACzE,MAAM,cAAc,SAAY,GAAG,GAAG,MAAM,cAAc,GAAG,4BAA4B;qBAC1F,MAAM,YAAY,SAAY,GAAG,GAAG,MAAM,UAAU,IAAI,MAAM,GAAG,2BAA2B;mBAC9F,IAAI;IACnB,QAAQ,KAAK,GAAG;AAElB,SAAO,KAAK,IAAI,YAAY,UAAU;;;;;CAMvC,MAAM,OAAO,YAAoB,WAAkC;AAClE,QAAM,KAAK,GACT,WAAW,cAAc,CACzB,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,KAAK,UAAU,CACnC,SAAS;;;;;;CAOZ,MAAM,iBAAiB,YAAoB,UAAkB,UAAiC;EAC7F,MAAM,SAAS,MAAM,KAAK,IAAI,YAAY,SAAS;AAGnD,MACC,OAAO,UAAU,QACjB,OAAO,gBAAgB,QACvB,OAAO,UAAU,QACjB,OAAO,QAEP,OAAM,KAAK,OAAO,YAAY,UAAU;GACvC,OAAO,OAAO;GACd,aAAa,OAAO;GACpB,OAAO,OAAO;GACd,WAAW;GACX,SAAS,OAAO;GAChB,CAAC"}
|
|
@@ -36,7 +36,7 @@ async function handleSitemapData(db, collectionSlug) {
|
|
|
36
36
|
const tableName = `ec_${col.slug}`;
|
|
37
37
|
try {
|
|
38
38
|
const rows = await sql`
|
|
39
|
-
SELECT c.slug, c.id, c.updated_at, c.locale, c.translation_group
|
|
39
|
+
SELECT c.slug, c.id, c.updated_at, c.locale, c.translation_group, s.seo_image
|
|
40
40
|
FROM ${sql.ref(tableName)} c
|
|
41
41
|
LEFT JOIN _emdash_seo s
|
|
42
42
|
ON s.collection = ${col.slug}
|
|
@@ -54,7 +54,8 @@ async function handleSitemapData(db, collectionSlug) {
|
|
|
54
54
|
slug: row.slug,
|
|
55
55
|
updatedAt: row.updated_at,
|
|
56
56
|
locale: row.locale,
|
|
57
|
-
translationGroup: row.translation_group
|
|
57
|
+
translationGroup: row.translation_group,
|
|
58
|
+
image: row.seo_image ?? null
|
|
58
59
|
});
|
|
59
60
|
result.push({
|
|
60
61
|
collection: col.slug,
|
|
@@ -85,4 +86,4 @@ async function handleSitemapData(db, collectionSlug) {
|
|
|
85
86
|
|
|
86
87
|
//#endregion
|
|
87
88
|
export { handleSitemapData as t };
|
|
88
|
-
//# sourceMappingURL=seo-
|
|
89
|
+
//# sourceMappingURL=seo-D_LPkOtu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-D_LPkOtu.mjs","names":[],"sources":["../src/api/handlers/seo.ts"],"sourcesContent":["/**\n * SEO Handlers\n *\n * Business logic for sitemap generation and robots.txt.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/** Raw content data for sitemap generation — the route builds the actual URLs */\nexport interface SitemapContentEntry {\n\t/** Content ID (ULID) */\n\tid: string;\n\t/** Content slug, or null when the entry has no slug */\n\tslug: string | null;\n\t/** ISO date of last modification */\n\tupdatedAt: string;\n\t/**\n\t * Locale of this row (e.g. `\"en\"`, `\"fr\"`). Always present — rows in\n\t * pre-i18n databases are backfilled to the configured `defaultLocale`.\n\t */\n\tlocale: string;\n\t/**\n\t * `translation_group` ULID shared across all locale variants of the\n\t * same content. Used by the sitemap route to emit `hreflang`\n\t * alternates between siblings.\n\t */\n\ttranslationGroup: string | null;\n\t/**\n\t * Stored SEO image reference (`_emdash_seo.seo_image`), or null when\n\t * the entry has no SEO image. The route resolves it to an absolute\n\t * URL and emits it as an `<image:image>` sitemap entry.\n\t */\n\timage: string | null;\n}\n\n/** Per-collection sitemap data with entries and URL pattern */\nexport interface SitemapCollectionData {\n\t/** Collection slug (e.g., \"post\", \"page\") */\n\tcollection: string;\n\t/** URL pattern with {slug} placeholder, or null for default /{collection}/{slug} */\n\turlPattern: string | null;\n\t/** Most recent updated_at across all entries (for sitemap index lastmod) */\n\tlastmod: string;\n\t/** Individual content entries */\n\tentries: SitemapContentEntry[];\n}\n\nexport interface SitemapDataResponse {\n\tcollections: SitemapCollectionData[];\n}\n\n/** Maximum entries per sitemap (per spec) */\nconst SITEMAP_MAX_ENTRIES = 50_000;\n\n/**\n * Collect all published, indexable content across SEO-enabled collections\n * for sitemap generation, grouped by collection.\n *\n * Only includes content from collections with `has_seo = 1`.\n * Excludes content with `seo_no_index = 1` in the `_emdash_seo` table.\n *\n * Returns raw data grouped per collection. The caller (route) is\n * responsible for building absolute URLs — this handler does NOT\n * assume a URL structure.\n */\nexport async function handleSitemapData(\n\tdb: Kysely<Database>,\n\t/** When set, only return data for this collection. */\n\tcollectionSlug?: string,\n): Promise<ApiResult<SitemapDataResponse>> {\n\ttry {\n\t\t// Find SEO-enabled collections (optionally filtered)\n\t\tlet query = db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"slug\", \"url_pattern\"])\n\t\t\t.where(\"has_seo\", \"=\", 1);\n\n\t\tif (collectionSlug) {\n\t\t\tquery = query.where(\"slug\", \"=\", collectionSlug);\n\t\t}\n\n\t\tconst collections = await query.execute();\n\n\t\tconst result: SitemapCollectionData[] = [];\n\n\t\tfor (const col of collections) {\n\t\t\t// Validate the slug before using it as a table name identifier.\n\t\t\t// Should always pass (slugs are validated on creation), but\n\t\t\t// guards against corrupted DB data.\n\t\t\ttry {\n\t\t\t\tvalidateIdentifier(col.slug, \"collection slug\");\n\t\t\t} catch {\n\t\t\t\tconsole.warn(`[SITEMAP] Skipping collection with invalid slug: ${col.slug}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst tableName = `ec_${col.slug}`;\n\n\t\t\t// Query published, non-deleted content.\n\t\t\t// LEFT JOIN _emdash_seo to check noindex flag.\n\t\t\t// Content without an SEO row is assumed indexable (default).\n\t\t\t// Wrapped in try/catch so a missing/broken table doesn't fail the\n\t\t\t// entire sitemap — we skip that collection and continue.\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{\n\t\t\t\t\tslug: string | null;\n\t\t\t\t\tid: string;\n\t\t\t\t\tupdated_at: string;\n\t\t\t\t\tlocale: string;\n\t\t\t\t\ttranslation_group: string | null;\n\t\t\t\t\tseo_image: string | null;\n\t\t\t\t}>`\n\t\t\t\t\tSELECT c.slug, c.id, c.updated_at, c.locale, c.translation_group, s.seo_image\n\t\t\t\t\tFROM ${sql.ref(tableName)} c\n\t\t\t\t\tLEFT JOIN _emdash_seo s\n\t\t\t\t\t\tON s.collection = ${col.slug}\n\t\t\t\t\t\tAND s.content_id = c.id\n\t\t\t\t\tWHERE c.status = 'published'\n\t\t\t\t\tAND c.deleted_at IS NULL\n\t\t\t\t\tAND (s.seo_no_index IS NULL OR s.seo_no_index = 0)\n\t\t\t\t\tORDER BY c.updated_at DESC\n\t\t\t\t\tLIMIT ${SITEMAP_MAX_ENTRIES}\n\t\t\t\t`.execute(db);\n\n\t\t\t\tif (rows.rows.length === 0) continue;\n\n\t\t\t\tconst entries: SitemapContentEntry[] = [];\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tentries.push({\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\tslug: row.slug,\n\t\t\t\t\t\tupdatedAt: row.updated_at,\n\t\t\t\t\t\tlocale: row.locale,\n\t\t\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t\t\t\timage: row.seo_image ?? null,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tresult.push({\n\t\t\t\t\tcollection: col.slug,\n\t\t\t\t\turlPattern: col.url_pattern,\n\t\t\t\t\t// Rows are ordered by updated_at DESC, so first row is the latest\n\t\t\t\t\tlastmod: rows.rows[0].updated_at,\n\t\t\t\t\tentries,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\t// Table missing or query error — skip this collection\n\t\t\t\tconsole.warn(`[SITEMAP] Failed to query collection \"${col.slug}\":`, err);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\treturn { success: true, data: { collections: result } };\n\t} catch (error) {\n\t\tconsole.error(\"[SITEMAP_ERROR]\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SITEMAP_ERROR\", message: \"Failed to generate sitemap data\" },\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;AAwDA,MAAM,sBAAsB;;;;;;;;;;;;AAa5B,eAAsB,kBACrB,IAEA,gBAC0C;AAC1C,KAAI;EAEH,IAAI,QAAQ,GACV,WAAW,sBAAsB,CACjC,OAAO,CAAC,QAAQ,cAAc,CAAC,CAC/B,MAAM,WAAW,KAAK,EAAE;AAE1B,MAAI,eACH,SAAQ,MAAM,MAAM,QAAQ,KAAK,eAAe;EAGjD,MAAM,cAAc,MAAM,MAAM,SAAS;EAEzC,MAAM,SAAkC,EAAE;AAE1C,OAAK,MAAM,OAAO,aAAa;AAI9B,OAAI;AACH,uBAAmB,IAAI,MAAM,kBAAkB;WACxC;AACP,YAAQ,KAAK,oDAAoD,IAAI,OAAO;AAC5E;;GAGD,MAAM,YAAY,MAAM,IAAI;AAO5B,OAAI;IACH,MAAM,OAAO,MAAM,GAOjB;;YAEM,IAAI,IAAI,UAAU,CAAC;;0BAEL,IAAI,KAAK;;;;;;aAMtB,oBAAoB;MAC3B,QAAQ,GAAG;AAEb,QAAI,KAAK,KAAK,WAAW,EAAG;IAE5B,MAAM,UAAiC,EAAE;AACzC,SAAK,MAAM,OAAO,KAAK,KACtB,SAAQ,KAAK;KACZ,IAAI,IAAI;KACR,MAAM,IAAI;KACV,WAAW,IAAI;KACf,QAAQ,IAAI;KACZ,kBAAkB,IAAI;KACtB,OAAO,IAAI,aAAa;KACxB,CAAC;AAGH,WAAO,KAAK;KACX,YAAY,IAAI;KAChB,YAAY,IAAI;KAEhB,SAAS,KAAK,KAAK,GAAG;KACtB;KACA,CAAC;YACM,KAAK;AAEb,YAAQ,KAAK,yCAAyC,IAAI,KAAK,KAAK,IAAI;AACxE;;;AAIF,SAAO;GAAE,SAAS;GAAM,MAAM,EAAE,aAAa,QAAQ;GAAE;UAC/C,OAAO;AACf,UAAQ,MAAM,mBAAmB,MAAM;AACvC,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAiB,SAAS;IAAmC;GAC5E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
|
-
import { t as CommentRepository } from "./comment-
|
|
3
|
-
import { t as escapeHtml } from "./escape-
|
|
2
|
+
import { t as CommentRepository } from "./comment-sqQxNpN3.mjs";
|
|
3
|
+
import { t as escapeHtml } from "./escape-Ds07EEyu.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/comments/notifications.ts
|
|
6
6
|
const NOTIFICATION_SOURCE = "emdash-comments";
|
|
@@ -192,4 +192,4 @@ function commentToStored(comment) {
|
|
|
192
192
|
|
|
193
193
|
//#endregion
|
|
194
194
|
export { sendCommentNotification as i, moderateComment as n, lookupContentAuthor as r, createComment as t };
|
|
195
|
-
//# sourceMappingURL=service-
|
|
195
|
+
//# sourceMappingURL=service-ChDcsTBs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-BhR2acnc.mjs","names":[],"sources":["../src/comments/notifications.ts","../src/comments/service.ts"],"sourcesContent":["/**\n * Comment Notification Emails\n *\n * Sends email notifications to content authors when comments are\n * approved on their content. Used by:\n * - Public comment POST route (comment:afterCreate, if auto-approved)\n * - Admin moderation route (comment:afterModerate, when approving)\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { escapeHtml } from \"../api/escape.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport type { EmailPipeline } from \"../plugins/email.js\";\nimport type { EmailMessage } from \"../plugins/types.js\";\n\nconst NOTIFICATION_SOURCE = \"emdash-comments\";\nconst MAX_EXCERPT_LENGTH = 500;\nconst CRLF_RE = /[\\r\\n]/g;\n\nexport interface CommentNotificationData {\n\tcommentAuthorName: string;\n\tcommentBody: string;\n\tcontentTitle: string;\n\tcollection: string;\n\tadminBaseUrl: string;\n}\n\n/**\n * Build an email notification for a new comment.\n */\nexport function buildCommentNotificationEmail(\n\tto: string,\n\tdata: CommentNotificationData,\n): EmailMessage {\n\tconst title = data.contentTitle || `${data.collection} item`;\n\tconst subject = `New comment on \"${title}\"`.replace(CRLF_RE, \" \");\n\n\tconst excerpt =\n\t\tdata.commentBody.length > MAX_EXCERPT_LENGTH\n\t\t\t? data.commentBody.slice(0, MAX_EXCERPT_LENGTH) + \"...\"\n\t\t\t: data.commentBody;\n\n\tconst adminUrl = `${data.adminBaseUrl}/admin/comments`;\n\n\tconst text = [\n\t\t`${data.commentAuthorName} commented on \"${title}\":`,\n\t\t\"\",\n\t\texcerpt,\n\t\t\"\",\n\t\t`View in admin: ${adminUrl}`,\n\t].join(\"\\n\");\n\n\tconst html = [\n\t\t`<p><strong>${escapeHtml(data.commentAuthorName)}</strong> commented on “${escapeHtml(title)}”:</p>`,\n\t\t`<blockquote style=\"border-left:3px solid #ccc;padding-left:12px;margin:12px 0;color:#555\">${escapeHtml(excerpt)}</blockquote>`,\n\t\t`<p><a href=\"${escapeHtml(adminUrl)}\">View in admin</a></p>`,\n\t].join(\"\\n\");\n\n\treturn { to, subject, text, html };\n}\n\n/**\n * Send a comment notification to the content author if all conditions are met:\n * 1. Comment status is \"approved\"\n * 2. Content author exists and has an email\n * 3. Email provider is configured\n * 4. Commenter is not the content author (no self-notifications)\n *\n * Returns true if the email was sent, false if skipped.\n */\nexport async function sendCommentNotification(params: {\n\temail: EmailPipeline;\n\tcomment: {\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tbody: string;\n\t\tstatus: string;\n\t\tcollection: string;\n\t};\n\tcontentTitle?: string;\n\tcontentAuthor?: { email: string; name: string | null };\n\tadminBaseUrl: string;\n}): Promise<boolean> {\n\tconst { email, comment, contentAuthor, adminBaseUrl } = params;\n\n\tif (comment.status !== \"approved\") return false;\n\tif (!contentAuthor?.email) return false;\n\tif (!email.isAvailable()) return false;\n\tif (comment.authorEmail.toLowerCase() === contentAuthor.email.toLowerCase()) return false;\n\n\tconst message = buildCommentNotificationEmail(contentAuthor.email, {\n\t\tcommentAuthorName: comment.authorName,\n\t\tcommentBody: comment.body,\n\t\tcontentTitle: params.contentTitle || \"\",\n\t\tcollection: comment.collection,\n\t\tadminBaseUrl,\n\t});\n\n\tawait email.send(message, NOTIFICATION_SOURCE);\n\treturn true;\n}\n\n/**\n * Look up a content item's author from the database.\n *\n * Used by the admin moderation route where content info isn't\n * readily available (only the comment record is at hand).\n */\nexport async function lookupContentAuthor(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n): Promise<{\n\tslug: string;\n\tauthor?: { id: string; email: string; name: string | null };\n} | null> {\n\tvalidateIdentifier(collection, \"collection\");\n\n\tconst contentRow = await db\n\t\t.selectFrom(`ec_${collection}` as never)\n\t\t.select([\"slug\" as never, \"author_id\" as never])\n\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t.executeTakeFirst();\n\n\tif (!contentRow) return null;\n\n\tconst typed = contentRow as { slug: string; author_id: string | null };\n\n\tlet author: { id: string; email: string; name: string | null } | undefined;\n\tif (typed.author_id) {\n\t\tconst userRow = await db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t.where(\"id\", \"=\", typed.author_id)\n\t\t\t.executeTakeFirst();\n\t\tif (userRow && userRow.email_verified) {\n\t\t\tauthor = { id: userRow.id, email: userRow.email, name: userRow.name };\n\t\t}\n\t}\n\n\treturn { slug: typed.slug, author };\n}\n","/**\n * Comment Service\n *\n * Orchestrates comment creation through the hook pipeline:\n * 1. Run comment:beforeCreate pipeline (transform/reject)\n * 2. Query priorApprovedCount for first-time moderation\n * 3. Invoke comment:moderate exclusive hook (or built-in fallback)\n * 4. Save comment with determined status\n * 5. Fire comment:afterCreate (fire-and-forget)\n *\n * Also handles admin moderation (status changes) with afterModerate hooks.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../database/repositories/comment.js\";\nimport type { Comment, CommentStatus } from \"../database/repositories/comment.js\";\nimport type { Database } from \"../database/types.js\";\nimport type {\n\tCollectionCommentSettings,\n\tCommentAfterCreateEvent,\n\tCommentAfterModerateEvent,\n\tCommentBeforeCreateEvent,\n\tCommentModerateEvent,\n\tModerationDecision,\n\tStoredComment,\n} from \"../plugins/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CommentCreateInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n}\n\nexport interface CommentCreateResult {\n\tcomment: Comment;\n\tdecision: ModerationDecision;\n}\n\n/**\n * Hook runner interface — injected from the runtime so the service\n * doesn't need to know about the hook pipeline internals.\n */\nexport interface CommentHookRunner {\n\t/** Run comment:beforeCreate pipeline. Returns modified event or false. */\n\trunBeforeCreate(event: CommentBeforeCreateEvent): Promise<CommentBeforeCreateEvent | false>;\n\n\t/** Run comment:moderate exclusive hook. Returns moderation decision. */\n\trunModerate(event: CommentModerateEvent): Promise<ModerationDecision>;\n\n\t/** Fire comment:afterCreate (fire-and-forget). */\n\tfireAfterCreate(event: CommentAfterCreateEvent): void;\n\n\t/** Fire comment:afterModerate (fire-and-forget). */\n\tfireAfterModerate(event: CommentAfterModerateEvent): void;\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\n/**\n * Create a comment through the full hook pipeline.\n *\n * Returns null if the comment was rejected by a beforeCreate handler.\n */\nexport async function createComment(\n\tdb: Kysely<Database>,\n\tinput: CommentCreateInput,\n\tcollectionSettings: CollectionCommentSettings,\n\thooks: CommentHookRunner,\n\tcontentInfo?: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tslug: string;\n\t\ttitle?: string;\n\t\tauthor?: { id: string; name: string | null; email: string };\n\t},\n): Promise<CommentCreateResult | null> {\n\tconst repo = new CommentRepository(db);\n\n\t// 1. Build the beforeCreate event\n\tconst beforeCreateEvent: CommentBeforeCreateEvent = {\n\t\tcomment: {\n\t\t\tcollection: input.collection,\n\t\t\tcontentId: input.contentId,\n\t\t\tparentId: input.parentId ?? null,\n\t\t\tauthorName: input.authorName,\n\t\t\tauthorEmail: input.authorEmail,\n\t\t\tauthorUserId: input.authorUserId ?? null,\n\t\t\tbody: input.body,\n\t\t\tipHash: input.ipHash ?? null,\n\t\t\tuserAgent: input.userAgent ?? null,\n\t\t},\n\t\tmetadata: {},\n\t};\n\n\t// 2. Run comment:beforeCreate pipeline\n\tconst result = await hooks.runBeforeCreate(beforeCreateEvent);\n\tif (result === false) {\n\t\treturn null; // Rejected\n\t}\n\n\tconst event = result;\n\n\t// 3. Query prior approved count for first-time moderation\n\tconst priorApprovedCount = await repo.countApprovedByEmail(event.comment.authorEmail);\n\n\t// 4. Run comment:moderate exclusive hook\n\tconst moderateEvent: CommentModerateEvent = {\n\t\tcomment: event.comment,\n\t\tmetadata: event.metadata,\n\t\tcollectionSettings,\n\t\tpriorApprovedCount,\n\t};\n\n\tconst decision = await hooks.runModerate(moderateEvent);\n\n\t// 5. Save comment with determined status\n\tconst comment = await repo.create({\n\t\tcollection: event.comment.collection,\n\t\tcontentId: event.comment.contentId,\n\t\tparentId: event.comment.parentId,\n\t\tauthorName: event.comment.authorName,\n\t\tauthorEmail: event.comment.authorEmail,\n\t\tauthorUserId: event.comment.authorUserId,\n\t\tbody: event.comment.body,\n\t\tstatus: decision.status as CommentStatus,\n\t\tipHash: event.comment.ipHash,\n\t\tuserAgent: event.comment.userAgent,\n\t\tmoderationMetadata: Object.keys(event.metadata).length > 0 ? event.metadata : null,\n\t});\n\n\t// 6. Fire comment:afterCreate (fire-and-forget)\n\tif (contentInfo) {\n\t\tconst afterEvent: CommentAfterCreateEvent = {\n\t\t\tcomment: commentToStored(comment),\n\t\t\tmetadata: event.metadata,\n\t\t\tcontent: {\n\t\t\t\tid: contentInfo.id,\n\t\t\t\tcollection: contentInfo.collection,\n\t\t\t\tslug: contentInfo.slug,\n\t\t\t\ttitle: contentInfo.title,\n\t\t\t},\n\t\t\tcontentAuthor: contentInfo.author,\n\t\t};\n\t\thooks.fireAfterCreate(afterEvent);\n\t}\n\n\treturn { comment, decision };\n}\n\n/**\n * Admin moderation — change a comment's status.\n * Fires comment:afterModerate hook.\n */\nexport async function moderateComment(\n\tdb: Kysely<Database>,\n\tid: string,\n\tnewStatus: CommentStatus,\n\tmoderator: { id: string; name: string | null },\n\thooks: CommentHookRunner,\n): Promise<Comment | null> {\n\tconst repo = new CommentRepository(db);\n\tconst existing = await repo.findById(id);\n\tif (!existing) return null;\n\n\tconst previousStatus = existing.status;\n\tconst updated = await repo.updateStatus(id, newStatus);\n\tif (!updated) return null;\n\n\t// Fire comment:afterModerate (fire-and-forget)\n\tconst afterEvent: CommentAfterModerateEvent = {\n\t\tcomment: commentToStored(updated),\n\t\tpreviousStatus,\n\t\tnewStatus,\n\t\tmoderator,\n\t};\n\thooks.fireAfterModerate(afterEvent);\n\n\treturn updated;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction commentToStored(comment: Comment): StoredComment {\n\treturn {\n\t\tid: comment.id,\n\t\tcollection: comment.collection,\n\t\tcontentId: comment.contentId,\n\t\tparentId: comment.parentId,\n\t\tauthorName: comment.authorName,\n\t\tauthorEmail: comment.authorEmail,\n\t\tauthorUserId: comment.authorUserId,\n\t\tbody: comment.body,\n\t\tstatus: comment.status,\n\t\tmoderationMetadata: comment.moderationMetadata,\n\t\tcreatedAt: comment.createdAt,\n\t\tupdatedAt: comment.updatedAt,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service-ChDcsTBs.mjs","names":[],"sources":["../src/comments/notifications.ts","../src/comments/service.ts"],"sourcesContent":["/**\n * Comment Notification Emails\n *\n * Sends email notifications to content authors when comments are\n * approved on their content. Used by:\n * - Public comment POST route (comment:afterCreate, if auto-approved)\n * - Admin moderation route (comment:afterModerate, when approving)\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { escapeHtml } from \"../api/escape.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport type { EmailPipeline } from \"../plugins/email.js\";\nimport type { EmailMessage } from \"../plugins/types.js\";\n\nconst NOTIFICATION_SOURCE = \"emdash-comments\";\nconst MAX_EXCERPT_LENGTH = 500;\nconst CRLF_RE = /[\\r\\n]/g;\n\nexport interface CommentNotificationData {\n\tcommentAuthorName: string;\n\tcommentBody: string;\n\tcontentTitle: string;\n\tcollection: string;\n\tadminBaseUrl: string;\n}\n\n/**\n * Build an email notification for a new comment.\n */\nexport function buildCommentNotificationEmail(\n\tto: string,\n\tdata: CommentNotificationData,\n): EmailMessage {\n\tconst title = data.contentTitle || `${data.collection} item`;\n\tconst subject = `New comment on \"${title}\"`.replace(CRLF_RE, \" \");\n\n\tconst excerpt =\n\t\tdata.commentBody.length > MAX_EXCERPT_LENGTH\n\t\t\t? data.commentBody.slice(0, MAX_EXCERPT_LENGTH) + \"...\"\n\t\t\t: data.commentBody;\n\n\tconst adminUrl = `${data.adminBaseUrl}/admin/comments`;\n\n\tconst text = [\n\t\t`${data.commentAuthorName} commented on \"${title}\":`,\n\t\t\"\",\n\t\texcerpt,\n\t\t\"\",\n\t\t`View in admin: ${adminUrl}`,\n\t].join(\"\\n\");\n\n\tconst html = [\n\t\t`<p><strong>${escapeHtml(data.commentAuthorName)}</strong> commented on “${escapeHtml(title)}”:</p>`,\n\t\t`<blockquote style=\"border-left:3px solid #ccc;padding-left:12px;margin:12px 0;color:#555\">${escapeHtml(excerpt)}</blockquote>`,\n\t\t`<p><a href=\"${escapeHtml(adminUrl)}\">View in admin</a></p>`,\n\t].join(\"\\n\");\n\n\treturn { to, subject, text, html };\n}\n\n/**\n * Send a comment notification to the content author if all conditions are met:\n * 1. Comment status is \"approved\"\n * 2. Content author exists and has an email\n * 3. Email provider is configured\n * 4. Commenter is not the content author (no self-notifications)\n *\n * Returns true if the email was sent, false if skipped.\n */\nexport async function sendCommentNotification(params: {\n\temail: EmailPipeline;\n\tcomment: {\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tbody: string;\n\t\tstatus: string;\n\t\tcollection: string;\n\t};\n\tcontentTitle?: string;\n\tcontentAuthor?: { email: string; name: string | null };\n\tadminBaseUrl: string;\n}): Promise<boolean> {\n\tconst { email, comment, contentAuthor, adminBaseUrl } = params;\n\n\tif (comment.status !== \"approved\") return false;\n\tif (!contentAuthor?.email) return false;\n\tif (!email.isAvailable()) return false;\n\tif (comment.authorEmail.toLowerCase() === contentAuthor.email.toLowerCase()) return false;\n\n\tconst message = buildCommentNotificationEmail(contentAuthor.email, {\n\t\tcommentAuthorName: comment.authorName,\n\t\tcommentBody: comment.body,\n\t\tcontentTitle: params.contentTitle || \"\",\n\t\tcollection: comment.collection,\n\t\tadminBaseUrl,\n\t});\n\n\tawait email.send(message, NOTIFICATION_SOURCE);\n\treturn true;\n}\n\n/**\n * Look up a content item's author from the database.\n *\n * Used by the admin moderation route where content info isn't\n * readily available (only the comment record is at hand).\n */\nexport async function lookupContentAuthor(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n): Promise<{\n\tslug: string;\n\tauthor?: { id: string; email: string; name: string | null };\n} | null> {\n\tvalidateIdentifier(collection, \"collection\");\n\n\tconst contentRow = await db\n\t\t.selectFrom(`ec_${collection}` as never)\n\t\t.select([\"slug\" as never, \"author_id\" as never])\n\t\t.where(\"id\" as never, \"=\", contentId as never)\n\t\t.executeTakeFirst();\n\n\tif (!contentRow) return null;\n\n\tconst typed = contentRow as { slug: string; author_id: string | null };\n\n\tlet author: { id: string; email: string; name: string | null } | undefined;\n\tif (typed.author_id) {\n\t\tconst userRow = await db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.select([\"id\", \"name\", \"email\", \"email_verified\"])\n\t\t\t.where(\"id\", \"=\", typed.author_id)\n\t\t\t.executeTakeFirst();\n\t\tif (userRow && userRow.email_verified) {\n\t\t\tauthor = { id: userRow.id, email: userRow.email, name: userRow.name };\n\t\t}\n\t}\n\n\treturn { slug: typed.slug, author };\n}\n","/**\n * Comment Service\n *\n * Orchestrates comment creation through the hook pipeline:\n * 1. Run comment:beforeCreate pipeline (transform/reject)\n * 2. Query priorApprovedCount for first-time moderation\n * 3. Invoke comment:moderate exclusive hook (or built-in fallback)\n * 4. Save comment with determined status\n * 5. Fire comment:afterCreate (fire-and-forget)\n *\n * Also handles admin moderation (status changes) with afterModerate hooks.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../database/repositories/comment.js\";\nimport type { Comment, CommentStatus } from \"../database/repositories/comment.js\";\nimport type { Database } from \"../database/types.js\";\nimport type {\n\tCollectionCommentSettings,\n\tCommentAfterCreateEvent,\n\tCommentAfterModerateEvent,\n\tCommentBeforeCreateEvent,\n\tCommentModerateEvent,\n\tModerationDecision,\n\tStoredComment,\n} from \"../plugins/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CommentCreateInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n}\n\nexport interface CommentCreateResult {\n\tcomment: Comment;\n\tdecision: ModerationDecision;\n}\n\n/**\n * Hook runner interface — injected from the runtime so the service\n * doesn't need to know about the hook pipeline internals.\n */\nexport interface CommentHookRunner {\n\t/** Run comment:beforeCreate pipeline. Returns modified event or false. */\n\trunBeforeCreate(event: CommentBeforeCreateEvent): Promise<CommentBeforeCreateEvent | false>;\n\n\t/** Run comment:moderate exclusive hook. Returns moderation decision. */\n\trunModerate(event: CommentModerateEvent): Promise<ModerationDecision>;\n\n\t/** Fire comment:afterCreate (fire-and-forget). */\n\tfireAfterCreate(event: CommentAfterCreateEvent): void;\n\n\t/** Fire comment:afterModerate (fire-and-forget). */\n\tfireAfterModerate(event: CommentAfterModerateEvent): void;\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\n/**\n * Create a comment through the full hook pipeline.\n *\n * Returns null if the comment was rejected by a beforeCreate handler.\n */\nexport async function createComment(\n\tdb: Kysely<Database>,\n\tinput: CommentCreateInput,\n\tcollectionSettings: CollectionCommentSettings,\n\thooks: CommentHookRunner,\n\tcontentInfo?: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tslug: string;\n\t\ttitle?: string;\n\t\tauthor?: { id: string; name: string | null; email: string };\n\t},\n): Promise<CommentCreateResult | null> {\n\tconst repo = new CommentRepository(db);\n\n\t// 1. Build the beforeCreate event\n\tconst beforeCreateEvent: CommentBeforeCreateEvent = {\n\t\tcomment: {\n\t\t\tcollection: input.collection,\n\t\t\tcontentId: input.contentId,\n\t\t\tparentId: input.parentId ?? null,\n\t\t\tauthorName: input.authorName,\n\t\t\tauthorEmail: input.authorEmail,\n\t\t\tauthorUserId: input.authorUserId ?? null,\n\t\t\tbody: input.body,\n\t\t\tipHash: input.ipHash ?? null,\n\t\t\tuserAgent: input.userAgent ?? null,\n\t\t},\n\t\tmetadata: {},\n\t};\n\n\t// 2. Run comment:beforeCreate pipeline\n\tconst result = await hooks.runBeforeCreate(beforeCreateEvent);\n\tif (result === false) {\n\t\treturn null; // Rejected\n\t}\n\n\tconst event = result;\n\n\t// 3. Query prior approved count for first-time moderation\n\tconst priorApprovedCount = await repo.countApprovedByEmail(event.comment.authorEmail);\n\n\t// 4. Run comment:moderate exclusive hook\n\tconst moderateEvent: CommentModerateEvent = {\n\t\tcomment: event.comment,\n\t\tmetadata: event.metadata,\n\t\tcollectionSettings,\n\t\tpriorApprovedCount,\n\t};\n\n\tconst decision = await hooks.runModerate(moderateEvent);\n\n\t// 5. Save comment with determined status\n\tconst comment = await repo.create({\n\t\tcollection: event.comment.collection,\n\t\tcontentId: event.comment.contentId,\n\t\tparentId: event.comment.parentId,\n\t\tauthorName: event.comment.authorName,\n\t\tauthorEmail: event.comment.authorEmail,\n\t\tauthorUserId: event.comment.authorUserId,\n\t\tbody: event.comment.body,\n\t\tstatus: decision.status as CommentStatus,\n\t\tipHash: event.comment.ipHash,\n\t\tuserAgent: event.comment.userAgent,\n\t\tmoderationMetadata: Object.keys(event.metadata).length > 0 ? event.metadata : null,\n\t});\n\n\t// 6. Fire comment:afterCreate (fire-and-forget)\n\tif (contentInfo) {\n\t\tconst afterEvent: CommentAfterCreateEvent = {\n\t\t\tcomment: commentToStored(comment),\n\t\t\tmetadata: event.metadata,\n\t\t\tcontent: {\n\t\t\t\tid: contentInfo.id,\n\t\t\t\tcollection: contentInfo.collection,\n\t\t\t\tslug: contentInfo.slug,\n\t\t\t\ttitle: contentInfo.title,\n\t\t\t},\n\t\t\tcontentAuthor: contentInfo.author,\n\t\t};\n\t\thooks.fireAfterCreate(afterEvent);\n\t}\n\n\treturn { comment, decision };\n}\n\n/**\n * Admin moderation — change a comment's status.\n * Fires comment:afterModerate hook.\n */\nexport async function moderateComment(\n\tdb: Kysely<Database>,\n\tid: string,\n\tnewStatus: CommentStatus,\n\tmoderator: { id: string; name: string | null },\n\thooks: CommentHookRunner,\n): Promise<Comment | null> {\n\tconst repo = new CommentRepository(db);\n\tconst existing = await repo.findById(id);\n\tif (!existing) return null;\n\n\tconst previousStatus = existing.status;\n\tconst updated = await repo.updateStatus(id, newStatus);\n\tif (!updated) return null;\n\n\t// Fire comment:afterModerate (fire-and-forget)\n\tconst afterEvent: CommentAfterModerateEvent = {\n\t\tcomment: commentToStored(updated),\n\t\tpreviousStatus,\n\t\tnewStatus,\n\t\tmoderator,\n\t};\n\thooks.fireAfterModerate(afterEvent);\n\n\treturn updated;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction commentToStored(comment: Comment): StoredComment {\n\treturn {\n\t\tid: comment.id,\n\t\tcollection: comment.collection,\n\t\tcontentId: comment.contentId,\n\t\tparentId: comment.parentId,\n\t\tauthorName: comment.authorName,\n\t\tauthorEmail: comment.authorEmail,\n\t\tauthorUserId: comment.authorUserId,\n\t\tbody: comment.body,\n\t\tstatus: comment.status,\n\t\tmoderationMetadata: comment.moderationMetadata,\n\t\tcreatedAt: comment.createdAt,\n\t\tupdatedAt: comment.updatedAt,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as __exportAll } from "./runner
|
|
2
|
-
import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-
|
|
1
|
+
import { a as __exportAll } from "./runner--4wMWwKM.mjs";
|
|
2
|
+
import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-DfxiWY_s.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/api/handlers/settings.ts
|
|
5
5
|
var settings_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -48,4 +48,4 @@ async function handleSettingsUpdate(db, storage, input) {
|
|
|
48
48
|
|
|
49
49
|
//#endregion
|
|
50
50
|
export { handleSettingsUpdate as n, settings_exports as r, handleSettingsGet as t };
|
|
51
|
-
//# sourceMappingURL=settings-
|
|
51
|
+
//# sourceMappingURL=settings-Cv47v9u8.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-
|
|
1
|
+
{"version":3,"file":"settings-Cv47v9u8.mjs","names":[],"sources":["../src/api/handlers/settings.ts"],"sourcesContent":["/**\n * Settings handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { getSiteSettingsWithDb, setSiteSettings } from \"../../settings/index.js\";\nimport type { SiteSettings } from \"../../settings/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/**\n * Get all site settings\n */\nexport async function handleSettingsGet(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tconst settings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: settings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_READ_ERROR\", message: \"Failed to get settings\" },\n\t\t};\n\t}\n}\n\n/**\n * Update site settings\n */\nexport async function handleSettingsUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tinput: Partial<SiteSettings>,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tawait setSiteSettings(input, db);\n\t\tconst updatedSettings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: updatedSettings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_UPDATE_ERROR\", message: \"Failed to update settings\" },\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAeA,eAAsB,kBACrB,IACA,SAC4C;AAC5C,KAAI;AAEH,SAAO;GAAE,SAAS;GAAM,MADP,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACjC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAuB,SAAS;IAA0B;GACzE;;;;;;AAOH,eAAsB,qBACrB,IACA,SACA,OAC4C;AAC5C,KAAI;AACH,QAAM,gBAAgB,OAAO,GAAG;AAEhC,SAAO;GAAE,SAAS;GAAM,MADA,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACxC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAyB,SAAS;IAA6B;GAC9E"}
|