emdash 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +9 -0
  2. package/dist/{adapters-BLMa4JGD.d.mts → adapters-N6BF7RCD.d.mts} +1 -1
  3. package/dist/{adapters-BLMa4JGD.d.mts.map → adapters-N6BF7RCD.d.mts.map} +1 -1
  4. package/dist/{apply-Bjfq_b4-.mjs → apply-wmVEOSbR.mjs} +57 -10
  5. package/dist/apply-wmVEOSbR.mjs.map +1 -0
  6. package/dist/astro/index.d.mts +6 -6
  7. package/dist/astro/index.mjs +90 -22
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +5 -5
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +127 -56
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/request-context.mjs +85 -23
  14. package/dist/astro/middleware/request-context.mjs.map +1 -1
  15. package/dist/astro/middleware/setup.mjs +1 -1
  16. package/dist/astro/middleware.d.mts.map +1 -1
  17. package/dist/astro/middleware.mjs +107 -43
  18. package/dist/astro/middleware.mjs.map +1 -1
  19. package/dist/astro/types.d.mts +31 -9
  20. package/dist/astro/types.d.mts.map +1 -1
  21. package/dist/{byline-CL847F26.mjs → byline-1WQPlISL.mjs} +51 -29
  22. package/dist/byline-1WQPlISL.mjs.map +1 -0
  23. package/dist/{bylines-C2a-2TGt.mjs → bylines-BYdTYmia.mjs} +10 -8
  24. package/dist/{bylines-C2a-2TGt.mjs.map → bylines-BYdTYmia.mjs.map} +1 -1
  25. package/dist/cli/index.mjs +75 -13
  26. package/dist/cli/index.mjs.map +1 -1
  27. package/dist/client/cf-access.d.mts +1 -1
  28. package/dist/client/index.d.mts +1 -1
  29. package/dist/client/index.mjs +1 -1
  30. package/dist/{config-CKE8p9xM.mjs → config-Cq8H0SfX.mjs} +2 -10
  31. package/dist/{config-CKE8p9xM.mjs.map → config-Cq8H0SfX.mjs.map} +1 -1
  32. package/dist/{content-D6C2WsZC.mjs → content-BmXndhdi.mjs} +16 -3
  33. package/dist/content-BmXndhdi.mjs.map +1 -0
  34. package/dist/db/index.d.mts +3 -3
  35. package/dist/db/index.mjs +1 -1
  36. package/dist/db/libsql.d.mts +1 -1
  37. package/dist/db/postgres.d.mts +1 -1
  38. package/dist/db/sqlite.d.mts +1 -1
  39. package/dist/{default-Cyi4aAxu.mjs → default-WYlzADZL.mjs} +1 -1
  40. package/dist/{default-Cyi4aAxu.mjs.map → default-WYlzADZL.mjs.map} +1 -1
  41. package/dist/{error-Cxz0tQeO.mjs → error-DrxtnGPg.mjs} +1 -1
  42. package/dist/{error-Cxz0tQeO.mjs.map → error-DrxtnGPg.mjs.map} +1 -1
  43. package/dist/{index-C1xF3OGh.d.mts → index-UHEVQMus.d.mts} +83 -14
  44. package/dist/index-UHEVQMus.d.mts.map +1 -0
  45. package/dist/index.d.mts +11 -11
  46. package/dist/index.mjs +18 -18
  47. package/dist/{load-yOOlckBj.mjs → load-Veizk2cT.mjs} +1 -1
  48. package/dist/{load-yOOlckBj.mjs.map → load-Veizk2cT.mjs.map} +1 -1
  49. package/dist/{loader-fz8Q_3EO.mjs → loader-CHb2v0jm.mjs} +1 -1
  50. package/dist/{loader-fz8Q_3EO.mjs.map → loader-CHb2v0jm.mjs.map} +1 -1
  51. package/dist/{manifest-schema-Dcl0R6nM.mjs → manifest-schema-CuMio1A9.mjs} +5 -2
  52. package/dist/manifest-schema-CuMio1A9.mjs.map +1 -0
  53. package/dist/media/index.d.mts +1 -1
  54. package/dist/media/index.mjs +1 -1
  55. package/dist/media/local-runtime.d.mts +7 -7
  56. package/dist/{mode-C2EzN1uE.mjs → mode-CYeM2rPt.mjs} +1 -1
  57. package/dist/{mode-C2EzN1uE.mjs.map → mode-CYeM2rPt.mjs.map} +1 -1
  58. package/dist/page/index.d.mts +10 -1
  59. package/dist/page/index.d.mts.map +1 -1
  60. package/dist/page/index.mjs +8 -4
  61. package/dist/page/index.mjs.map +1 -1
  62. package/dist/{placeholder-SmpOx-_v.mjs → placeholder-aiCD8aSZ.mjs} +27 -2
  63. package/dist/placeholder-aiCD8aSZ.mjs.map +1 -0
  64. package/dist/{placeholder-CmGAmqeO.d.mts → placeholder-bOx1xCTY.d.mts} +10 -2
  65. package/dist/{placeholder-CmGAmqeO.d.mts.map → placeholder-bOx1xCTY.d.mts.map} +1 -1
  66. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  67. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  68. package/dist/{query-CS_iSj34.mjs → query-5Hcv_5ER.mjs} +20 -8
  69. package/dist/{query-CS_iSj34.mjs.map → query-5Hcv_5ER.mjs.map} +1 -1
  70. package/dist/{registry-D_w5HW4G.mjs → registry-1EvbAfsC.mjs} +27 -38
  71. package/dist/registry-1EvbAfsC.mjs.map +1 -0
  72. package/dist/{runner-C0hCbYnD.mjs → runner-BoN0-FPi.mjs} +276 -158
  73. package/dist/runner-BoN0-FPi.mjs.map +1 -0
  74. package/dist/{runner-EAtf0ZIe.d.mts → runner-DTqkzOzc.d.mts} +2 -2
  75. package/dist/runner-DTqkzOzc.d.mts.map +1 -0
  76. package/dist/runtime.d.mts +6 -6
  77. package/dist/runtime.mjs +1 -1
  78. package/dist/{search-DG603UrT.mjs → search-BsYMed12.mjs} +355 -118
  79. package/dist/search-BsYMed12.mjs.map +1 -0
  80. package/dist/seed/index.d.mts +2 -2
  81. package/dist/seed/index.mjs +8 -8
  82. package/dist/seo/index.d.mts +1 -1
  83. package/dist/storage/local.d.mts +1 -1
  84. package/dist/storage/local.mjs +1 -1
  85. package/dist/storage/s3.d.mts +1 -1
  86. package/dist/storage/s3.mjs +1 -1
  87. package/dist/{tokens-DpgrkrXK.mjs → tokens-DrB-W6Q-.mjs} +1 -1
  88. package/dist/{tokens-DpgrkrXK.mjs.map → tokens-DrB-W6Q-.mjs.map} +1 -1
  89. package/dist/{transport-yxiQsi8I.mjs → transport-Bl8cTdYt.mjs} +1 -1
  90. package/dist/{transport-yxiQsi8I.mjs.map → transport-Bl8cTdYt.mjs.map} +1 -1
  91. package/dist/{transport-BFGblqwG.d.mts → transport-COOs9GSE.d.mts} +1 -1
  92. package/dist/{transport-BFGblqwG.d.mts.map → transport-COOs9GSE.d.mts.map} +1 -1
  93. package/dist/{types-DvhsUmSJ.d.mts → types-6dqxBqsH.d.mts} +93 -106
  94. package/dist/types-6dqxBqsH.d.mts.map +1 -0
  95. package/dist/{types-DRjfYOEv.d.mts → types-7-UjSEyB.d.mts} +1 -1
  96. package/dist/{types-DRjfYOEv.d.mts.map → types-7-UjSEyB.d.mts.map} +1 -1
  97. package/dist/{types-CUBbjgmP.mjs → types-Bec-r_3_.mjs} +1 -1
  98. package/dist/{types-CUBbjgmP.mjs.map → types-Bec-r_3_.mjs.map} +1 -1
  99. package/dist/{types-DaNLHo_T.d.mts → types-BljtYPSd.d.mts} +1 -1
  100. package/dist/{types-DaNLHo_T.d.mts.map → types-BljtYPSd.d.mts.map} +1 -1
  101. package/dist/{types-BRuPJGdV.d.mts → types-CIsTnQvJ.d.mts} +3 -1
  102. package/dist/types-CIsTnQvJ.d.mts.map +1 -0
  103. package/dist/types-CMMN0pNg.mjs.map +1 -1
  104. package/dist/{types-C4-fAxN3.d.mts → types-CcreFIIH.d.mts} +13 -2
  105. package/dist/types-CcreFIIH.d.mts.map +1 -0
  106. package/dist/{types-DY5zk5HN.mjs → types-DuNbGKjF.mjs} +5 -3
  107. package/dist/types-DuNbGKjF.mjs.map +1 -0
  108. package/dist/{validate-CpBtVMsD.d.mts → validate-B7KP7VLM.d.mts} +4 -4
  109. package/dist/{validate-CpBtVMsD.d.mts.map → validate-B7KP7VLM.d.mts.map} +1 -1
  110. package/dist/{validate-O7PWmlnq.mjs → validate-CXnRKfJK.mjs} +2 -2
  111. package/dist/{validate-O7PWmlnq.mjs.map → validate-CXnRKfJK.mjs.map} +1 -1
  112. package/package.json +9 -7
  113. package/src/api/csrf.ts +13 -2
  114. package/src/api/handlers/content.ts +7 -0
  115. package/src/api/handlers/dashboard.ts +4 -8
  116. package/src/api/handlers/device-flow.ts +55 -37
  117. package/src/api/handlers/index.ts +6 -1
  118. package/src/api/handlers/marketplace.ts +7 -4
  119. package/src/api/handlers/seo.ts +48 -21
  120. package/src/api/public-url.ts +84 -0
  121. package/src/api/schemas/content.ts +2 -2
  122. package/src/api/schemas/menus.ts +12 -2
  123. package/src/api/schemas/schema.ts +12 -0
  124. package/src/astro/integration/index.ts +41 -1
  125. package/src/astro/integration/routes.ts +13 -2
  126. package/src/astro/integration/runtime.ts +15 -0
  127. package/src/astro/integration/virtual-modules.ts +13 -1
  128. package/src/astro/integration/vite-config.ts +52 -9
  129. package/src/astro/middleware/auth.ts +60 -56
  130. package/src/astro/middleware/csp.ts +25 -0
  131. package/src/astro/middleware.ts +31 -3
  132. package/src/astro/routes/PluginRegistry.tsx +8 -2
  133. package/src/astro/routes/admin.astro +8 -3
  134. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +3 -1
  135. package/src/astro/routes/api/admin/users/[id]/disable.ts +18 -12
  136. package/src/astro/routes/api/admin/users/[id]/index.ts +26 -5
  137. package/src/astro/routes/api/auth/invite/complete.ts +3 -1
  138. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +2 -1
  139. package/src/astro/routes/api/auth/oauth/[provider].ts +2 -1
  140. package/src/astro/routes/api/auth/passkey/options.ts +3 -1
  141. package/src/astro/routes/api/auth/passkey/register/options.ts +3 -1
  142. package/src/astro/routes/api/auth/passkey/register/verify.ts +3 -1
  143. package/src/astro/routes/api/auth/passkey/verify.ts +3 -1
  144. package/src/astro/routes/api/auth/signup/complete.ts +3 -1
  145. package/src/astro/routes/api/content/[collection]/index.ts +31 -3
  146. package/src/astro/routes/api/import/wordpress/analyze.ts +24 -3
  147. package/src/astro/routes/api/import/wordpress/execute.ts +14 -1
  148. package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
  149. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +10 -0
  150. package/src/astro/routes/api/manifest.ts +1 -0
  151. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
  152. package/src/astro/routes/api/media.ts +16 -4
  153. package/src/astro/routes/api/oauth/authorize.ts +12 -7
  154. package/src/astro/routes/api/oauth/device/code.ts +5 -1
  155. package/src/astro/routes/api/search/index.ts +1 -5
  156. package/src/astro/routes/api/search/suggest.ts +1 -5
  157. package/src/astro/routes/api/setup/admin-verify.ts +3 -1
  158. package/src/astro/routes/api/setup/admin.ts +3 -1
  159. package/src/astro/routes/api/setup/dev-bypass.ts +2 -1
  160. package/src/astro/routes/api/setup/index.ts +3 -2
  161. package/src/astro/routes/api/snapshot.ts +2 -1
  162. package/src/astro/routes/api/themes/preview.ts +2 -1
  163. package/src/astro/routes/api/well-known/auth.ts +1 -0
  164. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +3 -2
  165. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
  166. package/src/astro/routes/robots.txt.ts +5 -1
  167. package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
  168. package/src/astro/routes/sitemap.xml.ts +18 -23
  169. package/src/astro/types.ts +28 -1
  170. package/src/auth/passkey-config.ts +20 -3
  171. package/src/bylines/index.ts +11 -8
  172. package/src/cli/commands/bundle-utils.ts +26 -0
  173. package/src/cli/commands/bundle.ts +15 -0
  174. package/src/cli/commands/content.ts +11 -1
  175. package/src/cli/commands/login.ts +7 -2
  176. package/src/cli/commands/media.ts +5 -1
  177. package/src/cli/commands/menu.ts +3 -1
  178. package/src/cli/commands/schema.ts +7 -1
  179. package/src/cli/commands/search-cmd.ts +2 -1
  180. package/src/cli/commands/taxonomy.ts +4 -1
  181. package/src/cli/output.ts +14 -0
  182. package/src/components/InlinePortableTextEditor.tsx +38 -6
  183. package/src/content/converters/portable-text-to-prosemirror.ts +50 -2
  184. package/src/database/migrations/033_optimize_content_indexes.ts +113 -0
  185. package/src/database/migrations/034_published_at_index.ts +29 -0
  186. package/src/database/migrations/runner.ts +42 -33
  187. package/src/database/repositories/byline.ts +48 -42
  188. package/src/database/repositories/comment.ts +32 -20
  189. package/src/database/repositories/content.ts +23 -1
  190. package/src/database/repositories/options.ts +9 -3
  191. package/src/database/repositories/seo.ts +34 -17
  192. package/src/database/repositories/types.ts +2 -0
  193. package/src/emdash-runtime.ts +125 -20
  194. package/src/import/index.ts +1 -1
  195. package/src/import/sources/wxr.ts +45 -2
  196. package/src/index.ts +9 -1
  197. package/src/mcp/server.ts +85 -5
  198. package/src/media/placeholder.ts +31 -0
  199. package/src/media/thumbnail.ts +32 -0
  200. package/src/menus/index.ts +2 -1
  201. package/src/page/context.ts +13 -1
  202. package/src/page/jsonld.ts +10 -6
  203. package/src/page/seo-contributions.ts +1 -1
  204. package/src/plugins/context.ts +145 -35
  205. package/src/plugins/hooks.ts +91 -0
  206. package/src/plugins/manager.ts +34 -0
  207. package/src/plugins/manifest-schema.ts +3 -0
  208. package/src/plugins/marketplace.ts +25 -12
  209. package/src/plugins/types.ts +104 -4
  210. package/src/query.ts +18 -0
  211. package/src/schema/registry.ts +26 -25
  212. package/src/schema/types.ts +27 -1
  213. package/src/search/fts-manager.ts +1 -18
  214. package/src/settings/index.ts +64 -0
  215. package/src/utils/chunks.ts +17 -0
  216. package/src/visual-editing/toolbar.ts +84 -22
  217. package/dist/apply-Bjfq_b4-.mjs.map +0 -1
  218. package/dist/byline-CL847F26.mjs.map +0 -1
  219. package/dist/content-D6C2WsZC.mjs.map +0 -1
  220. package/dist/index-C1xF3OGh.d.mts.map +0 -1
  221. package/dist/manifest-schema-Dcl0R6nM.mjs.map +0 -1
  222. package/dist/placeholder-SmpOx-_v.mjs.map +0 -1
  223. package/dist/registry-D_w5HW4G.mjs.map +0 -1
  224. package/dist/runner-C0hCbYnD.mjs.map +0 -1
  225. package/dist/runner-EAtf0ZIe.d.mts.map +0 -1
  226. package/dist/search-DG603UrT.mjs.map +0 -1
  227. package/dist/types-BRuPJGdV.d.mts.map +0 -1
  228. package/dist/types-C4-fAxN3.d.mts.map +0 -1
  229. package/dist/types-DY5zk5HN.mjs.map +0 -1
  230. package/dist/types-DvhsUmSJ.d.mts.map +0 -1
  231. /package/src/astro/routes/api/media/file/{[key].ts → [...key].ts} +0 -0
@@ -1,17 +1,17 @@
1
1
  import { o as jsonExtractExpr } from "./dialect-helpers-B9uSp2GJ.mjs";
2
2
  import { n as validateJsonFieldName, r as validatePluginIdentifier, t as validateIdentifier } from "./validate-CqRJb_xU.mjs";
3
- import { i as slugify, r as RevisionRepository, t as ContentRepository } from "./content-D6C2WsZC.mjs";
3
+ import { i as slugify, r as RevisionRepository, t as ContentRepository } from "./content-BmXndhdi.mjs";
4
4
  import { r as encodeBase64, t as decodeBase64 } from "./base64-MBPo9ozB.mjs";
5
5
  import { n as decodeCursor, r as encodeCursor, t as EmDashValidationError } from "./types-CMMN0pNg.mjs";
6
6
  import { t as MediaRepository } from "./media-DqHVh136.mjs";
7
- import { a as stripCredentialHeaders, i as ssrfSafeFetch, o as validateExternalUrl, r as SsrfError, u as OptionsRepository } from "./apply-Bjfq_b4-.mjs";
8
- import { a as withTransaction, i as FTSManager, n as SchemaRegistry } from "./registry-D_w5HW4G.mjs";
7
+ import { a as stripCredentialHeaders, f as OptionsRepository, i as ssrfSafeFetch, o as validateExternalUrl, r as SsrfError } from "./apply-wmVEOSbR.mjs";
8
+ import { a as withTransaction, i as FTSManager, n as SchemaRegistry } from "./registry-1EvbAfsC.mjs";
9
9
  import { t as RedirectRepository } from "./redirect-DIfIni3r.mjs";
10
- import { t as BylineRepository } from "./byline-CL847F26.mjs";
11
- import { i as isI18nEnabled } from "./config-CKE8p9xM.mjs";
12
- import { n as getDb } from "./loader-fz8Q_3EO.mjs";
13
- import { i as pluginManifestSchema } from "./manifest-schema-Dcl0R6nM.mjs";
14
- import { t as generatePreviewToken } from "./tokens-DpgrkrXK.mjs";
10
+ import { n as SQL_BATCH_SIZE, r as chunks, t as BylineRepository } from "./byline-1WQPlISL.mjs";
11
+ import { r as isI18nEnabled } from "./config-Cq8H0SfX.mjs";
12
+ import { n as getDb } from "./loader-CHb2v0jm.mjs";
13
+ import { i as pluginManifestSchema } from "./manifest-schema-CuMio1A9.mjs";
14
+ import { t as generatePreviewToken } from "./tokens-DrB-W6Q-.mjs";
15
15
  import { sql } from "kysely";
16
16
  import { ulid } from "ulidx";
17
17
  import { z } from "astro/zod";
@@ -323,20 +323,24 @@ var CommentRepository = class CommentRepository {
323
323
  }
324
324
  /**
325
325
  * Count comments grouped by status (for inbox badges)
326
+ *
327
+ * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes
328
+ * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)
329
+ * instead of a full table GROUP BY scan.
326
330
  */
327
331
  async countByStatus() {
328
- const rows = await this.db.selectFrom("_emdash_comments").select(["status"]).select((eb) => eb.fn.count("id").as("count")).groupBy("status").execute();
329
- const counts = {
330
- pending: 0,
331
- approved: 0,
332
- spam: 0,
333
- trash: 0
332
+ const [pending, approved, spam, trash] = await Promise.all([
333
+ this.db.selectFrom("_emdash_comments").select((eb) => eb.fn.count("id").as("count")).where("status", "=", "pending").executeTakeFirst(),
334
+ this.db.selectFrom("_emdash_comments").select((eb) => eb.fn.count("id").as("count")).where("status", "=", "approved").executeTakeFirst(),
335
+ this.db.selectFrom("_emdash_comments").select((eb) => eb.fn.count("id").as("count")).where("status", "=", "spam").executeTakeFirst(),
336
+ this.db.selectFrom("_emdash_comments").select((eb) => eb.fn.count("id").as("count")).where("status", "=", "trash").executeTakeFirst()
337
+ ]);
338
+ return {
339
+ pending: Number(pending?.count ?? 0),
340
+ approved: Number(approved?.count ?? 0),
341
+ spam: Number(spam?.count ?? 0),
342
+ trash: Number(trash?.count ?? 0)
334
343
  };
335
- for (const row of rows) {
336
- const status = row.status;
337
- if (status in counts) counts[status] = Number(row.count);
338
- }
339
- return counts;
340
344
  }
341
345
  /**
342
346
  * Count approved comments from a given email address.
@@ -823,6 +827,13 @@ var SeoRepository = class {
823
827
  this.db = db;
824
828
  }
825
829
  /**
830
+ * Check whether a collection has SEO enabled (`has_seo = 1`).
831
+ * Returns `false` if the collection does not exist.
832
+ */
833
+ async isEnabled(collection) {
834
+ return (await this.db.selectFrom("_emdash_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
835
+ }
836
+ /**
826
837
  * Get SEO data for a content item. Returns null defaults if no row exists.
827
838
  */
828
839
  async get(collection, contentId) {
@@ -837,24 +848,27 @@ var SeoRepository = class {
837
848
  };
838
849
  }
839
850
  /**
840
- * Get SEO data for multiple content items in a single query.
851
+ * Get SEO data for multiple content items.
841
852
  * Returns a Map keyed by content_id. Items without SEO rows get defaults.
853
+ *
854
+ * Chunks the `content_id IN (…)` clause so the total bound-parameter count
855
+ * per statement (ids + the `collection = ?` filter) stays within Cloudflare
856
+ * D1's 100-variable limit regardless of how many content items are passed.
842
857
  */
843
858
  async getMany(collection, contentIds) {
844
859
  const result = /* @__PURE__ */ new Map();
845
860
  if (contentIds.length === 0) return result;
846
- const rows = await this.db.selectFrom("_emdash_seo").selectAll().where("collection", "=", collection).where("content_id", "in", contentIds).execute();
847
- const rowMap = new Map(rows.map((r) => [r.content_id, r]));
848
- for (const id of contentIds) {
849
- const row = rowMap.get(id);
850
- if (row) result.set(id, {
861
+ for (const id of contentIds) result.set(id, { ...SEO_DEFAULTS$1 });
862
+ const uniqueContentIds = [...new Set(contentIds)];
863
+ for (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {
864
+ const rows = await this.db.selectFrom("_emdash_seo").selectAll().where("collection", "=", collection).where("content_id", "in", chunk).execute();
865
+ for (const row of rows) result.set(row.content_id, {
851
866
  title: row.seo_title ?? null,
852
867
  description: row.seo_description ?? null,
853
868
  image: row.seo_image ?? null,
854
869
  canonical: row.seo_canonical ?? null,
855
870
  noIndex: row.seo_no_index === 1
856
871
  });
857
- else result.set(id, { ...SEO_DEFAULTS$1 });
858
872
  }
859
873
  return result;
860
874
  }
@@ -1216,7 +1230,9 @@ async function handleContentCreate(db, collection, body) {
1216
1230
  status: body.status || "draft",
1217
1231
  authorId: body.authorId,
1218
1232
  locale: body.locale,
1219
- translationOf: body.translationOf
1233
+ translationOf: body.translationOf,
1234
+ createdAt: body.createdAt,
1235
+ publishedAt: body.publishedAt
1220
1236
  });
1221
1237
  if (body.bylines !== void 0) {
1222
1238
  await bylineRepo.setContentBylines(collection, created.id, body.bylines);
@@ -1450,6 +1466,7 @@ async function handleContentPermanentDelete(db, collection, id) {
1450
1466
  if (wasDeleted) {
1451
1467
  await new SeoRepository(trx).delete(collection, resolvedId);
1452
1468
  await new CommentRepository(trx).deleteByContent(collection, resolvedId);
1469
+ await new RevisionRepository(trx).deleteByEntry(collection, resolvedId);
1453
1470
  }
1454
1471
  return wasDeleted;
1455
1472
  })) return {
@@ -2625,7 +2642,7 @@ const contentListQuery = cursorPaginationQuery.extend({
2625
2642
  const contentCreateBody = z$1.object({
2626
2643
  data: z$1.record(z$1.string(), z$1.unknown()),
2627
2644
  slug: z$1.string().nullish(),
2628
- status: z$1.string().optional(),
2645
+ status: z$1.enum(["draft"]).optional(),
2629
2646
  bylines: z$1.array(contentBylineInputSchema).optional(),
2630
2647
  locale: localeCode.optional(),
2631
2648
  translationOf: z$1.string().optional(),
@@ -2634,7 +2651,7 @@ const contentCreateBody = z$1.object({
2634
2651
  const contentUpdateBody = z$1.object({
2635
2652
  data: z$1.record(z$1.string(), z$1.unknown()).optional(),
2636
2653
  slug: z$1.string().nullish(),
2637
- status: z$1.string().optional(),
2654
+ status: z$1.enum(["draft"]).optional(),
2638
2655
  authorId: z$1.string().nullish(),
2639
2656
  bylines: z$1.array(contentBylineInputSchema).optional(),
2640
2657
  _rev: z$1.string().optional().meta({ description: "Opaque revision token for optimistic concurrency" }),
@@ -2821,8 +2838,24 @@ const fieldTypeValues = z$1.enum([
2821
2838
  "file",
2822
2839
  "reference",
2823
2840
  "json",
2824
- "slug"
2841
+ "slug",
2842
+ "repeater"
2825
2843
  ]);
2844
+ const repeaterSubFieldSchema = z$1.object({
2845
+ slug: z$1.string().min(1).max(63).regex(slugPattern, "Invalid slug format"),
2846
+ type: z$1.enum([
2847
+ "string",
2848
+ "text",
2849
+ "number",
2850
+ "integer",
2851
+ "boolean",
2852
+ "datetime",
2853
+ "select"
2854
+ ]),
2855
+ label: z$1.string().min(1),
2856
+ required: z$1.boolean().optional(),
2857
+ options: z$1.array(z$1.string()).optional()
2858
+ });
2826
2859
  const fieldValidation = z$1.object({
2827
2860
  required: z$1.boolean().optional(),
2828
2861
  min: z$1.number().optional(),
@@ -2830,7 +2863,10 @@ const fieldValidation = z$1.object({
2830
2863
  minLength: z$1.number().int().min(0).optional(),
2831
2864
  maxLength: z$1.number().int().min(0).optional(),
2832
2865
  pattern: z$1.string().optional(),
2833
- options: z$1.array(z$1.string()).optional()
2866
+ options: z$1.array(z$1.string()).optional(),
2867
+ subFields: z$1.array(repeaterSubFieldSchema).min(1).optional(),
2868
+ minItems: z$1.number().int().min(0).optional(),
2869
+ maxItems: z$1.number().int().min(1).optional()
2834
2870
  }).optional();
2835
2871
  const fieldWidgetOptions = z$1.record(z$1.string(), z$1.unknown()).optional();
2836
2872
  const createCollectionBody = z$1.object({
@@ -3088,9 +3124,58 @@ const passkeyRegisterVerifyBody = z$1.object({
3088
3124
  const passkeyRenameBody = z$1.object({ name: z$1.string().min(1) }).meta({ id: "PasskeyRenameBody" });
3089
3125
  const authMeActionBody = z$1.object({ action: z$1.string().min(1) }).meta({ id: "AuthMeActionBody" });
3090
3126
 
3127
+ //#endregion
3128
+ //#region src/utils/url.ts
3129
+ /**
3130
+ * URL scheme validation utilities
3131
+ *
3132
+ * Prevents XSS via dangerous URL schemes (javascript:, data:, vbscript:, etc.)
3133
+ * by allowlisting known-safe schemes before rendering into href attributes.
3134
+ */
3135
+ /**
3136
+ * Matches URLs that are safe to render in href attributes.
3137
+ *
3138
+ * Allowed:
3139
+ * - http:// and https://
3140
+ * - mailto: and tel:
3141
+ * - Relative paths (starting with /)
3142
+ * - Fragment links (starting with #)
3143
+ * - Protocol-relative URLs are NOT allowed (starting with //) as they can
3144
+ * redirect to attacker-controlled hosts.
3145
+ */
3146
+ const SAFE_URL_SCHEME_RE = /^(https?:|mailto:|tel:|\/(?!\/)|#)/i;
3147
+ /**
3148
+ * Returns the URL unchanged if it uses a safe scheme, otherwise returns "#".
3149
+ *
3150
+ * Use this at the render layer as the primary defense against XSS via
3151
+ * dangerous URL schemes like `javascript:`, `data:`, or `vbscript:`.
3152
+ *
3153
+ * @example
3154
+ * ```ts
3155
+ * sanitizeHref("https://example.com") // "https://example.com"
3156
+ * sanitizeHref("/about") // "/about"
3157
+ * sanitizeHref("#section") // "#section"
3158
+ * sanitizeHref("mailto:a@b.com") // "mailto:a@b.com"
3159
+ * sanitizeHref("javascript:alert(1)") // "#"
3160
+ * sanitizeHref("data:text/html,<script>") // "#"
3161
+ * sanitizeHref("") // "#"
3162
+ * ```
3163
+ */
3164
+ function sanitizeHref(url) {
3165
+ if (!url) return "#";
3166
+ return SAFE_URL_SCHEME_RE.test(url) ? url : "#";
3167
+ }
3168
+ /**
3169
+ * Returns true if the URL uses a safe scheme for rendering in href attributes.
3170
+ */
3171
+ function isSafeHref(url) {
3172
+ return SAFE_URL_SCHEME_RE.test(url);
3173
+ }
3174
+
3091
3175
  //#endregion
3092
3176
  //#region src/api/schemas/menus.ts
3093
3177
  const menuItemType = z$1.string().min(1);
3178
+ const safeHref = z$1.string().trim().refine(isSafeHref, "URL must use http, https, mailto, tel, a relative path, or a fragment identifier");
3094
3179
  const createMenuBody = z$1.object({
3095
3180
  name: z$1.string().min(1),
3096
3181
  label: z$1.string().min(1)
@@ -3101,7 +3186,7 @@ const createMenuItemBody = z$1.object({
3101
3186
  label: z$1.string().min(1),
3102
3187
  referenceCollection: z$1.string().optional(),
3103
3188
  referenceId: z$1.string().optional(),
3104
- customUrl: z$1.string().optional(),
3189
+ customUrl: safeHref.optional(),
3105
3190
  target: z$1.string().optional(),
3106
3191
  titleAttr: z$1.string().optional(),
3107
3192
  cssClasses: z$1.string().optional(),
@@ -3110,7 +3195,7 @@ const createMenuItemBody = z$1.object({
3110
3195
  }).meta({ id: "CreateMenuItemBody" });
3111
3196
  const updateMenuItemBody = z$1.object({
3112
3197
  label: z$1.string().min(1).optional(),
3113
- customUrl: z$1.string().optional(),
3198
+ customUrl: safeHref.optional(),
3114
3199
  target: z$1.string().optional(),
3115
3200
  titleAttr: z$1.string().optional(),
3116
3201
  cssClasses: z$1.string().optional(),
@@ -3895,10 +3980,13 @@ function isTextBlock(block) {
3895
3980
  return block._type === "block";
3896
3981
  }
3897
3982
  /**
3898
- * Type guard for image blocks
3983
+ * Type guard for image blocks.
3984
+ * Checks both `_type` and that `asset` is a valid object — image blocks
3985
+ * without an `asset` wrapper (e.g. `{ _type: "image", url: "..." }`) are
3986
+ * malformed and should not be cast to `PortableTextImageBlock`.
3899
3987
  */
3900
3988
  function isImageBlock(block) {
3901
- return block._type === "image";
3989
+ return block._type === "image" && "asset" in block && typeof block.asset === "object" && block.asset !== null;
3902
3990
  }
3903
3991
  /**
3904
3992
  * Type guard for code blocks
@@ -3912,6 +4000,7 @@ function isCodeBlock(block) {
3912
4000
  function convertBlock(block) {
3913
4001
  if (isTextBlock(block)) return convertTextBlock(block);
3914
4002
  if (isImageBlock(block)) return convertImage(block);
4003
+ if (block._type === "image") return convertMalformedImage(block);
3915
4004
  if (isCodeBlock(block)) return convertCodeBlock(block);
3916
4005
  if (block._type === "break") return { type: "horizontalRule" };
3917
4006
  return {
@@ -4096,6 +4185,27 @@ function convertImage(block) {
4096
4185
  };
4097
4186
  }
4098
4187
  /**
4188
+ * Convert a malformed image block (missing `asset` wrapper) to ProseMirror.
4189
+ * Handles blocks like `{ _type: "image", url: "...", alt: "..." }` that may
4190
+ * originate from migrations or third-party imports.
4191
+ */
4192
+ function convertMalformedImage(block) {
4193
+ return {
4194
+ type: "image",
4195
+ attrs: {
4196
+ src: "url" in block && typeof block.url === "string" ? block.url : "",
4197
+ alt: "alt" in block && typeof block.alt === "string" ? block.alt : "",
4198
+ title: "caption" in block && typeof block.caption === "string" ? block.caption : "",
4199
+ mediaId: void 0,
4200
+ provider: void 0,
4201
+ width: "width" in block && typeof block.width === "number" ? block.width : void 0,
4202
+ height: "height" in block && typeof block.height === "number" ? block.height : void 0,
4203
+ displayWidth: "displayWidth" in block && typeof block.displayWidth === "number" ? block.displayWidth : void 0,
4204
+ displayHeight: "displayHeight" in block && typeof block.displayHeight === "number" ? block.displayHeight : void 0
4205
+ }
4206
+ };
4207
+ }
4208
+ /**
4099
4209
  * Convert code block to ProseMirror
4100
4210
  */
4101
4211
  function convertCodeBlock(block) {
@@ -4109,54 +4219,6 @@ function convertCodeBlock(block) {
4109
4219
  };
4110
4220
  }
4111
4221
 
4112
- //#endregion
4113
- //#region src/utils/url.ts
4114
- /**
4115
- * URL scheme validation utilities
4116
- *
4117
- * Prevents XSS via dangerous URL schemes (javascript:, data:, vbscript:, etc.)
4118
- * by allowlisting known-safe schemes before rendering into href attributes.
4119
- */
4120
- /**
4121
- * Matches URLs that are safe to render in href attributes.
4122
- *
4123
- * Allowed:
4124
- * - http:// and https://
4125
- * - mailto: and tel:
4126
- * - Relative paths (starting with /)
4127
- * - Fragment links (starting with #)
4128
- * - Protocol-relative URLs are NOT allowed (starting with //) as they can
4129
- * redirect to attacker-controlled hosts.
4130
- */
4131
- const SAFE_URL_SCHEME_RE = /^(https?:|mailto:|tel:|\/(?!\/)|#)/i;
4132
- /**
4133
- * Returns the URL unchanged if it uses a safe scheme, otherwise returns "#".
4134
- *
4135
- * Use this at the render layer as the primary defense against XSS via
4136
- * dangerous URL schemes like `javascript:`, `data:`, or `vbscript:`.
4137
- *
4138
- * @example
4139
- * ```ts
4140
- * sanitizeHref("https://example.com") // "https://example.com"
4141
- * sanitizeHref("/about") // "/about"
4142
- * sanitizeHref("#section") // "#section"
4143
- * sanitizeHref("mailto:a@b.com") // "mailto:a@b.com"
4144
- * sanitizeHref("javascript:alert(1)") // "#"
4145
- * sanitizeHref("data:text/html,<script>") // "#"
4146
- * sanitizeHref("") // "#"
4147
- * ```
4148
- */
4149
- function sanitizeHref(url) {
4150
- if (!url) return "#";
4151
- return SAFE_URL_SCHEME_RE.test(url) ? url : "#";
4152
- }
4153
- /**
4154
- * Returns true if the URL uses a safe scheme for rendering in href attributes.
4155
- */
4156
- function isSafeHref(url) {
4157
- return SAFE_URL_SCHEME_RE.test(url);
4158
- }
4159
-
4160
4222
  //#endregion
4161
4223
  //#region src/cli/wxr/parser.ts
4162
4224
  const PHP_SERIALIZED_STRING_PATTERN = /s:\d+:"([^"]+)"/g;
@@ -5304,21 +5366,45 @@ function createStorageAccess(db, pluginId, storageConfig) {
5304
5366
  return storage;
5305
5367
  }
5306
5368
  /**
5369
+ * Extract `seo` from a plugin-supplied content write input and return both
5370
+ * parts. Mutates nothing — returns a new field map without the `seo` key.
5371
+ */
5372
+ function splitSeoFromInput(input) {
5373
+ const { seo, ...fields } = input;
5374
+ if (seo !== void 0 && (seo === null || typeof seo !== "object" || Array.isArray(seo))) throw new Error("content.seo must be an object");
5375
+ return {
5376
+ fields,
5377
+ seo
5378
+ };
5379
+ }
5380
+ /**
5381
+ * Reject writing SEO to a collection that does not have it enabled.
5382
+ * Matches the REST API behavior (VALIDATION_ERROR).
5383
+ */
5384
+ async function assertSeoEnabled(seoRepo, collection, seo) {
5385
+ const hasSeo = await seoRepo.isEnabled(collection);
5386
+ if (seo !== void 0 && !hasSeo) throw new Error(`Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`);
5387
+ return hasSeo;
5388
+ }
5389
+ /**
5307
5390
  * Create read-only content access
5308
5391
  */
5309
5392
  function createContentAccess(db) {
5310
5393
  const contentRepo = new ContentRepository(db);
5394
+ const seoRepo = new SeoRepository(db);
5311
5395
  return {
5312
5396
  async get(collection, id) {
5313
5397
  const item = await contentRepo.findById(collection, id);
5314
5398
  if (!item) return null;
5315
- return {
5399
+ const result = {
5316
5400
  id: item.id,
5317
5401
  type: item.type,
5318
5402
  data: item.data,
5319
5403
  createdAt: item.createdAt,
5320
5404
  updatedAt: item.updatedAt
5321
5405
  };
5406
+ if (await seoRepo.isEnabled(collection)) result.seo = await seoRepo.get(collection, item.id);
5407
+ return result;
5322
5408
  },
5323
5409
  async list(collection, options) {
5324
5410
  let orderBy;
@@ -5334,14 +5420,22 @@ function createContentAccess(db) {
5334
5420
  cursor: options?.cursor,
5335
5421
  orderBy
5336
5422
  });
5423
+ const items = result.items.map((item) => ({
5424
+ id: item.id,
5425
+ type: item.type,
5426
+ data: item.data,
5427
+ createdAt: item.createdAt,
5428
+ updatedAt: item.updatedAt
5429
+ }));
5430
+ if (items.length > 0 && await seoRepo.isEnabled(collection)) {
5431
+ const seoMap = await seoRepo.getMany(collection, items.map((i) => i.id));
5432
+ for (const item of items) {
5433
+ const seo = seoMap.get(item.id);
5434
+ if (seo) item.seo = seo;
5435
+ }
5436
+ }
5337
5437
  return {
5338
- items: result.items.map((item) => ({
5339
- id: item.id,
5340
- type: item.type,
5341
- data: item.data,
5342
- createdAt: item.createdAt,
5343
- updatedAt: item.updatedAt
5344
- })),
5438
+ items,
5345
5439
  cursor: result.nextCursor,
5346
5440
  hasMore: !!result.nextCursor
5347
5441
  };
@@ -5349,37 +5443,62 @@ function createContentAccess(db) {
5349
5443
  };
5350
5444
  }
5351
5445
  /**
5352
- * Create full content access with write operations
5446
+ * Create full content access with write operations.
5447
+ *
5448
+ * `create` and `update` accept a reserved `seo` key in their `data`
5449
+ * argument. When present, it is routed to the core SEO panel
5450
+ * (`_emdash_seo`) via `SeoRepository.upsert`, in the same transaction as
5451
+ * the content write. The returned `ContentItem.seo` reflects the resulting
5452
+ * SEO state for SEO-enabled collections.
5353
5453
  */
5354
5454
  function createContentAccessWithWrite(db) {
5355
- const contentRepo = new ContentRepository(db);
5356
5455
  return {
5357
5456
  ...createContentAccess(db),
5358
5457
  async create(collection, data) {
5359
- const item = await contentRepo.create({
5360
- type: collection,
5361
- data
5458
+ const { fields, seo } = splitSeoFromInput(data);
5459
+ return withTransaction(db, async (trx) => {
5460
+ const trxContentRepo = new ContentRepository(trx);
5461
+ const trxSeoRepo = new SeoRepository(trx);
5462
+ const hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);
5463
+ const item = await trxContentRepo.create({
5464
+ type: collection,
5465
+ data: fields
5466
+ });
5467
+ const result = {
5468
+ id: item.id,
5469
+ type: item.type,
5470
+ data: item.data,
5471
+ createdAt: item.createdAt,
5472
+ updatedAt: item.updatedAt
5473
+ };
5474
+ if (hasSeo) result.seo = seo !== void 0 ? await trxSeoRepo.upsert(collection, item.id, seo) : await trxSeoRepo.get(collection, item.id);
5475
+ return result;
5362
5476
  });
5363
- return {
5364
- id: item.id,
5365
- type: item.type,
5366
- data: item.data,
5367
- createdAt: item.createdAt,
5368
- updatedAt: item.updatedAt
5369
- };
5370
5477
  },
5371
5478
  async update(collection, id, data) {
5372
- const item = await contentRepo.update(collection, id, { data });
5373
- return {
5374
- id: item.id,
5375
- type: item.type,
5376
- data: item.data,
5377
- createdAt: item.createdAt,
5378
- updatedAt: item.updatedAt
5379
- };
5479
+ const { fields, seo } = splitSeoFromInput(data);
5480
+ return withTransaction(db, async (trx) => {
5481
+ const trxContentRepo = new ContentRepository(trx);
5482
+ const trxSeoRepo = new SeoRepository(trx);
5483
+ const hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);
5484
+ const item = Object.keys(fields).length > 0 ? await trxContentRepo.update(collection, id, { data: fields }) : await (async () => {
5485
+ const existing = await trxContentRepo.findById(collection, id);
5486
+ if (!existing) throw new Error("Content not found");
5487
+ return existing;
5488
+ })();
5489
+ const result = {
5490
+ id: item.id,
5491
+ type: item.type,
5492
+ data: item.data,
5493
+ createdAt: item.createdAt,
5494
+ updatedAt: item.updatedAt
5495
+ };
5496
+ if (hasSeo) result.seo = seo !== void 0 ? await trxSeoRepo.upsert(collection, item.id, seo) : await trxSeoRepo.get(collection, item.id);
5497
+ return result;
5498
+ });
5380
5499
  },
5381
5500
  async delete(collection, id) {
5382
- return contentRepo.delete(collection, id);
5501
+ return new ContentRepository(db).delete(collection, id);
5383
5502
  }
5384
5503
  };
5385
5504
  }
@@ -5471,6 +5590,7 @@ function createMediaAccessWithWrite(db, getUploadUrlFn, storage) {
5471
5590
  const MAX_PLUGIN_REDIRECTS = 5;
5472
5591
  function isHostAllowed(host, allowedHosts) {
5473
5592
  return allowedHosts.some((pattern) => {
5593
+ if (pattern === "*") return true;
5474
5594
  if (pattern.startsWith("*.")) {
5475
5595
  const suffix = pattern.slice(1);
5476
5596
  return host.endsWith(suffix) || host === pattern.slice(2);
@@ -5792,6 +5912,8 @@ var HookPipeline = class HookPipeline {
5792
5912
  this.registerPluginHook(plugin, "content:afterSave");
5793
5913
  this.registerPluginHook(plugin, "content:beforeDelete");
5794
5914
  this.registerPluginHook(plugin, "content:afterDelete");
5915
+ this.registerPluginHook(plugin, "content:afterPublish");
5916
+ this.registerPluginHook(plugin, "content:afterUnpublish");
5795
5917
  this.registerPluginHook(plugin, "media:beforeUpload");
5796
5918
  this.registerPluginHook(plugin, "media:afterUpload");
5797
5919
  this.registerPluginHook(plugin, "cron");
@@ -5822,6 +5944,8 @@ var HookPipeline = class HookPipeline {
5822
5944
  ["content:afterSave", "read:content"],
5823
5945
  ["content:beforeDelete", "read:content"],
5824
5946
  ["content:afterDelete", "read:content"],
5947
+ ["content:afterPublish", "read:content"],
5948
+ ["content:afterUnpublish", "read:content"],
5825
5949
  ["media:beforeUpload", "write:media"],
5826
5950
  ["media:afterUpload", "read:media"],
5827
5951
  ["comment:beforeCreate", "read:users"],
@@ -6100,6 +6224,72 @@ var HookPipeline = class HookPipeline {
6100
6224
  return results;
6101
6225
  }
6102
6226
  /**
6227
+ * Run content:afterPublish hooks (fire-and-forget).
6228
+ */
6229
+ async runContentAfterPublish(content, collection) {
6230
+ const hooks = this.getTypedHooks("content:afterPublish");
6231
+ const results = [];
6232
+ for (const hook of hooks) {
6233
+ const { handler } = hook;
6234
+ const event = {
6235
+ content,
6236
+ collection
6237
+ };
6238
+ const ctx = this.getContext(hook.pluginId);
6239
+ const start = Date.now();
6240
+ try {
6241
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
6242
+ results.push({
6243
+ success: true,
6244
+ pluginId: hook.pluginId,
6245
+ duration: Date.now() - start
6246
+ });
6247
+ } catch (error) {
6248
+ results.push({
6249
+ success: false,
6250
+ error: error instanceof Error ? error : new Error(String(error)),
6251
+ pluginId: hook.pluginId,
6252
+ duration: Date.now() - start
6253
+ });
6254
+ if (hook.errorPolicy === "abort") throw error;
6255
+ }
6256
+ }
6257
+ return results;
6258
+ }
6259
+ /**
6260
+ * Run content:afterUnpublish hooks (fire-and-forget).
6261
+ */
6262
+ async runContentAfterUnpublish(content, collection) {
6263
+ const hooks = this.getTypedHooks("content:afterUnpublish");
6264
+ const results = [];
6265
+ for (const hook of hooks) {
6266
+ const { handler } = hook;
6267
+ const event = {
6268
+ content,
6269
+ collection
6270
+ };
6271
+ const ctx = this.getContext(hook.pluginId);
6272
+ const start = Date.now();
6273
+ try {
6274
+ await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
6275
+ results.push({
6276
+ success: true,
6277
+ pluginId: hook.pluginId,
6278
+ duration: Date.now() - start
6279
+ });
6280
+ } catch (error) {
6281
+ results.push({
6282
+ success: false,
6283
+ error: error instanceof Error ? error : new Error(String(error)),
6284
+ pluginId: hook.pluginId,
6285
+ duration: Date.now() - start
6286
+ });
6287
+ if (hook.errorPolicy === "abort") throw error;
6288
+ }
6289
+ }
6290
+ return results;
6291
+ }
6292
+ /**
6103
6293
  * Run media:beforeUpload hooks
6104
6294
  */
6105
6295
  async runMediaBeforeUpload(file) {
@@ -6942,6 +7132,14 @@ var PluginManager = class {
6942
7132
  };
6943
7133
  }
6944
7134
  /**
7135
+ * Set the email pipeline used when creating plugin contexts.
7136
+ * Reinitializes routes/hooks if already initialized so ctx.email is available immediately.
7137
+ */
7138
+ setEmailPipeline(pipeline) {
7139
+ this.factoryOptions.emailPipeline = pipeline;
7140
+ if (this.initialized) this.reinitialize();
7141
+ }
7142
+ /**
6945
7143
  * Register a plugin definition
6946
7144
  * This resolves the definition and adds it to the manager, but doesn't install it
6947
7145
  */
@@ -7064,6 +7262,20 @@ var PluginManager = class {
7064
7262
  return this.hookPipeline.runContentAfterDelete(id, collection);
7065
7263
  }
7066
7264
  /**
7265
+ * Run content:afterPublish hooks across all active plugins
7266
+ */
7267
+ async runContentAfterPublish(content, collection) {
7268
+ this.ensureInitialized();
7269
+ return this.hookPipeline.runContentAfterPublish(content, collection);
7270
+ }
7271
+ /**
7272
+ * Run content:afterUnpublish hooks across all active plugins
7273
+ */
7274
+ async runContentAfterUnpublish(content, collection) {
7275
+ this.ensureInitialized();
7276
+ return this.hookPipeline.runContentAfterUnpublish(content, collection);
7277
+ }
7278
+ /**
7067
7279
  * Run media:beforeUpload hooks across all active plugins
7068
7280
  */
7069
7281
  async runMediaBeforeUpload(file) {
@@ -7848,8 +8060,8 @@ function wxrPostToNormalizedItem(post, attachmentMap) {
7848
8060
  title: post.title || "Untitled",
7849
8061
  content,
7850
8062
  excerpt: post.excerpt,
7851
- date: post.postDate ? new Date(post.postDate) : /* @__PURE__ */ new Date(),
7852
- modified: post.postModified ? new Date(post.postModified) : void 0,
8063
+ date: parseWxrDate(post.postDateGmt, post.pubDate, post.postDate) ?? /* @__PURE__ */ new Date(),
8064
+ modified: parseWxrDate(post.postModifiedGmt, void 0, post.postModified),
7853
8065
  author: post.creator,
7854
8066
  categories: post.categories,
7855
8067
  tags: post.tags,
@@ -7860,6 +8072,31 @@ function wxrPostToNormalizedItem(post, attachmentMap) {
7860
8072
  customTaxonomies
7861
8073
  };
7862
8074
  }
8075
+ /**
8076
+ * WordPress uses "0000-00-00 00:00:00" as a sentinel for missing GMT dates
8077
+ * (e.g. unpublished drafts). This must be treated as absent.
8078
+ */
8079
+ const WXR_ZERO_DATE = "0000-00-00 00:00:00";
8080
+ /**
8081
+ * Parse a WXR date with the correct fallback chain:
8082
+ * 1. GMT date (always UTC, most reliable)
8083
+ * 2. pubDate (RFC 2822, includes timezone offset)
8084
+ * 3. Site-local date (MySQL datetime without timezone, imprecise but best available)
8085
+ *
8086
+ * Returns undefined when none of the inputs yield a valid date.
8087
+ * Callers that need a guaranteed Date should use `?? new Date()`.
8088
+ */
8089
+ function parseWxrDate(gmtDate, pubDate, localDate) {
8090
+ if (gmtDate && gmtDate !== WXR_ZERO_DATE) return /* @__PURE__ */ new Date(gmtDate.replace(" ", "T") + "Z");
8091
+ if (pubDate) {
8092
+ const d = new Date(pubDate);
8093
+ if (!isNaN(d.getTime())) return d;
8094
+ }
8095
+ if (localDate) {
8096
+ const d = new Date(localDate.replace(" ", "T"));
8097
+ if (!isNaN(d.getTime())) return d;
8098
+ }
8099
+ }
7863
8100
 
7864
8101
  //#endregion
7865
8102
  //#region src/import/sources/wordpress-rest.ts
@@ -8499,7 +8736,7 @@ async function resolveMenuItem(item, db, urlPatterns) {
8499
8736
  return {
8500
8737
  id: item.id,
8501
8738
  label: item.label,
8502
- url,
8739
+ url: sanitizeHref(url),
8503
8740
  target: item.target || void 0,
8504
8741
  titleAttr: item.title_attr || void 0,
8505
8742
  cssClasses: item.css_classes || void 0,
@@ -8699,7 +8936,7 @@ async function getTermsForEntries(collection, entryIds, taxonomyName) {
8699
8936
  * Get entries by term (wraps getEmDashCollection)
8700
8937
  */
8701
8938
  async function getEntriesByTerm(collection, taxonomyName, termSlug) {
8702
- const { getEmDashCollection } = await import("./query-CS_iSj34.mjs").then((n) => n.a);
8939
+ const { getEmDashCollection } = await import("./query-5Hcv_5ER.mjs").then((n) => n.a);
8703
8940
  const { entries } = await getEmDashCollection(collection, { where: { [taxonomyName]: termSlug } });
8704
8941
  return entries;
8705
8942
  }
@@ -9207,5 +9444,5 @@ function extractSearchableFields(entry, fields) {
9207
9444
  }
9208
9445
 
9209
9446
  //#endregion
9210
- export { definePlugin as $, getFileSources as A, handleContentGetIncludingTrashed as At, PluginManager as B, handleContentUpdate as Bt, isPreviewRequest as C, handleContentCountScheduled as Ct, wxrSource as D, handleContentDiscardDraft as Dt, wordpressRestSource as E, handleContentDelete as Et, importReusableBlocksAsSections as F, handleContentRestore as Ft, devConsoleEmailDeliver as G, PluginRouteError as H, portableText as Ht, isStandardPluginDefinition as I, handleContentSchedule as It, createHookPipeline as J, EmailPipeline as K, NoopSandboxRunner as L, handleContentTranslations as Lt, getUrlSources as M, handleContentListTrashed as Mt, probeUrl as N, handleContentPermanentDelete as Nt, clearSources as O, handleContentDuplicate as Ot, registerSource as P, handleContentPublish as Pt, sanitizeHeadersForSandbox as Q, SandboxNotAvailableError as R, handleContentUnpublish as Rt, getPreviewToken as S, handleContentCompare as St, getPreviewUrl as T, handleContentCreate as Tt, PluginRouteRegistry as U, reference as Ut, createPluginManager as V, validateRev as Vt, DEV_CONSOLE_EMAIL_PLUGIN_ID as W, image as Wt, CronExecutor as X, resolveExclusiveHooks as Y, extractRequestMeta as Z, getTermsForEntries as _, handleRevisionList as _t, search as a, prosemirrorToPortableText as at, getCommentCount as b, computeContentHash as bt, getWidgetArea as c, getSections as ct, getEntriesByTerm as d, handleMediaCreate as dt, parseWxr as et, getEntryTerms as f, handleMediaDelete as ft, getTerm as g, handleRevisionGet as gt, getTaxonomyTerms as h, handleMediaUpdate as ht, getSuggestions as i, portableTextToProsemirror as it, getSource as j, handleContentList as jt, getAllSources as k, handleContentGet as kt, getWidgetAreas as l, PluginStateRepository as lt, getTaxonomyDefs as m, handleMediaList as mt, extractSearchableFields as n, isSafeHref as nt, searchCollection as o, loadBundleFromR2 as ot, getTaxonomyDef as p, handleMediaGet as pt, HookPipeline as q, getSearchStats as r, sanitizeHref as rt, searchWithDb as s, getSection as st, extractPlainText as t, parseWxrString as tt, getWidgetComponents as u, getCollectionInfo as ut, getMenu as v, handleRevisionRestore as vt, buildPreviewUrl as w, handleContentCountTrashed as wt, getComments as x, hashString as xt, getMenus as y, generateManifest as yt, createNoopSandboxRunner as z, handleContentUnschedule as zt };
9211
- //# sourceMappingURL=search-DG603UrT.mjs.map
9447
+ export { sanitizeHeadersForSandbox as $, getAllSources as A, handleContentGet as At, createNoopSandboxRunner as B, handleContentUnschedule as Bt, isPreviewRequest as C, handleContentCompare as Ct, parseWxrDate as D, handleContentDelete as Dt, wordpressRestSource as E, handleContentCreate as Et, registerSource as F, handleContentPublish as Ft, DEV_CONSOLE_EMAIL_PLUGIN_ID as G, image as Gt, createPluginManager as H, validateRev as Ht, importReusableBlocksAsSections as I, handleContentRestore as It, HookPipeline as J, devConsoleEmailDeliver as K, isStandardPluginDefinition as L, handleContentSchedule as Lt, getSource as M, handleContentList as Mt, getUrlSources as N, handleContentListTrashed as Nt, wxrSource as O, handleContentDiscardDraft as Ot, probeUrl as P, handleContentPermanentDelete as Pt, extractRequestMeta as Q, NoopSandboxRunner as R, handleContentTranslations as Rt, getPreviewToken as S, hashString as St, getPreviewUrl as T, handleContentCountTrashed as Tt, PluginRouteError as U, portableText as Ut, PluginManager as V, handleContentUpdate as Vt, PluginRouteRegistry as W, reference as Wt, resolveExclusiveHooks as X, createHookPipeline as Y, CronExecutor as Z, getTermsForEntries as _, handleRevisionGet as _t, search as a, isSafeHref as at, getCommentCount as b, generateManifest as bt, getWidgetArea as c, getSection as ct, getEntriesByTerm as d, getCollectionInfo as dt, definePlugin as et, getEntryTerms as f, handleMediaCreate as ft, getTerm as g, handleMediaUpdate as gt, getTaxonomyTerms as h, handleMediaList as ht, getSuggestions as i, prosemirrorToPortableText as it, getFileSources as j, handleContentGetIncludingTrashed as jt, clearSources as k, handleContentDuplicate as kt, getWidgetAreas as l, getSections as lt, getTaxonomyDefs as m, handleMediaGet as mt, extractSearchableFields as n, parseWxrString as nt, searchCollection as o, sanitizeHref as ot, getTaxonomyDef as p, handleMediaDelete as pt, EmailPipeline as q, getSearchStats as r, portableTextToProsemirror as rt, searchWithDb as s, loadBundleFromR2 as st, extractPlainText as t, parseWxr as tt, getWidgetComponents as u, PluginStateRepository as ut, getMenu as v, handleRevisionList as vt, buildPreviewUrl as w, handleContentCountScheduled as wt, getComments as x, computeContentHash as xt, getMenus as y, handleRevisionRestore as yt, SandboxNotAvailableError as z, handleContentUnpublish as zt };
9448
+ //# sourceMappingURL=search-BsYMed12.mjs.map