emdash 0.9.0 → 0.11.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 (239) hide show
  1. package/dist/{adapters-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-BzltprvY.mjs → apply-Ded_1vng.mjs} +167 -254
  4. package/dist/apply-Ded_1vng.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.mjs +10 -2
  7. package/dist/astro/index.mjs.map +1 -1
  8. package/dist/astro/middleware/auth.d.mts +5 -5
  9. package/dist/astro/middleware/auth.mjs +5 -5
  10. package/dist/astro/middleware/redirect.mjs +5 -5
  11. package/dist/astro/middleware/request-context.mjs +4 -4
  12. package/dist/astro/middleware/setup.mjs +1 -1
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +94 -43
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/types.d.mts +12 -11
  17. package/dist/astro/types.d.mts.map +1 -1
  18. package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
  19. package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
  20. package/dist/{byline-BSaNL1w7.mjs → byline-gFn1r0vA.mjs} +4 -4
  21. package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
  22. package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
  23. package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
  24. package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
  25. package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
  26. package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
  27. package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
  28. package/dist/cli/index.mjs +342 -95
  29. package/dist/cli/index.mjs.map +1 -1
  30. package/dist/client/cf-access.d.mts +1 -1
  31. package/dist/client/index.d.mts +1 -1
  32. package/dist/client/index.mjs +1 -1
  33. package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
  34. package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
  35. package/dist/{content-8lOYF0pr.mjs → content-CERxPUN0.mjs} +14 -3
  36. package/dist/content-CERxPUN0.mjs.map +1 -0
  37. package/dist/database/instrumentation.d.mts +6 -4
  38. package/dist/database/instrumentation.d.mts.map +1 -1
  39. package/dist/database/instrumentation.mjs +19 -7
  40. package/dist/database/instrumentation.mjs.map +1 -1
  41. package/dist/db/index.d.mts +3 -3
  42. package/dist/db/index.mjs +1 -1
  43. package/dist/db/libsql.d.mts +1 -1
  44. package/dist/db/postgres.d.mts +1 -1
  45. package/dist/db/sqlite.d.mts +1 -1
  46. package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  47. package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  48. package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
  49. package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  50. package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
  51. package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  52. package/dist/{index-BFRaVcD6.d.mts → index-Cg-rC4Gj.d.mts} +110 -87
  53. package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
  54. package/dist/index.d.mts +11 -11
  55. package/dist/index.mjs +29 -28
  56. package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
  57. package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
  58. package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
  59. package/dist/loader-ou_PXAjg.mjs.map +1 -0
  60. package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
  61. package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
  62. package/dist/media/index.d.mts +1 -1
  63. package/dist/media/index.mjs +1 -1
  64. package/dist/media/local-runtime.d.mts +7 -7
  65. package/dist/media/local-runtime.mjs +3 -3
  66. package/dist/{media-BW32b4gi.mjs → media-1fFhub9c.mjs} +22 -10
  67. package/dist/media-1fFhub9c.mjs.map +1 -0
  68. package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
  69. package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  70. package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
  71. package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
  72. package/dist/page/index.d.mts +2 -2
  73. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  74. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  75. package/dist/{placeholder-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  76. package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  77. package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  78. package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  79. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  80. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  81. package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
  82. package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
  83. package/dist/{query-Cg9ZKRQ0.mjs → query-8c_meo_K.mjs} +13 -13
  84. package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.mjs.map} +1 -1
  85. package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
  86. package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
  87. package/dist/{registry-Dw70ChxB.mjs → registry-Do34mz_P.mjs} +7 -6
  88. package/dist/registry-Do34mz_P.mjs.map +1 -0
  89. package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
  90. package/dist/request-cache-D4I69LeL.mjs.map +1 -0
  91. package/dist/request-context.d.mts +27 -1
  92. package/dist/request-context.d.mts.map +1 -1
  93. package/dist/request-context.mjs +16 -3
  94. package/dist/request-context.mjs.map +1 -1
  95. package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
  96. package/dist/runner-DIcU2UCC.mjs.map +1 -0
  97. package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
  98. package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
  99. package/dist/runtime.d.mts +6 -6
  100. package/dist/runtime.mjs +3 -3
  101. package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
  102. package/dist/search-DuWhx4NG.mjs.map +1 -0
  103. package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
  104. package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
  105. package/dist/seed/index.d.mts +2 -2
  106. package/dist/seed/index.mjs +15 -14
  107. package/dist/seo/index.d.mts +1 -1
  108. package/dist/storage/local.d.mts +1 -1
  109. package/dist/storage/local.mjs +1 -1
  110. package/dist/storage/s3.d.mts +1 -1
  111. package/dist/storage/s3.mjs +1 -1
  112. package/dist/taxonomies-Bw76xAxo.mjs +407 -0
  113. package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
  114. package/dist/taxonomy-D6NvlKo8.mjs +218 -0
  115. package/dist/taxonomy-D6NvlKo8.mjs.map +1 -0
  116. package/dist/{tokens-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
  117. package/dist/{tokens-D7zMmWi2.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  118. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  119. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  120. package/dist/{transport-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  121. package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  122. package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
  123. package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  124. package/dist/{types-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
  125. package/dist/types-56BKbld_.mjs.map +1 -0
  126. package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
  127. package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
  128. package/dist/{types-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
  129. package/dist/types-BQx6ZXpR.d.mts.map +1 -0
  130. package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
  131. package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  132. package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
  133. package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  134. package/dist/types-DiI8NOG_.mjs +16 -0
  135. package/dist/types-DiI8NOG_.mjs.map +1 -0
  136. package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
  137. package/dist/types-IN5z_S3P.d.mts.map +1 -0
  138. package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
  139. package/dist/types-IZSZfEwv.d.mts.map +1 -0
  140. package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
  141. package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  142. package/dist/{validate-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
  143. package/dist/validate-CO3JjFV5.d.mts.map +1 -0
  144. package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
  145. package/dist/validate-UK4Ja1uo.mjs.map +1 -0
  146. package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
  147. package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
  148. package/dist/version-Bg31I_Ff.mjs +7 -0
  149. package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
  150. package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
  151. package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
  152. package/package.json +9 -8
  153. package/src/api/errors.ts +5 -0
  154. package/src/api/handlers/content.ts +20 -0
  155. package/src/api/handlers/dashboard.ts +29 -36
  156. package/src/api/handlers/media-allowlist.ts +40 -0
  157. package/src/api/handlers/media.ts +1 -1
  158. package/src/api/handlers/menus.ts +400 -89
  159. package/src/api/handlers/taxonomies.ts +273 -97
  160. package/src/api/handlers/validate-media-fields.ts +125 -0
  161. package/src/api/schemas/common.ts +7 -0
  162. package/src/api/schemas/media.ts +23 -3
  163. package/src/api/schemas/menus.ts +23 -0
  164. package/src/api/schemas/schema.ts +11 -2
  165. package/src/api/schemas/taxonomies.ts +39 -0
  166. package/src/astro/integration/routes.ts +10 -0
  167. package/src/astro/middleware.ts +46 -11
  168. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  169. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  170. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  171. package/src/astro/routes/api/media/upload-url.ts +10 -4
  172. package/src/astro/routes/api/media.ts +12 -4
  173. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  174. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  175. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  176. package/src/astro/routes/api/menus/[name].ts +19 -10
  177. package/src/astro/routes/api/menus/index.ts +9 -6
  178. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  179. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  180. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  181. package/src/astro/routes/api/taxonomies/index.ts +9 -6
  182. package/src/astro/types.ts +5 -1
  183. package/src/auth/rate-limit.ts +3 -3
  184. package/src/cli/commands/bundle-utils.ts +81 -6
  185. package/src/cli/commands/bundle.ts +18 -15
  186. package/src/cli/commands/export-seed.ts +139 -24
  187. package/src/cli/commands/plugin-init.ts +216 -90
  188. package/src/database/instrumentation.ts +22 -8
  189. package/src/database/migrations/016_api_tokens.ts +18 -3
  190. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  191. package/src/database/migrations/037_credential_algorithm.ts +18 -0
  192. package/src/database/migrations/runner.ts +4 -0
  193. package/src/database/repositories/content.ts +11 -0
  194. package/src/database/repositories/media.ts +40 -10
  195. package/src/database/repositories/taxonomy.ts +193 -89
  196. package/src/database/types.ts +12 -3
  197. package/src/emdash-runtime.ts +16 -3
  198. package/src/fields/file.ts +7 -6
  199. package/src/fields/image.ts +12 -11
  200. package/src/fields/types.ts +3 -0
  201. package/src/i18n/resolve.ts +37 -0
  202. package/src/index.ts +1 -1
  203. package/src/loader.ts +49 -2
  204. package/src/mcp/server.ts +114 -26
  205. package/src/media/mime.ts +75 -0
  206. package/src/menus/index.ts +143 -124
  207. package/src/menus/types.ts +15 -1
  208. package/src/plugins/types.ts +81 -191
  209. package/src/request-cache.ts +6 -2
  210. package/src/request-context.ts +42 -2
  211. package/src/schema/registry.ts +5 -5
  212. package/src/schema/types.ts +3 -2
  213. package/src/schema/zod-generator.ts +12 -2
  214. package/src/seed/apply.ts +157 -54
  215. package/src/seed/types.ts +18 -1
  216. package/src/seed/validate.ts +27 -13
  217. package/src/taxonomies/index.ts +230 -213
  218. package/src/taxonomies/types.ts +10 -0
  219. package/dist/apply-BzltprvY.mjs.map +0 -1
  220. package/dist/content-8lOYF0pr.mjs.map +0 -1
  221. package/dist/index-BFRaVcD6.d.mts.map +0 -1
  222. package/dist/loader-CKLbBnhK.mjs.map +0 -1
  223. package/dist/media-BW32b4gi.mjs.map +0 -1
  224. package/dist/registry-Dw70ChxB.mjs.map +0 -1
  225. package/dist/request-cache-B-bmkipQ.mjs.map +0 -1
  226. package/dist/runner-C7ADox5q.mjs.map +0 -1
  227. package/dist/search-dOGEccMa.mjs.map +0 -1
  228. package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
  229. package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
  230. package/dist/types-4fVtCIm0.mjs +0 -68
  231. package/dist/types-4fVtCIm0.mjs.map +0 -1
  232. package/dist/types-BSyXeCFW.d.mts.map +0 -1
  233. package/dist/types-BuBIptGk.d.mts.map +0 -1
  234. package/dist/types-CIOg5AR8.mjs.map +0 -1
  235. package/dist/types-CrtWgIvl.d.mts.map +0 -1
  236. package/dist/validate-Baqf0slj.mjs.map +0 -1
  237. package/dist/validate-BfQh_C_y.d.mts.map +0 -1
  238. package/dist/version-DoxrVdYf.mjs +0 -7
  239. package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
@@ -1,25 +1,26 @@
1
1
  import { n as validateJsonFieldName, r as validatePluginIdentifier, t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
2
2
  import { s as jsonExtractExpr } from "./dialect-helpers-BKCvISIQ.mjs";
3
- import { a as slugify, r as RevisionRepository, t as ContentRepository } from "./content-8lOYF0pr.mjs";
4
- import { r as encodeBase64, t as decodeBase64 } from "./base64-BRICGH2l.mjs";
5
- import { i as encodeCursor, n as InvalidCursorError, r as decodeCursor, t as EmDashValidationError } from "./types-CRxNbK-Z.mjs";
6
- import { t as MediaRepository } from "./media-BW32b4gi.mjs";
7
- import { a as ssrfSafeFetch, i as resolveAndValidateExternalUrl, o as stripCredentialHeaders, r as SsrfError, s as validateExternalUrl } from "./apply-BzltprvY.mjs";
8
- import { t as OptionsRepository } from "./options-BVp3UsTS.mjs";
9
- import { t as withTransaction } from "./transaction-Cn2rjY78.mjs";
10
- import { t as RedirectRepository } from "./redirect-BhUBKRc1.mjs";
11
- import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-NBQVDOci.mjs";
12
- import { t as BylineRepository } from "./byline-BSaNL1w7.mjs";
13
- import { r as isI18nEnabled } from "./config-BI0V3ICQ.mjs";
14
- import { r as invalidateRedirectCache } from "./cache-C6N_hhN7.mjs";
15
- import { t as isMissingTableError } from "./db-errors-WRezodiz.mjs";
16
- import { r as hashString } from "./zod-generator-CC0xNe_K.mjs";
17
- import { i as FTSManager, n as SchemaRegistry } from "./registry-Dw70ChxB.mjs";
18
- import { r as getDb } from "./loader-CKLbBnhK.mjs";
19
- import { n as requestCached } from "./request-cache-B-bmkipQ.mjs";
20
- import { i as pluginManifestSchema } from "./manifest-schema-DqWNC3lM.mjs";
21
- import { i as normalizeCapabilities } from "./types-4fVtCIm0.mjs";
22
- import { t as generatePreviewToken } from "./tokens-D7zMmWi2.mjs";
3
+ import { r as isI18nEnabled } from "./config-CVssduLe.mjs";
4
+ import { a as slugify, r as RevisionRepository, t as ContentRepository } from "./content-CERxPUN0.mjs";
5
+ import { r as encodeBase64, t as decodeBase64 } from "./base64-MBPo9ozB.mjs";
6
+ import { i as encodeCursor, n as InvalidCursorError, r as decodeCursor, t as EmDashValidationError } from "./types-BIgulNsW.mjs";
7
+ import { t as MediaRepository } from "./media-1fFhub9c.mjs";
8
+ import { t as OptionsRepository } from "./options-nPxWnrya.mjs";
9
+ import { t as withTransaction } from "./transaction-D44LBXvU.mjs";
10
+ import { t as RedirectRepository } from "./redirect-C5H7VGIX.mjs";
11
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BK1oZS-l.mjs";
12
+ import { t as BylineRepository } from "./byline-gFn1r0vA.mjs";
13
+ import { r as invalidateRedirectCache } from "./cache-BAJbeoZ8.mjs";
14
+ import { t as isMissingTableError } from "./db-errors-B7P2pSCn.mjs";
15
+ import { n as requestCached } from "./request-cache-D4I69LeL.mjs";
16
+ import { r as hashString } from "./zod-generator-CHnJUP2l.mjs";
17
+ import { i as FTSManager, n as SchemaRegistry } from "./registry-Do34mz_P.mjs";
18
+ import { r as getDb } from "./loader-ou_PXAjg.mjs";
19
+ import { a as ssrfSafeFetch, i as resolveAndValidateExternalUrl, o as stripCredentialHeaders, r as SsrfError, s as validateExternalUrl } from "./apply-Ded_1vng.mjs";
20
+ import { d as resolveLocale, f as resolveLocaleChain } from "./taxonomies-Bw76xAxo.mjs";
21
+ import { i as pluginManifestSchema } from "./manifest-schema-CXAbd1vH.mjs";
22
+ import { i as normalizeCapabilities } from "./types-DiI8NOG_.mjs";
23
+ import { t as generatePreviewToken } from "./tokens-CyRDPVW2.mjs";
23
24
  import { sql } from "kysely";
24
25
  import { AsyncLocalStorage } from "node:async_hooks";
25
26
  import { ulid } from "ulidx";
@@ -738,9 +739,6 @@ var PluginStorageRepository = class {
738
739
 
739
740
  //#endregion
740
741
  //#region src/fields/image.ts
741
- /**
742
- * Image field schema
743
- */
744
742
  const imageSchema = z.object({
745
743
  id: z.string(),
746
744
  src: z.string(),
@@ -748,17 +746,39 @@ const imageSchema = z.object({
748
746
  width: z.number().optional(),
749
747
  height: z.number().optional()
750
748
  });
751
- /**
752
- * Image field
753
- * References media items from the media library
754
- */
755
- function image(options) {
749
+ function image(options = {}) {
750
+ const validation = options.allowedTypes && options.allowedTypes.length > 0 ? { allowedMimeTypes: [...options.allowedTypes] } : void 0;
756
751
  return {
757
752
  type: "image",
758
753
  columnType: "TEXT",
759
- schema: options?.required === false ? imageSchema.optional() : imageSchema,
754
+ schema: options.required === false ? imageSchema.optional() : imageSchema,
760
755
  options,
761
- ui: { widget: "image" }
756
+ ui: { widget: "image" },
757
+ validation
758
+ };
759
+ }
760
+
761
+ //#endregion
762
+ //#region src/fields/file.ts
763
+ function file(options = {}) {
764
+ const fileObjSchema = z.object({
765
+ id: z.string(),
766
+ url: z.string(),
767
+ filename: z.string(),
768
+ mimeType: z.string(),
769
+ size: z.number()
770
+ });
771
+ return {
772
+ type: "file",
773
+ columnType: "TEXT",
774
+ schema: options.required ? fileObjSchema : fileObjSchema.optional(),
775
+ options,
776
+ ui: {
777
+ widget: "file",
778
+ helpText: options.helpText,
779
+ maxSize: options.maxSize
780
+ },
781
+ validation: options.allowedTypes && options.allowedTypes.length > 0 ? { allowedMimeTypes: [...options.allowedTypes] } : void 0
762
782
  };
763
783
  }
764
784
 
@@ -977,6 +997,115 @@ function validateRev(rev, item) {
977
997
  return { valid: true };
978
998
  }
979
999
 
1000
+ //#endregion
1001
+ //#region src/media/mime.ts
1002
+ function normalizeMime(mime) {
1003
+ return mime.split(";")[0].trim().toLowerCase();
1004
+ }
1005
+ function matchesMimeAllowlist(mime, allowList) {
1006
+ const normalized = normalizeMime(mime);
1007
+ for (const entry of allowList) {
1008
+ if (!entry || !entry.includes("/")) continue;
1009
+ const normalizedEntry = normalizeMime(entry);
1010
+ if (normalizedEntry.endsWith("/")) {
1011
+ if (normalized.startsWith(normalizedEntry)) return true;
1012
+ } else if (normalized === normalizedEntry) return true;
1013
+ }
1014
+ return false;
1015
+ }
1016
+ /**
1017
+ * Extract the `allowedMimeTypes` list from a `_emdash_fields.validation` row
1018
+ * (raw JSON string). Returns null when the value is missing, malformed, or the
1019
+ * list is empty — callers treat that as "no field-specific constraint".
1020
+ */
1021
+ function parseAllowedMimeTypes(rawValidation) {
1022
+ if (!rawValidation) return null;
1023
+ try {
1024
+ const parsed = JSON.parse(rawValidation);
1025
+ if (typeof parsed !== "object" || parsed === null) return null;
1026
+ const list = parsed.allowedMimeTypes;
1027
+ if (!Array.isArray(list) || list.length === 0) return null;
1028
+ return list.filter((entry) => typeof entry === "string");
1029
+ } catch {
1030
+ return null;
1031
+ }
1032
+ }
1033
+
1034
+ //#endregion
1035
+ //#region src/api/handlers/validate-media-fields.ts
1036
+ function asMediaRef(value) {
1037
+ if (value === null || value === void 0) return null;
1038
+ if (typeof value !== "object" || Array.isArray(value)) return null;
1039
+ return value;
1040
+ }
1041
+ function fail(message) {
1042
+ return {
1043
+ success: false,
1044
+ error: {
1045
+ code: "INVALID_MIME_FOR_FIELD",
1046
+ message
1047
+ }
1048
+ };
1049
+ }
1050
+ async function loadMediaFieldsForCollection(db, collectionSlug) {
1051
+ const rows = await db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
1052
+ "_emdash_fields.slug",
1053
+ "_emdash_fields.type",
1054
+ "_emdash_fields.validation"
1055
+ ]).where("_emdash_collections.slug", "=", collectionSlug).where("_emdash_fields.type", "in", ["file", "image"]).execute();
1056
+ const out = [];
1057
+ for (const row of rows) {
1058
+ const list = parseAllowedMimeTypes(row.validation);
1059
+ if (!list) continue;
1060
+ out.push({
1061
+ slug: row.slug,
1062
+ type: row.type,
1063
+ allowedMimeTypes: list
1064
+ });
1065
+ }
1066
+ return out;
1067
+ }
1068
+ async function validateMediaFields(db, collectionSlug, data) {
1069
+ const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
1070
+ if (fields.length === 0) return {
1071
+ success: true,
1072
+ data: true
1073
+ };
1074
+ const localIds = /* @__PURE__ */ new Set();
1075
+ for (const field of fields) {
1076
+ const ref = asMediaRef(data[field.slug]);
1077
+ if (!ref) continue;
1078
+ if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
1079
+ }
1080
+ const idList = [...localIds];
1081
+ const mimeById = /* @__PURE__ */ new Map();
1082
+ if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
1083
+ const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
1084
+ for (const r of rows) mimeById.set(r.id, r.mime_type);
1085
+ }
1086
+ for (const field of fields) {
1087
+ const value = data[field.slug];
1088
+ if (value === null || value === void 0) continue;
1089
+ const ref = asMediaRef(value);
1090
+ if (!ref) continue;
1091
+ const provider = typeof ref.provider === "string" ? ref.provider : "local";
1092
+ let mime;
1093
+ if (provider === "local") {
1094
+ if (typeof ref.id !== "string") return fail(`Field '${field.slug}' references media with an invalid id`);
1095
+ mime = mimeById.get(ref.id);
1096
+ if (!mime) return fail(`Field '${field.slug}' references media with unknown MIME type`);
1097
+ } else {
1098
+ if (typeof ref.mimeType !== "string") return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
1099
+ mime = ref.mimeType;
1100
+ }
1101
+ if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) return fail(`Field '${field.slug}' does not accept ${mime}`);
1102
+ }
1103
+ return {
1104
+ success: true,
1105
+ data: true
1106
+ };
1107
+ }
1108
+
980
1109
  //#endregion
981
1110
  //#region src/api/handlers/content.ts
982
1111
  /**
@@ -1254,6 +1383,8 @@ async function handleContentCreate(db, collection, body) {
1254
1383
  message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
1255
1384
  }
1256
1385
  };
1386
+ const mimeCheck = await validateMediaFields(db, collection, body.data);
1387
+ if (!mimeCheck.success) return mimeCheck;
1257
1388
  const item = await withTransaction(db, async (trx) => {
1258
1389
  const repo = new ContentRepository(trx);
1259
1390
  const bylineRepo = new BylineRepository(trx);
@@ -1278,6 +1409,10 @@ async function handleContentCreate(db, collection, body) {
1278
1409
  created.primaryBylineId = body.bylines[0]?.bylineId ?? null;
1279
1410
  }
1280
1411
  await hydrateBylines(trx, collection, created);
1412
+ if (body.translationOf) {
1413
+ const { TaxonomyRepository } = await import("./taxonomy-D6NvlKo8.mjs").then((n) => n.n);
1414
+ await new TaxonomyRepository(trx).copyEntryTerms(collection, body.translationOf, created.id);
1415
+ }
1281
1416
  if (body.seo && hasSeo) created.seo = await new SeoRepository(trx).upsert(collection, created.id, body.seo);
1282
1417
  else if (hasSeo) created.seo = { ...SEO_DEFAULTS };
1283
1418
  return created;
@@ -1348,6 +1483,10 @@ async function handleContentUpdate(db, collection, id, body) {
1348
1483
  message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
1349
1484
  }
1350
1485
  };
1486
+ if (body.data) {
1487
+ const mimeCheck = await validateMediaFields(db, collection, body.data);
1488
+ if (!mimeCheck.success) return mimeCheck;
1489
+ }
1351
1490
  const resolvedId = await resolveId(new ContentRepository(db), collection, id) ?? id;
1352
1491
  const item = await withTransaction(db, async (trx) => {
1353
1492
  const trxRepo = new ContentRepository(trx);
@@ -2659,6 +2798,8 @@ const HTTP_SCHEME_RE = /^https?:\/\//i;
2659
2798
  const httpUrl = z$1.string().url().refine((url) => HTTP_SCHEME_RE.test(url), "URL must use http or https");
2660
2799
  /** BCP 47 locale code — language with optional script/region subtags (e.g. en, en-US, pt-BR, es-419, zh-Hant) */
2661
2800
  const localeCode = z$1.string().regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code").transform((v) => v.toLowerCase());
2801
+ /** Shared `?locale=xx` query shape for endpoints that filter by locale. */
2802
+ const localeFilterQuery = z$1.object({ locale: z$1.string().min(1).optional() }).meta({ id: "LocaleFilterQuery" });
2662
2803
  /** Standard API error response */
2663
2804
  const apiErrorSchema = z$1.object({ error: z$1.object({
2664
2805
  code: z$1.string().meta({
@@ -2862,7 +3003,14 @@ const contentTranslationsResponseSchema = z$1.object({
2862
3003
 
2863
3004
  //#endregion
2864
3005
  //#region src/api/schemas/media.ts
2865
- const mediaListQuery = cursorPaginationQuery.extend({ mimeType: z$1.string().optional() }).meta({ id: "MediaListQuery" });
3006
+ /**
3007
+ * Accepts a comma-separated string (from URL query params) or an array of
3008
+ * strings (from JSON body or programmatic use) and normalises to string[].
3009
+ */
3010
+ const mimeTypeFilter = z$1.union([z$1.string(), z$1.array(z$1.string())]).transform((v) => {
3011
+ return (Array.isArray(v) ? v : v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
3012
+ }).optional();
3013
+ const mediaListQuery = cursorPaginationQuery.extend({ mimeType: mimeTypeFilter }).meta({ id: "MediaListQuery" });
2866
3014
  const mediaUpdateBody = z$1.object({
2867
3015
  alt: z$1.string().optional(),
2868
3016
  caption: z$1.string().optional(),
@@ -2878,7 +3026,7 @@ const mediaConfirmBody = z$1.object({
2878
3026
  }).meta({ id: "MediaConfirmBody" });
2879
3027
  const mediaProviderListQuery = cursorPaginationQuery.extend({
2880
3028
  query: z$1.string().optional(),
2881
- mimeType: z$1.string().optional()
3029
+ mimeType: mimeTypeFilter
2882
3030
  }).meta({ id: "MediaProviderListQuery" });
2883
3031
  const mediaStatusSchema = z$1.enum([
2884
3032
  "pending",
@@ -2976,7 +3124,8 @@ const fieldValidation = z$1.object({
2976
3124
  options: z$1.array(z$1.string()).optional(),
2977
3125
  subFields: z$1.array(repeaterSubFieldSchema).min(1).optional(),
2978
3126
  minItems: z$1.number().int().min(0).optional(),
2979
- maxItems: z$1.number().int().min(1).optional()
3127
+ maxItems: z$1.number().int().min(1).optional(),
3128
+ allowedMimeTypes: z$1.array(z$1.string().regex(/^[a-z0-9][a-z0-9!#$&^_+\-.]*\/[a-z0-9!#$&^_+\-.]*$/i, "Invalid MIME type")).min(1, "allowedMimeTypes must not be empty — omit the field to allow all types").max(64, "allowedMimeTypes may contain at most 64 entries").optional()
2980
3129
  }).optional();
2981
3130
  const fieldWidgetOptions = z$1.record(z$1.string(), z$1.unknown()).optional();
2982
3131
  const createCollectionBody = z$1.object({
@@ -3014,7 +3163,7 @@ const createFieldBody = z$1.object({
3014
3163
  required: z$1.boolean().optional(),
3015
3164
  unique: z$1.boolean().optional(),
3016
3165
  defaultValue: z$1.unknown().optional(),
3017
- validation: fieldValidation,
3166
+ validation: fieldValidation.nullable(),
3018
3167
  widget: z$1.string().optional(),
3019
3168
  options: fieldWidgetOptions,
3020
3169
  sortOrder: z$1.number().int().min(0).optional(),
@@ -3026,7 +3175,7 @@ const updateFieldBody = z$1.object({
3026
3175
  required: z$1.boolean().optional(),
3027
3176
  unique: z$1.boolean().optional(),
3028
3177
  defaultValue: z$1.unknown().optional(),
3029
- validation: fieldValidation,
3178
+ validation: fieldValidation.nullable(),
3030
3179
  widget: z$1.string().optional(),
3031
3180
  options: fieldWidgetOptions,
3032
3181
  sortOrder: z$1.number().int().min(0).optional(),
@@ -3292,7 +3441,9 @@ const menuItemType = z$1.string().min(1);
3292
3441
  const safeHref = z$1.string().trim().refine(isSafeHref, "URL must use http, https, mailto, tel, a relative path, or a fragment identifier");
3293
3442
  const createMenuBody = z$1.object({
3294
3443
  name: z$1.string().min(1),
3295
- label: z$1.string().min(1)
3444
+ label: z$1.string().min(1),
3445
+ locale: z$1.string().min(1).optional(),
3446
+ translationOf: z$1.string().min(1).optional()
3296
3447
  }).meta({ id: "CreateMenuBody" });
3297
3448
  const updateMenuBody = z$1.object({ label: z$1.string().min(1).optional() }).meta({ id: "UpdateMenuBody" });
3298
3449
  const createMenuItemBody = z$1.object({
@@ -3328,7 +3479,9 @@ const menuSchema = z$1.object({
3328
3479
  name: z$1.string(),
3329
3480
  label: z$1.string(),
3330
3481
  created_at: z$1.string(),
3331
- updated_at: z$1.string()
3482
+ updated_at: z$1.string(),
3483
+ locale: z$1.string(),
3484
+ translation_group: z$1.string().nullable()
3332
3485
  }).meta({ id: "Menu" });
3333
3486
  const menuItemSchema = z$1.object({
3334
3487
  id: z$1.string(),
@@ -3343,8 +3496,20 @@ const menuItemSchema = z$1.object({
3343
3496
  title_attr: z$1.string().nullable(),
3344
3497
  target: z$1.string().nullable(),
3345
3498
  css_classes: z$1.string().nullable(),
3346
- created_at: z$1.string()
3499
+ created_at: z$1.string(),
3500
+ locale: z$1.string(),
3501
+ translation_group: z$1.string().nullable()
3347
3502
  }).meta({ id: "MenuItem" });
3503
+ const menuTranslationsSchema = z$1.object({
3504
+ translationGroup: z$1.string().nullable(),
3505
+ translations: z$1.array(z$1.object({
3506
+ id: z$1.string(),
3507
+ name: z$1.string(),
3508
+ label: z$1.string(),
3509
+ locale: z$1.string(),
3510
+ updatedAt: z$1.string()
3511
+ }))
3512
+ }).meta({ id: "MenuTranslations" });
3348
3513
  const menuListItemSchema = menuSchema.extend({ itemCount: z$1.number().int() }).meta({ id: "MenuListItem" });
3349
3514
  const menuWithItemsSchema = menuSchema.extend({ items: z$1.array(menuItemSchema) }).meta({ id: "MenuWithItems" });
3350
3515
 
@@ -3355,14 +3520,19 @@ const collectionSlugPattern = /^[a-z][a-z0-9_]*$/;
3355
3520
  const createTaxonomyDefBody = z$1.object({
3356
3521
  name: z$1.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/, "Name must be lowercase alphanumeric with underscores"),
3357
3522
  label: z$1.string().min(1).max(200),
3523
+ labelSingular: z$1.string().min(1).max(200).optional(),
3358
3524
  hierarchical: z$1.boolean().optional().default(false),
3359
- collections: z$1.array(z$1.string().min(1).max(63).regex(collectionSlugPattern, "Invalid collection slug format")).max(100).optional().default([])
3525
+ collections: z$1.array(z$1.string().min(1).max(63).regex(collectionSlugPattern, "Invalid collection slug format")).max(100).optional().default([]),
3526
+ locale: z$1.string().min(1).optional(),
3527
+ translationOf: z$1.string().min(1).optional()
3360
3528
  }).meta({ id: "CreateTaxonomyDefBody" });
3361
3529
  const createTermBody = z$1.object({
3362
3530
  slug: z$1.string().min(1),
3363
3531
  label: z$1.string().min(1),
3364
3532
  parentId: z$1.string().nullish(),
3365
- description: z$1.string().optional()
3533
+ description: z$1.string().optional(),
3534
+ locale: z$1.string().min(1).optional(),
3535
+ translationOf: z$1.string().min(1).optional()
3366
3536
  }).meta({ id: "CreateTermBody" });
3367
3537
  const updateTermBody = z$1.object({
3368
3538
  slug: z$1.string().min(1).optional(),
@@ -3376,8 +3546,19 @@ const taxonomyDefSchema = z$1.object({
3376
3546
  label: z$1.string(),
3377
3547
  labelSingular: z$1.string().optional(),
3378
3548
  hierarchical: z$1.boolean(),
3379
- collections: z$1.array(z$1.string())
3549
+ collections: z$1.array(z$1.string()),
3550
+ locale: z$1.string(),
3551
+ translationGroup: z$1.string().nullable()
3380
3552
  }).meta({ id: "TaxonomyDef" });
3553
+ const taxonomyDefTranslationsSchema = z$1.object({
3554
+ translationGroup: z$1.string().nullable(),
3555
+ translations: z$1.array(z$1.object({
3556
+ id: z$1.string(),
3557
+ name: z$1.string(),
3558
+ label: z$1.string(),
3559
+ locale: z$1.string()
3560
+ }))
3561
+ }).meta({ id: "TaxonomyDefTranslations" });
3381
3562
  const taxonomyListResponseSchema = z$1.object({ taxonomies: z$1.array(taxonomyDefSchema) }).meta({ id: "TaxonomyListResponse" });
3382
3563
  const termSchema = z$1.object({
3383
3564
  id: z$1.string(),
@@ -3385,8 +3566,19 @@ const termSchema = z$1.object({
3385
3566
  slug: z$1.string(),
3386
3567
  label: z$1.string(),
3387
3568
  parentId: z$1.string().nullable(),
3388
- description: z$1.string().optional()
3569
+ description: z$1.string().optional(),
3570
+ locale: z$1.string(),
3571
+ translationGroup: z$1.string().nullable()
3389
3572
  }).meta({ id: "Term" });
3573
+ const termTranslationsSchema = z$1.object({
3574
+ translationGroup: z$1.string().nullable(),
3575
+ translations: z$1.array(z$1.object({
3576
+ id: z$1.string(),
3577
+ slug: z$1.string(),
3578
+ label: z$1.string(),
3579
+ locale: z$1.string()
3580
+ }))
3581
+ }).meta({ id: "TermTranslations" });
3390
3582
  const termWithCountSchema = z$1.object({
3391
3583
  id: z$1.string(),
3392
3584
  name: z$1.string(),
@@ -3395,7 +3587,9 @@ const termWithCountSchema = z$1.object({
3395
3587
  parentId: z$1.string().nullable(),
3396
3588
  description: z$1.string().optional(),
3397
3589
  count: z$1.number().int(),
3398
- children: z$1.array(z$1.lazy(() => termWithCountSchema))
3590
+ children: z$1.array(z$1.lazy(() => termWithCountSchema)),
3591
+ locale: z$1.string(),
3592
+ translationGroup: z$1.string().nullable()
3399
3593
  }).meta({ id: "TermWithCount" });
3400
3594
  const termListResponseSchema = z$1.object({ terms: z$1.array(termWithCountSchema) }).meta({ id: "TermListResponse" });
3401
3595
  const termResponseSchema = z$1.object({ term: termSchema }).meta({ id: "TermResponse" });
@@ -8926,53 +9120,53 @@ async function getCommentCountWithDb(db, collection, contentId) {
8926
9120
  //#endregion
8927
9121
  //#region src/menus/index.ts
8928
9122
  /**
8929
- * Get menu by name with resolved URLs
9123
+ * Get a menu by name with resolved URLs.
8930
9124
  *
8931
9125
  * @example
8932
9126
  * ```ts
8933
- * import { getMenu } from "emdash";
8934
- *
8935
9127
  * const menu = await getMenu("primary");
8936
- * if (menu) {
8937
- * console.log(menu.items); // Array of MenuItem with resolved URLs
8938
- * }
9128
+ * const menuEs = await getMenu("primary", { locale: "es" });
8939
9129
  * ```
8940
9130
  */
8941
- function getMenu(name) {
8942
- return requestCached(`menu:${name}`, async () => {
8943
- return getMenuWithDb(name, await getDb());
9131
+ function getMenu(name, options = {}) {
9132
+ const locale = resolveLocale(options.locale);
9133
+ return requestCached(`menu:${name}:${locale ?? "*"}`, async () => {
9134
+ return getMenuWithDb(name, await getDb(), { locale });
8944
9135
  });
8945
9136
  }
8946
9137
  /**
8947
- * Get menu by name with resolved URLs (with explicit db)
8948
- *
8949
- * @internal Use `getMenu()` in templates. This variant is for admin routes
8950
- * that already have a database handle.
9138
+ * Get menu by name with resolved URLs (with explicit db). Internal helper for
9139
+ * admin routes that already have a database handle.
8951
9140
  */
8952
- async function getMenuWithDb(name, db) {
8953
- const menuRow = await db.selectFrom("_emdash_menus").selectAll().where("name", "=", name).executeTakeFirst();
9141
+ async function getMenuWithDb(name, db, options = {}) {
9142
+ const chain = resolveLocaleChain(options.locale);
9143
+ const selectMenu = () => db.selectFrom("_emdash_menus").selectAll().where("name", "=", name);
9144
+ let menuRow;
9145
+ if (chain.length === 0) menuRow = await selectMenu().orderBy("locale", "asc").executeTakeFirst();
9146
+ else {
9147
+ menuRow = void 0;
9148
+ for (const locale of chain) {
9149
+ menuRow = await selectMenu().where("locale", "=", locale).executeTakeFirst();
9150
+ if (menuRow) break;
9151
+ }
9152
+ }
8954
9153
  if (!menuRow) return null;
8955
- const items = await buildMenuTree(await db.selectFrom("_emdash_menu_items").selectAll().$castTo().where("menu_id", "=", menuRow.id).orderBy("sort_order", "asc").execute(), db);
9154
+ const items = await buildMenuTree(await db.selectFrom("_emdash_menu_items").selectAll().$castTo().where("menu_id", "=", menuRow.id).orderBy("sort_order", "asc").execute(), db, menuRow.locale);
8956
9155
  return {
8957
9156
  id: menuRow.id,
8958
9157
  name: menuRow.name,
8959
9158
  label: menuRow.label,
8960
- items
9159
+ items,
9160
+ locale: menuRow.locale,
9161
+ translationGroup: menuRow.translation_group
8961
9162
  };
8962
9163
  }
8963
9164
  /**
8964
- * Get all menus (without items - for admin list)
8965
- *
8966
- * @example
8967
- * ```ts
8968
- * import { getMenus } from "emdash";
8969
- *
8970
- * const menus = await getMenus();
8971
- * console.log(menus); // [{ id, name, label }]
8972
- * ```
9165
+ * Get all menus (without items, locale-filtered for admin list / site nav
9166
+ * summaries). When no locale is configured, returns menus across all locales.
8973
9167
  */
8974
- async function getMenus() {
8975
- return getMenusWithDb(await getDb());
9168
+ async function getMenus(options = {}) {
9169
+ return getMenusWithDb(await getDb(), options);
8976
9170
  }
8977
9171
  /**
8978
9172
  * Get all menus (with explicit db)
@@ -8980,17 +9174,23 @@ async function getMenus() {
8980
9174
  * @internal Use `getMenus()` in templates. This variant is for admin routes
8981
9175
  * that already have a database handle.
8982
9176
  */
8983
- async function getMenusWithDb(db) {
8984
- return await db.selectFrom("_emdash_menus").select([
9177
+ async function getMenusWithDb(db, options = {}) {
9178
+ const locale = resolveLocale(options.locale);
9179
+ let query = db.selectFrom("_emdash_menus").select([
8985
9180
  "id",
8986
9181
  "name",
8987
- "label"
8988
- ]).orderBy("name", "asc").execute();
9182
+ "label",
9183
+ "locale"
9184
+ ]).orderBy("name", "asc");
9185
+ if (locale !== void 0) query = query.where("locale", "=", locale);
9186
+ return query.execute();
8989
9187
  }
8990
9188
  /**
8991
- * Build hierarchical menu tree from flat array of items
9189
+ * Build a hierarchical menu tree from a flat list of items. Items are
9190
+ * resolved against the given `locale` so references land on the right
9191
+ * per-locale content rows.
8992
9192
  */
8993
- async function buildMenuTree(items, db) {
9193
+ async function buildMenuTree(items, db, locale) {
8994
9194
  const collectionSlugs = /* @__PURE__ */ new Set();
8995
9195
  for (const item of items) {
8996
9196
  if (item.reference_collection) collectionSlugs.add(item.reference_collection);
@@ -9001,7 +9201,7 @@ async function buildMenuTree(items, db) {
9001
9201
  const rows = await db.selectFrom("_emdash_collections").select(["slug", "url_pattern"]).where("slug", "in", [...collectionSlugs]).execute();
9002
9202
  for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
9003
9203
  }
9004
- const validItems = (await Promise.all(items.map((item) => resolveMenuItem(item, db, urlPatterns)))).filter((item) => item !== null);
9204
+ const validItems = (await Promise.all(items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)))).filter((item) => item !== null);
9005
9205
  const itemMap = /* @__PURE__ */ new Map();
9006
9206
  const rootItems = [];
9007
9207
  for (const item of validItems) itemMap.set(item.id, {
@@ -9020,11 +9220,11 @@ async function buildMenuTree(items, db) {
9020
9220
  return rootItems;
9021
9221
  }
9022
9222
  /**
9023
- * Resolve a single menu item's URL
9024
- *
9025
- * Returns null if the referenced content no longer exists (item should be skipped)
9223
+ * Resolve a single menu item's URL. `reference_id` is a translation_group
9224
+ * (migration 036 remapped all existing references); we join it against
9225
+ * the per-locale ec_* row or per-locale taxonomy row.
9026
9226
  */
9027
- async function resolveMenuItem(item, db, urlPatterns) {
9227
+ async function resolveMenuItem(item, db, urlPatterns, locale) {
9028
9228
  let url;
9029
9229
  try {
9030
9230
  switch (item.type) {
@@ -9033,18 +9233,18 @@ async function resolveMenuItem(item, db, urlPatterns) {
9033
9233
  break;
9034
9234
  case "page":
9035
9235
  case "post":
9036
- url = await resolveContentUrl(item.reference_collection || `${item.type}s`, item.reference_id, db, urlPatterns);
9236
+ url = await resolveContentUrl(item.reference_collection || `${item.type}s`, item.reference_id, db, urlPatterns, locale);
9037
9237
  if (url === null) return null;
9038
9238
  break;
9039
9239
  case "taxonomy":
9040
- url = await resolveTaxonomyUrl(item.reference_id, db);
9240
+ url = await resolveTaxonomyUrl(item.reference_id, db, locale);
9041
9241
  if (url === null) return null;
9042
9242
  break;
9043
9243
  case "collection":
9044
9244
  url = `/${item.reference_collection}/`;
9045
9245
  break;
9046
9246
  default: if (item.reference_collection && item.reference_id) {
9047
- url = await resolveContentUrl(item.reference_collection, item.reference_id, db, urlPatterns);
9247
+ url = await resolveContentUrl(item.reference_collection, item.reference_id, db, urlPatterns, locale);
9048
9248
  if (url === null) return null;
9049
9249
  } else url = "#";
9050
9250
  }
@@ -9073,37 +9273,51 @@ function interpolateUrlPattern(pattern, slug, id) {
9073
9273
  return pattern.replace(SLUG_PLACEHOLDER, slug).replace(ID_PLACEHOLDER, id);
9074
9274
  }
9075
9275
  /**
9076
- * Resolve URL for a content entry (page/post)
9077
- *
9078
- * Uses the collection's url_pattern if set, otherwise falls back to /{collection}/{slug}.
9079
- * Returns null if content not found (item should be skipped).
9276
+ * Resolve the URL for a content reference. `referenceGroup` is the content
9277
+ * row's translation_group; we look up the row in the requested locale
9278
+ * (falling back to the source if no translation exists so the menu link is
9279
+ * still clickable).
9080
9280
  */
9081
- async function resolveContentUrl(collection, entryId, db, urlPatterns) {
9082
- if (!entryId) return null;
9281
+ async function resolveContentUrl(collection, referenceGroup, db, urlPatterns, locale) {
9282
+ if (!referenceGroup) return null;
9083
9283
  try {
9084
9284
  validateIdentifier(collection, "menu item collection");
9085
- const row = (await sql`
9086
- SELECT slug FROM ${sql.ref(`ec_${collection}`)} WHERE id = ${entryId} LIMIT 1
9087
- `.execute(db)).rows[0];
9088
- if (row) {
9089
- const pattern = urlPatterns.get(collection);
9090
- if (pattern) return interpolateUrlPattern(pattern, row.slug, entryId);
9091
- return `/${collection}/${row.slug}`;
9285
+ let result = await sql`
9286
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
9287
+ WHERE translation_group = ${referenceGroup} AND locale = ${locale}
9288
+ LIMIT 1
9289
+ `.execute(db);
9290
+ let row = result.rows[0];
9291
+ if (!row) {
9292
+ result = await sql`
9293
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
9294
+ WHERE translation_group = ${referenceGroup}
9295
+ ORDER BY locale ASC LIMIT 1
9296
+ `.execute(db);
9297
+ row = result.rows[0];
9092
9298
  }
9093
- return null;
9299
+ if (!row) row = (await sql`
9300
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
9301
+ WHERE id = ${referenceGroup} LIMIT 1
9302
+ `.execute(db)).rows[0];
9303
+ if (!row) return null;
9304
+ const pattern = urlPatterns.get(collection);
9305
+ if (pattern) return interpolateUrlPattern(pattern, row.slug, row.id);
9306
+ return `/${collection}/${row.slug}`;
9094
9307
  } catch (error) {
9095
- console.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);
9308
+ console.error(`Failed to resolve content URL for ${collection}/${referenceGroup}:`, error);
9096
9309
  return null;
9097
9310
  }
9098
9311
  }
9099
9312
  /**
9100
- * Resolve URL for a taxonomy term
9101
- *
9102
- * Returns null if taxonomy not found (item should be skipped)
9313
+ * Resolve URL for a taxonomy term reference. `referenceGroup` is the term's
9314
+ * translation_group; we pick the row in the active locale (or fall back).
9103
9315
  */
9104
- async function resolveTaxonomyUrl(taxonomyId, db) {
9105
- if (!taxonomyId) return null;
9106
- const taxonomy = await db.selectFrom("taxonomies").select(["name", "slug"]).where("id", "=", taxonomyId).executeTakeFirst();
9316
+ async function resolveTaxonomyUrl(referenceGroup, db, locale) {
9317
+ if (!referenceGroup) return null;
9318
+ let taxonomy = await db.selectFrom("taxonomies").select(["name", "slug"]).where("translation_group", "=", referenceGroup).where("locale", "=", locale).executeTakeFirst();
9319
+ if (!taxonomy) taxonomy = await db.selectFrom("taxonomies").select(["name", "slug"]).where("translation_group", "=", referenceGroup).orderBy("locale", "asc").executeTakeFirst();
9320
+ if (!taxonomy) taxonomy = await db.selectFrom("taxonomies").select(["name", "slug"]).where("id", "=", referenceGroup).executeTakeFirst();
9107
9321
  if (!taxonomy) return null;
9108
9322
  return `/${taxonomy.name}/${taxonomy.slug}`;
9109
9323
  }
@@ -9679,5 +9893,5 @@ function extractSearchableFields(entry, fields) {
9679
9893
  }
9680
9894
 
9681
9895
  //#endregion
9682
- export { isSafeHref as $, NoopSandboxRunner as A, handleContentTranslations as At, HookPipeline as B, getAllSources as C, handleContentGetIncludingTrashed as Ct, probeUrl as D, handleContentPublish as Dt, getUrlSources as E, handleContentPermanentDelete as Et, PluginRouteError as F, portableText as Ft, sanitizeHeadersForSandbox as G, resolveExclusiveHooks as H, PluginRouteRegistry as I, reference as It, parseWxr as J, getTrustedProxyHeaders as K, DEV_CONSOLE_EMAIL_PLUGIN_ID as L, image as Lt, createNoopSandboxRunner as M, handleContentUnschedule as Mt, PluginManager as N, handleContentUpdate as Nt, registerSource as O, handleContentRestore as Ot, createPluginManager as P, validateRev as Pt, prosemirrorToPortableText as Q, devConsoleEmailDeliver as R, clearSources as S, handleContentGet as St, getSource as T, handleContentListTrashed as Tt, CronExecutor as U, createHookPipeline as V, extractRequestMeta as W, after as X, parseWxrString as Y, portableTextToProsemirror as Z, buildPreviewUrl as _, handleContentCountTrashed as _t, search as a, getCollectionInfo as at, parseWxrDate as b, handleContentDiscardDraft as bt, getWidgetArea as c, handleMediaGet as ct, getMenu as d, handleRevisionGet as dt, sanitizeHref as et, getMenus as f, handleRevisionList as ft, isPreviewRequest as g, handleContentCountScheduled as gt, getPreviewToken as h, handleContentCompare as ht, getSuggestions as i, PluginStateRepository as it, SandboxNotAvailableError as j, handleContentUnpublish as jt, importReusableBlocksAsSections as k, handleContentSchedule as kt, getWidgetAreas as l, handleMediaList as lt, getComments as m, generateManifest as mt, extractSearchableFields as n, getSection as nt, searchCollection as o, handleMediaCreate as ot, getCommentCount as p, handleRevisionRestore as pt, definePlugin as q, getSearchStats as r, getSections as rt, searchWithDb as s, handleMediaDelete as st, extractPlainText as t, loadBundleFromR2 as tt, getWidgetComponents as u, handleMediaUpdate as ut, getPreviewUrl as v, handleContentCreate as vt, getFileSources as w, handleContentList as wt, wxrSource as x, handleContentDuplicate as xt, wordpressRestSource as y, handleContentDelete as yt, EmailPipeline as z };
9683
- //# sourceMappingURL=search-dOGEccMa.mjs.map
9896
+ export { isSafeHref as $, NoopSandboxRunner as A, handleContentTranslations as At, HookPipeline as B, getAllSources as C, handleContentGetIncludingTrashed as Ct, probeUrl as D, handleContentPublish as Dt, getUrlSources as E, handleContentPermanentDelete as Et, PluginRouteError as F, portableText as Ft, sanitizeHeadersForSandbox as G, resolveExclusiveHooks as H, PluginRouteRegistry as I, reference as It, parseWxr as J, getTrustedProxyHeaders as K, DEV_CONSOLE_EMAIL_PLUGIN_ID as L, file as Lt, createNoopSandboxRunner as M, handleContentUnschedule as Mt, PluginManager as N, handleContentUpdate as Nt, registerSource as O, handleContentRestore as Ot, createPluginManager as P, validateRev as Pt, prosemirrorToPortableText as Q, devConsoleEmailDeliver as R, image as Rt, clearSources as S, handleContentGet as St, getSource as T, handleContentListTrashed as Tt, CronExecutor as U, createHookPipeline as V, extractRequestMeta as W, after as X, parseWxrString as Y, portableTextToProsemirror as Z, buildPreviewUrl as _, handleContentCountTrashed as _t, search as a, getCollectionInfo as at, parseWxrDate as b, handleContentDiscardDraft as bt, getWidgetArea as c, handleMediaGet as ct, getMenu as d, handleRevisionGet as dt, sanitizeHref as et, getMenus as f, handleRevisionList as ft, isPreviewRequest as g, handleContentCountScheduled as gt, getPreviewToken as h, handleContentCompare as ht, getSuggestions as i, PluginStateRepository as it, SandboxNotAvailableError as j, handleContentUnpublish as jt, importReusableBlocksAsSections as k, handleContentSchedule as kt, getWidgetAreas as l, handleMediaList as lt, getComments as m, generateManifest as mt, extractSearchableFields as n, getSection as nt, searchCollection as o, handleMediaCreate as ot, getCommentCount as p, handleRevisionRestore as pt, definePlugin as q, getSearchStats as r, getSections as rt, searchWithDb as s, handleMediaDelete as st, extractPlainText as t, loadBundleFromR2 as tt, getWidgetComponents as u, handleMediaUpdate as ut, getPreviewUrl as v, handleContentCreate as vt, getFileSources as w, handleContentList as wt, wxrSource as x, handleContentDuplicate as xt, wordpressRestSource as y, handleContentDelete as yt, EmailPipeline as z };
9897
+ //# sourceMappingURL=search-DuWhx4NG.mjs.map