emdash 0.5.0 → 0.7.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 (252) hide show
  1. package/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
  2. package/dist/adapters-Di31kZ28.d.mts.map +1 -0
  3. package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
  4. package/dist/apply-5uslYdUu.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +203 -33
  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 +30 -4
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.mjs +2 -2
  14. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  15. package/dist/astro/middleware/request-context.mjs +11 -4
  16. package/dist/astro/middleware/request-context.mjs.map +1 -1
  17. package/dist/astro/middleware/setup.mjs +1 -1
  18. package/dist/astro/middleware.d.mts.map +1 -1
  19. package/dist/astro/middleware.mjs +467 -186
  20. package/dist/astro/middleware.mjs.map +1 -1
  21. package/dist/astro/types.d.mts +17 -9
  22. package/dist/astro/types.d.mts.map +1 -1
  23. package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
  24. package/dist/byline-C4OVd8b3.mjs.map +1 -0
  25. package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
  26. package/dist/bylines-hPTW79hw.mjs.map +1 -0
  27. package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
  28. package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
  29. package/dist/chunks-HGz06Soa.mjs +19 -0
  30. package/dist/chunks-HGz06Soa.mjs.map +1 -0
  31. package/dist/cli/index.mjs +12 -11
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/client/cf-access.d.mts +1 -1
  34. package/dist/client/index.d.mts +1 -1
  35. package/dist/client/index.mjs +1 -1
  36. package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
  37. package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
  38. package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
  39. package/dist/connection-2igzM-AT.mjs.map +1 -0
  40. package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
  41. package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
  42. package/dist/database/instrumentation.d.mts +45 -0
  43. package/dist/database/instrumentation.d.mts.map +1 -0
  44. package/dist/database/instrumentation.mjs +61 -0
  45. package/dist/database/instrumentation.mjs.map +1 -0
  46. package/dist/db/index.d.mts +3 -3
  47. package/dist/db/index.mjs +1 -1
  48. package/dist/db/index.mjs.map +1 -1
  49. package/dist/db/libsql.d.mts +1 -1
  50. package/dist/db/postgres.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts +1 -1
  52. package/dist/db-errors-D0UT85nC.mjs +41 -0
  53. package/dist/db-errors-D0UT85nC.mjs.map +1 -0
  54. package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
  55. package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
  56. package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
  57. package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
  58. package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
  59. package/dist/index-De6_Xv3v.d.mts.map +1 -0
  60. package/dist/index.d.mts +11 -11
  61. package/dist/index.mjs +23 -21
  62. package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
  63. package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
  64. package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
  65. package/dist/loader-DeiBJEMe.mjs.map +1 -0
  66. package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
  67. package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
  68. package/dist/media/index.d.mts +1 -1
  69. package/dist/media/index.mjs +1 -1
  70. package/dist/media/local-runtime.d.mts +7 -7
  71. package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
  72. package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
  73. package/dist/page/index.d.mts +11 -2
  74. package/dist/page/index.d.mts.map +1 -1
  75. package/dist/page/index.mjs +23 -1
  76. package/dist/page/index.mjs.map +1 -1
  77. package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
  78. package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
  79. package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
  80. package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
  81. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  82. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  83. package/dist/{query-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
  84. package/dist/query-g4Ug-9j9.mjs.map +1 -0
  85. package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
  86. package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
  87. package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
  88. package/dist/registry-Ci3WxVAr.mjs.map +1 -0
  89. package/dist/request-cache-DiR961CV.mjs +79 -0
  90. package/dist/request-cache-DiR961CV.mjs.map +1 -0
  91. package/dist/request-context.d.mts +19 -16
  92. package/dist/request-context.d.mts.map +1 -1
  93. package/dist/request-context.mjs.map +1 -1
  94. package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
  95. package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.d.mts.map} +1 -1
  96. package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
  97. package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
  98. package/dist/runtime.d.mts +6 -6
  99. package/dist/runtime.mjs +1 -1
  100. package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
  101. package/dist/search-B0effn3j.mjs.map +1 -0
  102. package/dist/seed/index.d.mts +2 -2
  103. package/dist/seed/index.mjs +10 -9
  104. package/dist/seo/index.d.mts +1 -1
  105. package/dist/storage/local.d.mts +1 -1
  106. package/dist/storage/local.mjs +1 -1
  107. package/dist/storage/s3.d.mts +1 -1
  108. package/dist/storage/s3.mjs +1 -1
  109. package/dist/taxonomies-K2z0Uhnj.mjs +308 -0
  110. package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
  111. package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
  112. package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
  113. package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
  114. package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
  115. package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
  116. package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
  117. package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
  118. package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
  119. package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
  120. package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
  121. package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
  122. package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
  123. package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
  124. package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
  125. package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
  126. package/dist/types-CnZYHyLW.d.mts.map +1 -0
  127. package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
  128. package/dist/types-DDS4MxsT.mjs.map +1 -0
  129. package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
  130. package/dist/types-DgrIP0tF.d.mts.map +1 -0
  131. package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
  132. package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
  133. package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
  134. package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
  135. package/dist/version-BnTKdfam.mjs +7 -0
  136. package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
  137. package/package.json +10 -5
  138. package/src/after.ts +62 -0
  139. package/src/api/handlers/content.ts +2 -0
  140. package/src/api/handlers/oauth-authorization.ts +2 -32
  141. package/src/api/handlers/oauth-clients.ts +40 -4
  142. package/src/api/handlers/taxonomies.ts +13 -0
  143. package/src/api/oauth/redirect-uri.ts +34 -0
  144. package/src/api/openapi/document.ts +126 -118
  145. package/src/api/schemas/content.ts +8 -0
  146. package/src/api/schemas/media.ts +26 -15
  147. package/src/api/schemas/schema.ts +1 -0
  148. package/src/astro/integration/font-provider.ts +178 -0
  149. package/src/astro/integration/index.ts +44 -0
  150. package/src/astro/integration/routes.ts +6 -0
  151. package/src/astro/integration/runtime.ts +117 -0
  152. package/src/astro/integration/virtual-modules.ts +41 -39
  153. package/src/astro/integration/vite-config.ts +16 -5
  154. package/src/astro/middleware/auth.ts +33 -1
  155. package/src/astro/middleware/request-context.ts +15 -3
  156. package/src/astro/middleware.ts +340 -263
  157. package/src/astro/routes/admin.astro +21 -10
  158. package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
  159. package/src/astro/routes/api/auth/passkey/options.ts +2 -1
  160. package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
  161. package/src/astro/routes/api/auth/signup/request.ts +26 -8
  162. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
  163. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
  164. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
  165. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
  166. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
  167. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
  168. package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
  169. package/src/astro/routes/api/content/[collection]/index.ts +19 -1
  170. package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
  171. package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
  172. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
  173. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
  174. package/src/astro/routes/api/manifest.ts +7 -0
  175. package/src/astro/routes/api/media/upload-url.ts +10 -2
  176. package/src/astro/routes/api/media.ts +10 -7
  177. package/src/astro/routes/api/oauth/device/code.ts +2 -1
  178. package/src/astro/routes/api/oauth/device/token.ts +2 -1
  179. package/src/astro/routes/api/oauth/register.ts +178 -0
  180. package/src/astro/routes/api/oauth/token.ts +15 -0
  181. package/src/astro/routes/api/openapi.json.ts +15 -5
  182. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
  183. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
  184. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
  185. package/src/astro/routes/api/search/index.ts +5 -0
  186. package/src/astro/routes/api/search/suggest.ts +3 -0
  187. package/src/astro/routes/api/setup/admin-verify.ts +30 -5
  188. package/src/astro/routes/api/setup/admin.ts +32 -8
  189. package/src/astro/routes/api/setup/index.ts +5 -2
  190. package/src/astro/routes/api/taxonomies/index.ts +1 -0
  191. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
  192. package/src/astro/types.ts +9 -0
  193. package/src/auth/rate-limit.ts +50 -22
  194. package/src/auth/setup-nonce.ts +22 -0
  195. package/src/auth/trusted-proxy.ts +92 -0
  196. package/src/bylines/index.ts +22 -45
  197. package/src/components/EmDashHead.astro +23 -7
  198. package/src/database/connection.ts +23 -1
  199. package/src/database/instrumentation.ts +98 -0
  200. package/src/database/migrations/035_bounded_404_log.ts +112 -0
  201. package/src/database/migrations/runner.ts +2 -0
  202. package/src/database/repositories/content.ts +39 -0
  203. package/src/database/repositories/options.ts +25 -0
  204. package/src/database/repositories/redirect.ts +111 -8
  205. package/src/database/types.ts +9 -0
  206. package/src/db/adapters.ts +15 -0
  207. package/src/emdash-runtime.ts +312 -92
  208. package/src/import/registry.ts +4 -3
  209. package/src/import/ssrf.ts +253 -12
  210. package/src/index.ts +6 -0
  211. package/src/loader.ts +19 -24
  212. package/src/mcp/server.ts +76 -3
  213. package/src/menus/index.ts +6 -3
  214. package/src/page/index.ts +1 -1
  215. package/src/page/seo-contributions.ts +36 -0
  216. package/src/plugins/context.ts +15 -3
  217. package/src/plugins/manager.ts +6 -0
  218. package/src/plugins/request-meta.ts +66 -15
  219. package/src/plugins/routes.ts +3 -1
  220. package/src/query.ts +104 -7
  221. package/src/request-cache.ts +106 -0
  222. package/src/request-context.ts +19 -0
  223. package/src/schema/query.ts +5 -2
  224. package/src/schema/registry.ts +243 -166
  225. package/src/schema/types.ts +13 -2
  226. package/src/schema/zod-generator.ts +4 -0
  227. package/src/search/fts-manager.ts +19 -5
  228. package/src/search/query.ts +4 -3
  229. package/src/seed/apply.ts +41 -1
  230. package/src/settings/index.ts +24 -5
  231. package/src/taxonomies/index.ts +324 -124
  232. package/src/utils/db-errors.ts +46 -0
  233. package/src/virtual-modules.d.ts +31 -10
  234. package/src/visual-editing/toolbar.ts +6 -1
  235. package/src/widgets/index.ts +54 -25
  236. package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
  237. package/dist/apply-Cma_PiF6.mjs.map +0 -1
  238. package/dist/byline-WuOq9MFJ.mjs.map +0 -1
  239. package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
  240. package/dist/connection-B4zVnQIa.mjs.map +0 -1
  241. package/dist/index-CCWzlriB.d.mts.map +0 -1
  242. package/dist/loader-BYzwzORf.mjs.map +0 -1
  243. package/dist/query-B6Vu0d2i.mjs.map +0 -1
  244. package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
  245. package/dist/registry-BgnP3ysR.mjs.map +0 -1
  246. package/dist/runner-Cd-_WyDo.mjs.map +0 -1
  247. package/dist/search-Cn1SYvYF.mjs.map +0 -1
  248. package/dist/types-C3ronwXb.d.mts.map +0 -1
  249. package/dist/types-DeG21anB.d.mts.map +0 -1
  250. package/dist/types-xxCWI3j0.mjs.map +0 -1
  251. package/dist/validate-Db1yNL3i.d.mts.map +0 -1
  252. package/dist/version-CMMjTuqu.mjs +0 -7
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/page/context.ts","../../src/page/metadata.ts","../../src/page/fragments.ts","../../src/page/jsonld.ts","../../src/page/seo-contributions.ts","../../src/page/index.ts"],"sourcesContent":["/**\n * Public page context builder\n *\n * Templates call this to describe the page being rendered.\n * The resulting context is passed to EmDashHead / EmDashBodyStart / EmDashBodyEnd.\n */\n\nimport type { BreadcrumbItem, PublicPageContext } from \"../plugins/types.js\";\n\n/** Fields shared by both input forms */\ninterface PageContextFields {\n\tkind: \"content\" | \"custom\";\n\tpageType?: string;\n\ttitle?: string | null;\n\tpageTitle?: string | null;\n\tdescription?: string | null;\n\tcanonical?: string | null;\n\timage?: string | null;\n\tcontent?: { collection: string; id: string; slug?: string | null };\n\t/** SEO overrides for OG/Twitter meta generation */\n\tseo?: {\n\t\togTitle?: string | null;\n\t\togDescription?: string | null;\n\t\togImage?: string | null;\n\t\trobots?: string | null;\n\t};\n\t/** Article metadata for Open Graph article: tags */\n\tarticleMeta?: {\n\t\tpublishedTime?: string | null;\n\t\tmodifiedTime?: string | null;\n\t\tauthor?: string | null;\n\t};\n\t/** Site name for structured data and og:site_name */\n\tsiteName?: string;\n\t/**\n\t * Breadcrumb trail for this page, root first. Pass an empty array\n\t * to explicitly opt out of breadcrumbs (e.g. homepage), or omit the\n\t * field to let consumers fall back to their own derivation.\n\t */\n\tbreadcrumbs?: BreadcrumbItem[];\n\t/** Public-facing site URL (origin) for structured data */\n\tsiteUrl?: string;\n}\n\n/** Input with Astro global -- used in .astro files */\ninterface AstroInput extends PageContextFields {\n\tAstro: { url: URL; currentLocale?: string };\n}\n\n/** Input with explicit URL -- used outside .astro files */\ninterface UrlInput extends PageContextFields {\n\turl: URL | string;\n\tlocale?: string;\n}\n\nexport type CreatePublicPageContextInput = AstroInput | UrlInput;\n\nfunction isAstroInput(input: CreatePublicPageContextInput): input is AstroInput {\n\treturn \"Astro\" in input;\n}\n\n/**\n * Build a PublicPageContext from template input.\n */\nexport function createPublicPageContext(input: CreatePublicPageContextInput): PublicPageContext {\n\tlet url: string;\n\tlet path: string;\n\tlet locale: string | null;\n\n\tif (isAstroInput(input)) {\n\t\turl = input.Astro.url.href;\n\t\tpath = input.Astro.url.pathname;\n\t\tlocale = input.Astro.currentLocale ?? null;\n\t} else {\n\t\tconst parsed = typeof input.url === \"string\" ? new URL(input.url) : input.url;\n\t\turl = parsed.href;\n\t\tpath = parsed.pathname;\n\t\tlocale = input.locale ?? null;\n\t}\n\n\treturn {\n\t\turl,\n\t\tpath,\n\t\tlocale,\n\t\tkind: input.kind,\n\t\tpageType: input.pageType ?? (input.kind === \"content\" ? \"article\" : \"website\"),\n\t\ttitle: input.title ?? null,\n\t\tpageTitle: input.pageTitle ?? null,\n\t\tdescription: input.description ?? null,\n\t\tcanonical: input.canonical ?? null,\n\t\timage: input.image ?? null,\n\t\tcontent: input.content\n\t\t\t? {\n\t\t\t\t\tcollection: input.content.collection,\n\t\t\t\t\tid: input.content.id,\n\t\t\t\t\tslug: input.content.slug ?? null,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tseo: input.seo,\n\t\tarticleMeta: input.articleMeta,\n\t\tsiteName: input.siteName,\n\t\tbreadcrumbs: input.breadcrumbs,\n\t\tsiteUrl: input.siteUrl,\n\t};\n}\n","/**\n * Page metadata collection and rendering\n *\n * Collects typed metadata contributions from plugins via the page:metadata hook,\n * validates them, and resolves them into a deduplicated structure ready to render.\n */\n\nimport type { PageMetadataContribution, PageMetadataLinkRel } from \"../plugins/types.js\";\n\n// ── Resolved output ─────────────────────────────────────────────\n\nexport interface ResolvedPageMetadata {\n\tmeta: Array<{ name: string; content: string }>;\n\tproperties: Array<{ property: string; content: string }>;\n\tlinks: Array<{\n\t\trel: PageMetadataLinkRel;\n\t\thref: string;\n\t\threflang?: string;\n\t}>;\n\tjsonld: Array<{ id?: string; json: string }>;\n}\n\n// ── Validation ──────────────────────────────────────────────────\n\n/** Schemes safe for use in link href attributes */\nconst SAFE_HREF_RE = /^(https?|at):\\/\\//i;\nconst HTML_ESCAPE_MAP: Record<string, string> = {\n\t\"&\": \"&amp;\",\n\t\"<\": \"&lt;\",\n\t\">\": \"&gt;\",\n\t'\"': \"&quot;\",\n\t\"'\": \"&#39;\",\n};\nconst HTML_ESCAPE_RE = /[&<>\"']/g;\n\n/** Escape a string for safe use in an HTML attribute value */\nexport function escapeHtmlAttr(value: string): string {\n\treturn value.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch] ?? ch);\n}\n\n/** Validate that a URL uses a safe scheme (http, https, at) */\nfunction isSafeHref(url: string): boolean {\n\treturn SAFE_HREF_RE.test(url);\n}\n\n// ── JSON-LD serialization ───────────────────────────────────────\n\nconst JSONLD_LT_RE = /</g;\nconst JSONLD_GT_RE = />/g;\nconst JSONLD_U2028_RE = /\\u2028/g;\nconst JSONLD_U2029_RE = /\\u2029/g;\n\n/**\n * Safely serialize a value for embedding in a <script type=\"application/ld+json\"> tag.\n *\n * Plain JSON.stringify is not sufficient because:\n * - \"</script>\" in a nested string breaks out of the script tag\n * - \"<!--\" can open an HTML comment\n * - U+2028/U+2029 are line terminators in some JS engines\n */\nexport function safeJsonLdSerialize(value: unknown): string {\n\treturn JSON.stringify(value)\n\t\t.replace(JSONLD_LT_RE, \"\\\\u003c\")\n\t\t.replace(JSONLD_GT_RE, \"\\\\u003e\")\n\t\t.replace(JSONLD_U2028_RE, \"\\\\u2028\")\n\t\t.replace(JSONLD_U2029_RE, \"\\\\u2029\");\n}\n\n// ── Merge / dedupe ──────────────────────────────────────────────\n\n/**\n * Resolve a flat list of contributions into deduplicated metadata.\n * First contribution wins for any given dedupe key.\n */\nexport function resolvePageMetadata(\n\tcontributions: PageMetadataContribution[],\n): ResolvedPageMetadata {\n\tconst result: ResolvedPageMetadata = {\n\t\tmeta: [],\n\t\tproperties: [],\n\t\tlinks: [],\n\t\tjsonld: [],\n\t};\n\n\tconst seenMeta = new Set<string>();\n\tconst seenProperties = new Set<string>();\n\tconst seenLinks = new Set<string>();\n\tconst seenJsonLd = new Set<string>();\n\n\tfor (const c of contributions) {\n\t\tswitch (c.kind) {\n\t\t\tcase \"meta\": {\n\t\t\t\tconst dedupeKey = c.key ?? c.name;\n\t\t\t\tif (seenMeta.has(dedupeKey)) continue;\n\t\t\t\tseenMeta.add(dedupeKey);\n\t\t\t\tresult.meta.push({ name: c.name, content: c.content });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"property\": {\n\t\t\t\tconst dedupeKey = c.key ?? c.property;\n\t\t\t\tif (seenProperties.has(dedupeKey)) continue;\n\t\t\t\tseenProperties.add(dedupeKey);\n\t\t\t\tresult.properties.push({\n\t\t\t\t\tproperty: c.property,\n\t\t\t\t\tcontent: c.content,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"link\": {\n\t\t\t\tif (!isSafeHref(c.href)) {\n\t\t\t\t\tif (import.meta.env?.DEV) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[page:metadata] Rejected link contribution with unsafe href scheme: ${c.href}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (c.rel === \"canonical\") {\n\t\t\t\t\tif (seenLinks.has(\"canonical\")) continue;\n\t\t\t\t\tseenLinks.add(\"canonical\");\n\t\t\t\t} else {\n\t\t\t\t\tconst dedupeKey = c.key ?? c.hreflang ?? c.href;\n\t\t\t\t\tif (seenLinks.has(dedupeKey)) continue;\n\t\t\t\t\tseenLinks.add(dedupeKey);\n\t\t\t\t}\n\t\t\t\tresult.links.push({\n\t\t\t\t\trel: c.rel,\n\t\t\t\t\thref: c.href,\n\t\t\t\t\t...(c.hreflang && { hreflang: c.hreflang }),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"jsonld\": {\n\t\t\t\tif (c.id) {\n\t\t\t\t\tif (seenJsonLd.has(c.id)) continue;\n\t\t\t\t\tseenJsonLd.add(c.id);\n\t\t\t\t}\n\t\t\t\tresult.jsonld.push({\n\t\t\t\t\tid: c.id,\n\t\t\t\t\tjson: safeJsonLdSerialize(c.graph),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown contribution kind -- skip silently at runtime.\n\t\t\t\t// TypeScript catches this at compile time for typed callers,\n\t\t\t\t// but sandboxed plugins may return unexpected shapes.\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// ── HTML rendering ──────────────────────────────────────────────\n\n/** Render resolved metadata to an HTML string for embedding in <head> */\nexport function renderPageMetadata(metadata: ResolvedPageMetadata): string {\n\tconst parts: string[] = [];\n\n\tfor (const m of metadata.meta) {\n\t\tparts.push(`<meta name=\"${escapeHtmlAttr(m.name)}\" content=\"${escapeHtmlAttr(m.content)}\">`);\n\t}\n\n\tfor (const p of metadata.properties) {\n\t\tparts.push(\n\t\t\t`<meta property=\"${escapeHtmlAttr(p.property)}\" content=\"${escapeHtmlAttr(p.content)}\">`,\n\t\t);\n\t}\n\n\tfor (const l of metadata.links) {\n\t\tlet tag = `<link rel=\"${escapeHtmlAttr(l.rel)}\" href=\"${escapeHtmlAttr(l.href)}\"`;\n\t\tif (l.hreflang) {\n\t\t\ttag += ` hreflang=\"${escapeHtmlAttr(l.hreflang)}\"`;\n\t\t}\n\t\ttag += \">\";\n\t\tparts.push(tag);\n\t}\n\n\tfor (const j of metadata.jsonld) {\n\t\tparts.push(`<script type=\"application/ld+json\">${j.json}</script>`);\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n","/**\n * Page fragment collection and rendering\n *\n * Collects raw markup / script contributions from trusted plugins via\n * the page:fragments hook. Sandboxed plugins are never invoked.\n */\n\nimport type { PageFragmentContribution, PagePlacement } from \"../plugins/types.js\";\nimport { escapeHtmlAttr } from \"./metadata.js\";\n\n/** Escape sequences that would break out of a script tag */\nconst SCRIPT_CLOSE_RE = /<\\//g;\n\n// ── Dedupe and filter ───────────────────────────────────────────\n\n/**\n * Filter contributions to a specific placement and deduplicate.\n * - Contributions with the same `key + placement` are deduped (first wins).\n * - External scripts with the same `src + placement` are deduped.\n */\nexport function resolveFragments(\n\tcontributions: PageFragmentContribution[],\n\tplacement: PagePlacement,\n): PageFragmentContribution[] {\n\tconst filtered = contributions.filter((c) => c.placement === placement);\n\tconst seen = new Set<string>();\n\tconst result: PageFragmentContribution[] = [];\n\n\tfor (const c of filtered) {\n\t\t// Key-based dedupe\n\t\tif (c.key) {\n\t\t\tconst dedupeKey = `key:${c.key}`;\n\t\t\tif (seen.has(dedupeKey)) continue;\n\t\t\tseen.add(dedupeKey);\n\t\t} else if (c.kind === \"external-script\") {\n\t\t\tconst dedupeKey = `src:${c.src}`;\n\t\t\tif (seen.has(dedupeKey)) continue;\n\t\t\tseen.add(dedupeKey);\n\t\t}\n\n\t\tresult.push(c);\n\t}\n\n\treturn result;\n}\n\n// ── HTML rendering ──────────────────────────────────────────────\n\nconst EVENT_HANDLER_RE = /^on/i;\n\nfunction renderAttributes(attrs: Record<string, string>): string {\n\treturn Object.entries(attrs)\n\t\t.filter(([k]) => !EVENT_HANDLER_RE.test(k))\n\t\t.map(([k, v]) => ` ${escapeHtmlAttr(k)}=\"${escapeHtmlAttr(v)}\"`)\n\t\t.join(\"\");\n}\n\n/** Render a single fragment contribution to HTML */\nfunction renderFragment(c: PageFragmentContribution): string {\n\tswitch (c.kind) {\n\t\tcase \"external-script\": {\n\t\t\tlet tag = `<script src=\"${escapeHtmlAttr(c.src)}\"`;\n\t\t\tif (c.async) tag += \" async\";\n\t\t\tif (c.defer) tag += \" defer\";\n\t\t\tif (c.attributes) tag += renderAttributes(c.attributes);\n\t\t\ttag += \"></script>\";\n\t\t\treturn tag;\n\t\t}\n\t\tcase \"inline-script\": {\n\t\t\tlet tag = \"<script\";\n\t\t\tif (c.attributes) tag += renderAttributes(c.attributes);\n\t\t\t// Escape </ to <\\/ to prevent breaking out of the script tag.\n\t\t\t// This is valid JS and protects against code built from user data.\n\t\t\ttag += `>${c.code.replace(SCRIPT_CLOSE_RE, \"<\\\\/\")}</script>`;\n\t\t\treturn tag;\n\t\t}\n\t\tcase \"html\":\n\t\t\treturn c.html;\n\t}\n}\n\n/** Render a list of fragment contributions to an HTML string */\nexport function renderFragments(\n\tcontributions: PageFragmentContribution[],\n\tplacement: PagePlacement,\n): string {\n\tconst resolved = resolveFragments(contributions, placement);\n\treturn resolved.map(renderFragment).join(\"\\n\");\n}\n","/**\n * JSON-LD structured data builders\n *\n * Moved from template SEO.astro components into core so all JSON-LD\n * is serialized via safeJsonLdSerialize() and never hand-rolled in templates.\n */\n\nimport type { PublicPageContext } from \"../plugins/types.js\";\n\n/**\n * Remove null/undefined values from a JSON-LD object recursively.\n * JSON-LD validators prefer absent keys over null values.\n */\nexport function cleanJsonLd(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst cleaned: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tif (value !== undefined && value !== null) {\n\t\t\tif (typeof value === \"object\" && !Array.isArray(value)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- non-null, non-array object is safely treated as Record<string, unknown> for JSON-LD traversal\n\t\t\t\tcleaned[key] = cleanJsonLd(value as Record<string, unknown>);\n\t\t\t} else {\n\t\t\t\tcleaned[key] = value;\n\t\t\t}\n\t\t}\n\t}\n\treturn cleaned;\n}\n\n/**\n * Build a BlogPosting JSON-LD graph from page context.\n * Used for article-type content pages.\n */\nexport function buildBlogPostingJsonLd(page: PublicPageContext): Record<string, unknown> | null {\n\tif (page.pageType !== \"article\" || !page.canonical) return null;\n\n\tconst ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;\n\tconst description = page.seo?.ogDescription || page.description;\n\tconst ogImage = page.seo?.ogImage || page.image;\n\tconst publishedTime = page.articleMeta?.publishedTime;\n\tconst modifiedTime = page.articleMeta?.modifiedTime;\n\tconst author = page.articleMeta?.author;\n\tconst siteName = page.siteName;\n\n\treturn cleanJsonLd({\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"BlogPosting\",\n\t\theadline: ogTitle,\n\t\tdescription,\n\t\timage: ogImage || undefined,\n\t\turl: page.canonical,\n\t\tdatePublished: publishedTime || undefined,\n\t\tdateModified: modifiedTime || publishedTime || undefined,\n\t\tauthor: author\n\t\t\t? {\n\t\t\t\t\t\"@type\": \"Person\",\n\t\t\t\t\tname: author,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tpublisher: siteName\n\t\t\t? {\n\t\t\t\t\t\"@type\": \"Organization\",\n\t\t\t\t\tname: siteName,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tmainEntityOfPage: {\n\t\t\t\"@type\": \"WebPage\",\n\t\t\t\"@id\": page.canonical,\n\t\t},\n\t});\n}\n\n/**\n * Build a WebSite JSON-LD graph from page context.\n * Used for non-article pages (homepage, listing pages, etc.)\n */\nexport function buildWebSiteJsonLd(page: PublicPageContext): Record<string, unknown> | null {\n\tconst siteName = page.siteName;\n\tif (!siteName) return null;\n\n\t// Use configured public origin, falling back to page URL origin\n\tlet siteUrl: string;\n\tif (page.siteUrl) {\n\t\tsiteUrl = page.siteUrl;\n\t} else {\n\t\ttry {\n\t\t\tsiteUrl = new URL(page.url).origin;\n\t\t} catch {\n\t\t\tsiteUrl = page.canonical || page.url;\n\t\t}\n\t}\n\n\treturn cleanJsonLd({\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"WebSite\",\n\t\tname: siteName,\n\t\turl: siteUrl,\n\t});\n}\n","/**\n * Generate base SEO metadata contributions from PublicPageContext.\n *\n * These contributions are prepended BEFORE plugin contributions in\n * resolvePageMetadata(), which uses first-wins dedup. This means\n * plugins can override any base SEO tag by contributing the same key.\n *\n * This replaces the per-template SEO.astro components, eliminating\n * the class of XSS bugs where templates hand-rolled JSON-LD serialization.\n */\n\nimport type { PageMetadataContribution, PublicPageContext } from \"../plugins/types.js\";\nimport { buildBlogPostingJsonLd, buildWebSiteJsonLd } from \"./jsonld.js\";\n\n/**\n * Generate base metadata contributions from a page context's SEO data.\n * Returns an empty array if no SEO-relevant data is present.\n */\nexport function generateBaseSeoContributions(page: PublicPageContext): PageMetadataContribution[] {\n\tconst contributions: PageMetadataContribution[] = [];\n\n\tconst description = page.description;\n\tconst ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;\n\tconst ogDescription = page.seo?.ogDescription || description;\n\tconst ogImage = page.seo?.ogImage || page.image;\n\tconst robots = page.seo?.robots;\n\tconst canonical = page.canonical;\n\tconst siteName = page.siteName;\n\n\t// -- Meta tags --\n\n\tif (description) {\n\t\tcontributions.push({ kind: \"meta\", name: \"description\", content: description });\n\t}\n\n\tif (robots) {\n\t\tcontributions.push({ kind: \"meta\", name: \"robots\", content: robots });\n\t}\n\n\t// -- Canonical link --\n\n\tif (canonical) {\n\t\tcontributions.push({ kind: \"link\", rel: \"canonical\", href: canonical });\n\t}\n\n\t// -- Open Graph --\n\n\tcontributions.push({\n\t\tkind: \"property\",\n\t\tproperty: \"og:type\",\n\t\tcontent: page.pageType === \"article\" ? \"article\" : \"website\",\n\t});\n\n\tif (ogTitle) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:title\", content: ogTitle });\n\t}\n\n\tif (ogDescription) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:description\", content: ogDescription });\n\t}\n\n\tif (ogImage) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:image\", content: ogImage });\n\t}\n\n\tif (canonical) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:url\", content: canonical });\n\t}\n\n\tif (siteName) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:site_name\", content: siteName });\n\t}\n\n\t// -- Twitter Card --\n\n\tcontributions.push({\n\t\tkind: \"meta\",\n\t\tname: \"twitter:card\",\n\t\tcontent: ogImage ? \"summary_large_image\" : \"summary\",\n\t});\n\n\tif (ogTitle) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:title\", content: ogTitle });\n\t}\n\n\tif (ogDescription) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:description\", content: ogDescription });\n\t}\n\n\tif (ogImage) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:image\", content: ogImage });\n\t}\n\n\t// -- Article metadata --\n\n\tif (page.pageType === \"article\" && page.articleMeta) {\n\t\tconst { publishedTime, modifiedTime, author } = page.articleMeta;\n\t\tif (publishedTime) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:published_time\",\n\t\t\t\tcontent: publishedTime,\n\t\t\t});\n\t\t}\n\t\tif (modifiedTime) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:modified_time\",\n\t\t\t\tcontent: modifiedTime,\n\t\t\t});\n\t\t}\n\t\tif (author) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:author\",\n\t\t\t\tcontent: author,\n\t\t\t});\n\t\t}\n\t}\n\n\t// -- JSON-LD --\n\n\tif (page.pageType === \"article\") {\n\t\tconst blogPosting = buildBlogPostingJsonLd(page);\n\t\tif (blogPosting) {\n\t\t\tcontributions.push({ kind: \"jsonld\", id: \"primary\", graph: blogPosting });\n\t\t}\n\t} else if (siteName) {\n\t\tconst webSite = buildWebSiteJsonLd(page);\n\t\tif (webSite) {\n\t\t\tcontributions.push({ kind: \"jsonld\", id: \"primary\", graph: webSite });\n\t\t}\n\t}\n\n\treturn contributions;\n}\n","/**\n * emdash/page — Public page contribution API\n *\n * Template integration points for plugin-driven head metadata\n * and trusted body fragments.\n */\n\nimport type {\n\tPublicPageContext,\n\tPageMetadataContribution,\n\tPageFragmentContribution,\n} from \"../plugins/types.js\";\n\nexport { createPublicPageContext } from \"./context.js\";\nexport type { CreatePublicPageContextInput } from \"./context.js\";\n\nexport {\n\tresolvePageMetadata,\n\trenderPageMetadata,\n\tsafeJsonLdSerialize,\n\tescapeHtmlAttr,\n} from \"./metadata.js\";\nexport type { ResolvedPageMetadata } from \"./metadata.js\";\n\nexport { resolveFragments, renderFragments } from \"./fragments.js\";\n\nexport { generateBaseSeoContributions } from \"./seo-contributions.js\";\nexport { cleanJsonLd, buildBlogPostingJsonLd, buildWebSiteJsonLd } from \"./jsonld.js\";\n\n/**\n * Shape of the EmDash runtime methods used by the render components.\n * Extracted here so all three components share a single type definition.\n */\nexport interface EmDashPageRuntime {\n\tcollectPageMetadata: (page: PublicPageContext) => Promise<PageMetadataContribution[]>;\n\tcollectPageFragments: (page: PublicPageContext) => Promise<PageFragmentContribution[]>;\n}\n\n/**\n * Get the page runtime from Astro locals. Returns undefined when\n * EmDash is not initialized (components render nothing in that case).\n */\nexport function getPageRuntime(locals: Record<string, unknown>): EmDashPageRuntime | undefined {\n\tconst emdash = locals.emdash;\n\tif (\n\t\temdash &&\n\t\ttypeof emdash === \"object\" &&\n\t\t\"collectPageMetadata\" in emdash &&\n\t\t\"collectPageFragments\" in emdash\n\t) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- structural check above confirms presence of required methods\n\t\treturn emdash as EmDashPageRuntime;\n\t}\n\treturn undefined;\n}\n\n// Astro render components are exported from \"emdash/ui\":\n// import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from \"emdash/ui\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/page/context.ts","../../src/page/metadata.ts","../../src/page/fragments.ts","../../src/page/jsonld.ts","../../src/page/seo-contributions.ts","../../src/page/index.ts"],"sourcesContent":["/**\n * Public page context builder\n *\n * Templates call this to describe the page being rendered.\n * The resulting context is passed to EmDashHead / EmDashBodyStart / EmDashBodyEnd.\n */\n\nimport type { BreadcrumbItem, PublicPageContext } from \"../plugins/types.js\";\n\n/** Fields shared by both input forms */\ninterface PageContextFields {\n\tkind: \"content\" | \"custom\";\n\tpageType?: string;\n\ttitle?: string | null;\n\tpageTitle?: string | null;\n\tdescription?: string | null;\n\tcanonical?: string | null;\n\timage?: string | null;\n\tcontent?: { collection: string; id: string; slug?: string | null };\n\t/** SEO overrides for OG/Twitter meta generation */\n\tseo?: {\n\t\togTitle?: string | null;\n\t\togDescription?: string | null;\n\t\togImage?: string | null;\n\t\trobots?: string | null;\n\t};\n\t/** Article metadata for Open Graph article: tags */\n\tarticleMeta?: {\n\t\tpublishedTime?: string | null;\n\t\tmodifiedTime?: string | null;\n\t\tauthor?: string | null;\n\t};\n\t/** Site name for structured data and og:site_name */\n\tsiteName?: string;\n\t/**\n\t * Breadcrumb trail for this page, root first. Pass an empty array\n\t * to explicitly opt out of breadcrumbs (e.g. homepage), or omit the\n\t * field to let consumers fall back to their own derivation.\n\t */\n\tbreadcrumbs?: BreadcrumbItem[];\n\t/** Public-facing site URL (origin) for structured data */\n\tsiteUrl?: string;\n}\n\n/** Input with Astro global -- used in .astro files */\ninterface AstroInput extends PageContextFields {\n\tAstro: { url: URL; currentLocale?: string };\n}\n\n/** Input with explicit URL -- used outside .astro files */\ninterface UrlInput extends PageContextFields {\n\turl: URL | string;\n\tlocale?: string;\n}\n\nexport type CreatePublicPageContextInput = AstroInput | UrlInput;\n\nfunction isAstroInput(input: CreatePublicPageContextInput): input is AstroInput {\n\treturn \"Astro\" in input;\n}\n\n/**\n * Build a PublicPageContext from template input.\n */\nexport function createPublicPageContext(input: CreatePublicPageContextInput): PublicPageContext {\n\tlet url: string;\n\tlet path: string;\n\tlet locale: string | null;\n\n\tif (isAstroInput(input)) {\n\t\turl = input.Astro.url.href;\n\t\tpath = input.Astro.url.pathname;\n\t\tlocale = input.Astro.currentLocale ?? null;\n\t} else {\n\t\tconst parsed = typeof input.url === \"string\" ? new URL(input.url) : input.url;\n\t\turl = parsed.href;\n\t\tpath = parsed.pathname;\n\t\tlocale = input.locale ?? null;\n\t}\n\n\treturn {\n\t\turl,\n\t\tpath,\n\t\tlocale,\n\t\tkind: input.kind,\n\t\tpageType: input.pageType ?? (input.kind === \"content\" ? \"article\" : \"website\"),\n\t\ttitle: input.title ?? null,\n\t\tpageTitle: input.pageTitle ?? null,\n\t\tdescription: input.description ?? null,\n\t\tcanonical: input.canonical ?? null,\n\t\timage: input.image ?? null,\n\t\tcontent: input.content\n\t\t\t? {\n\t\t\t\t\tcollection: input.content.collection,\n\t\t\t\t\tid: input.content.id,\n\t\t\t\t\tslug: input.content.slug ?? null,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tseo: input.seo,\n\t\tarticleMeta: input.articleMeta,\n\t\tsiteName: input.siteName,\n\t\tbreadcrumbs: input.breadcrumbs,\n\t\tsiteUrl: input.siteUrl,\n\t};\n}\n","/**\n * Page metadata collection and rendering\n *\n * Collects typed metadata contributions from plugins via the page:metadata hook,\n * validates them, and resolves them into a deduplicated structure ready to render.\n */\n\nimport type { PageMetadataContribution, PageMetadataLinkRel } from \"../plugins/types.js\";\n\n// ── Resolved output ─────────────────────────────────────────────\n\nexport interface ResolvedPageMetadata {\n\tmeta: Array<{ name: string; content: string }>;\n\tproperties: Array<{ property: string; content: string }>;\n\tlinks: Array<{\n\t\trel: PageMetadataLinkRel;\n\t\thref: string;\n\t\threflang?: string;\n\t}>;\n\tjsonld: Array<{ id?: string; json: string }>;\n}\n\n// ── Validation ──────────────────────────────────────────────────\n\n/** Schemes safe for use in link href attributes */\nconst SAFE_HREF_RE = /^(https?|at):\\/\\//i;\nconst HTML_ESCAPE_MAP: Record<string, string> = {\n\t\"&\": \"&amp;\",\n\t\"<\": \"&lt;\",\n\t\">\": \"&gt;\",\n\t'\"': \"&quot;\",\n\t\"'\": \"&#39;\",\n};\nconst HTML_ESCAPE_RE = /[&<>\"']/g;\n\n/** Escape a string for safe use in an HTML attribute value */\nexport function escapeHtmlAttr(value: string): string {\n\treturn value.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch] ?? ch);\n}\n\n/** Validate that a URL uses a safe scheme (http, https, at) */\nfunction isSafeHref(url: string): boolean {\n\treturn SAFE_HREF_RE.test(url);\n}\n\n// ── JSON-LD serialization ───────────────────────────────────────\n\nconst JSONLD_LT_RE = /</g;\nconst JSONLD_GT_RE = />/g;\nconst JSONLD_U2028_RE = /\\u2028/g;\nconst JSONLD_U2029_RE = /\\u2029/g;\n\n/**\n * Safely serialize a value for embedding in a <script type=\"application/ld+json\"> tag.\n *\n * Plain JSON.stringify is not sufficient because:\n * - \"</script>\" in a nested string breaks out of the script tag\n * - \"<!--\" can open an HTML comment\n * - U+2028/U+2029 are line terminators in some JS engines\n */\nexport function safeJsonLdSerialize(value: unknown): string {\n\treturn JSON.stringify(value)\n\t\t.replace(JSONLD_LT_RE, \"\\\\u003c\")\n\t\t.replace(JSONLD_GT_RE, \"\\\\u003e\")\n\t\t.replace(JSONLD_U2028_RE, \"\\\\u2028\")\n\t\t.replace(JSONLD_U2029_RE, \"\\\\u2029\");\n}\n\n// ── Merge / dedupe ──────────────────────────────────────────────\n\n/**\n * Resolve a flat list of contributions into deduplicated metadata.\n * First contribution wins for any given dedupe key.\n */\nexport function resolvePageMetadata(\n\tcontributions: PageMetadataContribution[],\n): ResolvedPageMetadata {\n\tconst result: ResolvedPageMetadata = {\n\t\tmeta: [],\n\t\tproperties: [],\n\t\tlinks: [],\n\t\tjsonld: [],\n\t};\n\n\tconst seenMeta = new Set<string>();\n\tconst seenProperties = new Set<string>();\n\tconst seenLinks = new Set<string>();\n\tconst seenJsonLd = new Set<string>();\n\n\tfor (const c of contributions) {\n\t\tswitch (c.kind) {\n\t\t\tcase \"meta\": {\n\t\t\t\tconst dedupeKey = c.key ?? c.name;\n\t\t\t\tif (seenMeta.has(dedupeKey)) continue;\n\t\t\t\tseenMeta.add(dedupeKey);\n\t\t\t\tresult.meta.push({ name: c.name, content: c.content });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"property\": {\n\t\t\t\tconst dedupeKey = c.key ?? c.property;\n\t\t\t\tif (seenProperties.has(dedupeKey)) continue;\n\t\t\t\tseenProperties.add(dedupeKey);\n\t\t\t\tresult.properties.push({\n\t\t\t\t\tproperty: c.property,\n\t\t\t\t\tcontent: c.content,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"link\": {\n\t\t\t\tif (!isSafeHref(c.href)) {\n\t\t\t\t\tif (import.meta.env?.DEV) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[page:metadata] Rejected link contribution with unsafe href scheme: ${c.href}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (c.rel === \"canonical\") {\n\t\t\t\t\tif (seenLinks.has(\"canonical\")) continue;\n\t\t\t\t\tseenLinks.add(\"canonical\");\n\t\t\t\t} else {\n\t\t\t\t\tconst dedupeKey = c.key ?? c.hreflang ?? c.href;\n\t\t\t\t\tif (seenLinks.has(dedupeKey)) continue;\n\t\t\t\t\tseenLinks.add(dedupeKey);\n\t\t\t\t}\n\t\t\t\tresult.links.push({\n\t\t\t\t\trel: c.rel,\n\t\t\t\t\thref: c.href,\n\t\t\t\t\t...(c.hreflang && { hreflang: c.hreflang }),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"jsonld\": {\n\t\t\t\tif (c.id) {\n\t\t\t\t\tif (seenJsonLd.has(c.id)) continue;\n\t\t\t\t\tseenJsonLd.add(c.id);\n\t\t\t\t}\n\t\t\t\tresult.jsonld.push({\n\t\t\t\t\tid: c.id,\n\t\t\t\t\tjson: safeJsonLdSerialize(c.graph),\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown contribution kind -- skip silently at runtime.\n\t\t\t\t// TypeScript catches this at compile time for typed callers,\n\t\t\t\t// but sandboxed plugins may return unexpected shapes.\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// ── HTML rendering ──────────────────────────────────────────────\n\n/** Render resolved metadata to an HTML string for embedding in <head> */\nexport function renderPageMetadata(metadata: ResolvedPageMetadata): string {\n\tconst parts: string[] = [];\n\n\tfor (const m of metadata.meta) {\n\t\tparts.push(`<meta name=\"${escapeHtmlAttr(m.name)}\" content=\"${escapeHtmlAttr(m.content)}\">`);\n\t}\n\n\tfor (const p of metadata.properties) {\n\t\tparts.push(\n\t\t\t`<meta property=\"${escapeHtmlAttr(p.property)}\" content=\"${escapeHtmlAttr(p.content)}\">`,\n\t\t);\n\t}\n\n\tfor (const l of metadata.links) {\n\t\tlet tag = `<link rel=\"${escapeHtmlAttr(l.rel)}\" href=\"${escapeHtmlAttr(l.href)}\"`;\n\t\tif (l.hreflang) {\n\t\t\ttag += ` hreflang=\"${escapeHtmlAttr(l.hreflang)}\"`;\n\t\t}\n\t\ttag += \">\";\n\t\tparts.push(tag);\n\t}\n\n\tfor (const j of metadata.jsonld) {\n\t\tparts.push(`<script type=\"application/ld+json\">${j.json}</script>`);\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n","/**\n * Page fragment collection and rendering\n *\n * Collects raw markup / script contributions from trusted plugins via\n * the page:fragments hook. Sandboxed plugins are never invoked.\n */\n\nimport type { PageFragmentContribution, PagePlacement } from \"../plugins/types.js\";\nimport { escapeHtmlAttr } from \"./metadata.js\";\n\n/** Escape sequences that would break out of a script tag */\nconst SCRIPT_CLOSE_RE = /<\\//g;\n\n// ── Dedupe and filter ───────────────────────────────────────────\n\n/**\n * Filter contributions to a specific placement and deduplicate.\n * - Contributions with the same `key + placement` are deduped (first wins).\n * - External scripts with the same `src + placement` are deduped.\n */\nexport function resolveFragments(\n\tcontributions: PageFragmentContribution[],\n\tplacement: PagePlacement,\n): PageFragmentContribution[] {\n\tconst filtered = contributions.filter((c) => c.placement === placement);\n\tconst seen = new Set<string>();\n\tconst result: PageFragmentContribution[] = [];\n\n\tfor (const c of filtered) {\n\t\t// Key-based dedupe\n\t\tif (c.key) {\n\t\t\tconst dedupeKey = `key:${c.key}`;\n\t\t\tif (seen.has(dedupeKey)) continue;\n\t\t\tseen.add(dedupeKey);\n\t\t} else if (c.kind === \"external-script\") {\n\t\t\tconst dedupeKey = `src:${c.src}`;\n\t\t\tif (seen.has(dedupeKey)) continue;\n\t\t\tseen.add(dedupeKey);\n\t\t}\n\n\t\tresult.push(c);\n\t}\n\n\treturn result;\n}\n\n// ── HTML rendering ──────────────────────────────────────────────\n\nconst EVENT_HANDLER_RE = /^on/i;\n\nfunction renderAttributes(attrs: Record<string, string>): string {\n\treturn Object.entries(attrs)\n\t\t.filter(([k]) => !EVENT_HANDLER_RE.test(k))\n\t\t.map(([k, v]) => ` ${escapeHtmlAttr(k)}=\"${escapeHtmlAttr(v)}\"`)\n\t\t.join(\"\");\n}\n\n/** Render a single fragment contribution to HTML */\nfunction renderFragment(c: PageFragmentContribution): string {\n\tswitch (c.kind) {\n\t\tcase \"external-script\": {\n\t\t\tlet tag = `<script src=\"${escapeHtmlAttr(c.src)}\"`;\n\t\t\tif (c.async) tag += \" async\";\n\t\t\tif (c.defer) tag += \" defer\";\n\t\t\tif (c.attributes) tag += renderAttributes(c.attributes);\n\t\t\ttag += \"></script>\";\n\t\t\treturn tag;\n\t\t}\n\t\tcase \"inline-script\": {\n\t\t\tlet tag = \"<script\";\n\t\t\tif (c.attributes) tag += renderAttributes(c.attributes);\n\t\t\t// Escape </ to <\\/ to prevent breaking out of the script tag.\n\t\t\t// This is valid JS and protects against code built from user data.\n\t\t\ttag += `>${c.code.replace(SCRIPT_CLOSE_RE, \"<\\\\/\")}</script>`;\n\t\t\treturn tag;\n\t\t}\n\t\tcase \"html\":\n\t\t\treturn c.html;\n\t}\n}\n\n/** Render a list of fragment contributions to an HTML string */\nexport function renderFragments(\n\tcontributions: PageFragmentContribution[],\n\tplacement: PagePlacement,\n): string {\n\tconst resolved = resolveFragments(contributions, placement);\n\treturn resolved.map(renderFragment).join(\"\\n\");\n}\n","/**\n * JSON-LD structured data builders\n *\n * Moved from template SEO.astro components into core so all JSON-LD\n * is serialized via safeJsonLdSerialize() and never hand-rolled in templates.\n */\n\nimport type { PublicPageContext } from \"../plugins/types.js\";\n\n/**\n * Remove null/undefined values from a JSON-LD object recursively.\n * JSON-LD validators prefer absent keys over null values.\n */\nexport function cleanJsonLd(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst cleaned: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(obj)) {\n\t\tif (value !== undefined && value !== null) {\n\t\t\tif (typeof value === \"object\" && !Array.isArray(value)) {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- non-null, non-array object is safely treated as Record<string, unknown> for JSON-LD traversal\n\t\t\t\tcleaned[key] = cleanJsonLd(value as Record<string, unknown>);\n\t\t\t} else {\n\t\t\t\tcleaned[key] = value;\n\t\t\t}\n\t\t}\n\t}\n\treturn cleaned;\n}\n\n/**\n * Build a BlogPosting JSON-LD graph from page context.\n * Used for article-type content pages.\n */\nexport function buildBlogPostingJsonLd(page: PublicPageContext): Record<string, unknown> | null {\n\tif (page.pageType !== \"article\" || !page.canonical) return null;\n\n\tconst ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;\n\tconst description = page.seo?.ogDescription || page.description;\n\tconst ogImage = page.seo?.ogImage || page.image;\n\tconst publishedTime = page.articleMeta?.publishedTime;\n\tconst modifiedTime = page.articleMeta?.modifiedTime;\n\tconst author = page.articleMeta?.author;\n\tconst siteName = page.siteName;\n\n\treturn cleanJsonLd({\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"BlogPosting\",\n\t\theadline: ogTitle,\n\t\tdescription,\n\t\timage: ogImage || undefined,\n\t\turl: page.canonical,\n\t\tdatePublished: publishedTime || undefined,\n\t\tdateModified: modifiedTime || publishedTime || undefined,\n\t\tauthor: author\n\t\t\t? {\n\t\t\t\t\t\"@type\": \"Person\",\n\t\t\t\t\tname: author,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tpublisher: siteName\n\t\t\t? {\n\t\t\t\t\t\"@type\": \"Organization\",\n\t\t\t\t\tname: siteName,\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tmainEntityOfPage: {\n\t\t\t\"@type\": \"WebPage\",\n\t\t\t\"@id\": page.canonical,\n\t\t},\n\t});\n}\n\n/**\n * Build a WebSite JSON-LD graph from page context.\n * Used for non-article pages (homepage, listing pages, etc.)\n */\nexport function buildWebSiteJsonLd(page: PublicPageContext): Record<string, unknown> | null {\n\tconst siteName = page.siteName;\n\tif (!siteName) return null;\n\n\t// Use configured public origin, falling back to page URL origin\n\tlet siteUrl: string;\n\tif (page.siteUrl) {\n\t\tsiteUrl = page.siteUrl;\n\t} else {\n\t\ttry {\n\t\t\tsiteUrl = new URL(page.url).origin;\n\t\t} catch {\n\t\t\tsiteUrl = page.canonical || page.url;\n\t\t}\n\t}\n\n\treturn cleanJsonLd({\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"WebSite\",\n\t\tname: siteName,\n\t\turl: siteUrl,\n\t});\n}\n","/**\n * Generate base SEO metadata contributions from PublicPageContext.\n *\n * These contributions are prepended BEFORE plugin contributions in\n * resolvePageMetadata(), which uses first-wins dedup. This means\n * plugins can override any base SEO tag by contributing the same key.\n *\n * This replaces the per-template SEO.astro components, eliminating\n * the class of XSS bugs where templates hand-rolled JSON-LD serialization.\n */\n\nimport type { PageMetadataContribution, PublicPageContext } from \"../plugins/types.js\";\nimport type { SeoSettings } from \"../settings/types.js\";\nimport { buildBlogPostingJsonLd, buildWebSiteJsonLd } from \"./jsonld.js\";\n\n/**\n * Generate base metadata contributions from a page context's SEO data.\n * Returns an empty array if no SEO-relevant data is present.\n */\nexport function generateBaseSeoContributions(page: PublicPageContext): PageMetadataContribution[] {\n\tconst contributions: PageMetadataContribution[] = [];\n\n\tconst description = page.description;\n\tconst ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;\n\tconst ogDescription = page.seo?.ogDescription || description;\n\tconst ogImage = page.seo?.ogImage || page.image;\n\tconst robots = page.seo?.robots;\n\tconst canonical = page.canonical;\n\tconst siteName = page.siteName;\n\n\t// -- Meta tags --\n\n\tif (description) {\n\t\tcontributions.push({ kind: \"meta\", name: \"description\", content: description });\n\t}\n\n\tif (robots) {\n\t\tcontributions.push({ kind: \"meta\", name: \"robots\", content: robots });\n\t}\n\n\t// -- Canonical link --\n\n\tif (canonical) {\n\t\tcontributions.push({ kind: \"link\", rel: \"canonical\", href: canonical });\n\t}\n\n\t// -- Open Graph --\n\n\tcontributions.push({\n\t\tkind: \"property\",\n\t\tproperty: \"og:type\",\n\t\tcontent: page.pageType === \"article\" ? \"article\" : \"website\",\n\t});\n\n\tif (ogTitle) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:title\", content: ogTitle });\n\t}\n\n\tif (ogDescription) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:description\", content: ogDescription });\n\t}\n\n\tif (ogImage) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:image\", content: ogImage });\n\t}\n\n\tif (canonical) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:url\", content: canonical });\n\t}\n\n\tif (siteName) {\n\t\tcontributions.push({ kind: \"property\", property: \"og:site_name\", content: siteName });\n\t}\n\n\t// -- Twitter Card --\n\n\tcontributions.push({\n\t\tkind: \"meta\",\n\t\tname: \"twitter:card\",\n\t\tcontent: ogImage ? \"summary_large_image\" : \"summary\",\n\t});\n\n\tif (ogTitle) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:title\", content: ogTitle });\n\t}\n\n\tif (ogDescription) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:description\", content: ogDescription });\n\t}\n\n\tif (ogImage) {\n\t\tcontributions.push({ kind: \"meta\", name: \"twitter:image\", content: ogImage });\n\t}\n\n\t// -- Article metadata --\n\n\tif (page.pageType === \"article\" && page.articleMeta) {\n\t\tconst { publishedTime, modifiedTime, author } = page.articleMeta;\n\t\tif (publishedTime) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:published_time\",\n\t\t\t\tcontent: publishedTime,\n\t\t\t});\n\t\t}\n\t\tif (modifiedTime) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:modified_time\",\n\t\t\t\tcontent: modifiedTime,\n\t\t\t});\n\t\t}\n\t\tif (author) {\n\t\t\tcontributions.push({\n\t\t\t\tkind: \"property\",\n\t\t\t\tproperty: \"article:author\",\n\t\t\t\tcontent: author,\n\t\t\t});\n\t\t}\n\t}\n\n\t// -- JSON-LD --\n\n\tif (page.pageType === \"article\") {\n\t\tconst blogPosting = buildBlogPostingJsonLd(page);\n\t\tif (blogPosting) {\n\t\t\tcontributions.push({ kind: \"jsonld\", id: \"primary\", graph: blogPosting });\n\t\t}\n\t} else if (siteName) {\n\t\tconst webSite = buildWebSiteJsonLd(page);\n\t\tif (webSite) {\n\t\t\tcontributions.push({ kind: \"jsonld\", id: \"primary\", graph: webSite });\n\t\t}\n\t}\n\n\treturn contributions;\n}\n\n/**\n * Generate site-level SEO metadata contributions from SiteSettings.seo.\n *\n * These tags apply to every page (search engine ownership verification),\n * so they're sourced from site settings rather than per-page context.\n * Returns an empty array when no relevant settings are configured.\n */\nexport function generateSiteSeoContributions(\n\tseoSettings: SeoSettings | undefined,\n): PageMetadataContribution[] {\n\tconst contributions: PageMetadataContribution[] = [];\n\n\tif (!seoSettings) {\n\t\treturn contributions;\n\t}\n\n\tif (seoSettings.googleVerification) {\n\t\tcontributions.push({\n\t\t\tkind: \"meta\",\n\t\t\tname: \"google-site-verification\",\n\t\t\tcontent: seoSettings.googleVerification,\n\t\t});\n\t}\n\n\tif (seoSettings.bingVerification) {\n\t\tcontributions.push({\n\t\t\tkind: \"meta\",\n\t\t\tname: \"msvalidate.01\",\n\t\t\tcontent: seoSettings.bingVerification,\n\t\t});\n\t}\n\n\treturn contributions;\n}\n","/**\n * emdash/page — Public page contribution API\n *\n * Template integration points for plugin-driven head metadata\n * and trusted body fragments.\n */\n\nimport type {\n\tPublicPageContext,\n\tPageMetadataContribution,\n\tPageFragmentContribution,\n} from \"../plugins/types.js\";\n\nexport { createPublicPageContext } from \"./context.js\";\nexport type { CreatePublicPageContextInput } from \"./context.js\";\n\nexport {\n\tresolvePageMetadata,\n\trenderPageMetadata,\n\tsafeJsonLdSerialize,\n\tescapeHtmlAttr,\n} from \"./metadata.js\";\nexport type { ResolvedPageMetadata } from \"./metadata.js\";\n\nexport { resolveFragments, renderFragments } from \"./fragments.js\";\n\nexport { generateBaseSeoContributions, generateSiteSeoContributions } from \"./seo-contributions.js\";\nexport { cleanJsonLd, buildBlogPostingJsonLd, buildWebSiteJsonLd } from \"./jsonld.js\";\n\n/**\n * Shape of the EmDash runtime methods used by the render components.\n * Extracted here so all three components share a single type definition.\n */\nexport interface EmDashPageRuntime {\n\tcollectPageMetadata: (page: PublicPageContext) => Promise<PageMetadataContribution[]>;\n\tcollectPageFragments: (page: PublicPageContext) => Promise<PageFragmentContribution[]>;\n}\n\n/**\n * Get the page runtime from Astro locals. Returns undefined when\n * EmDash is not initialized (components render nothing in that case).\n */\nexport function getPageRuntime(locals: Record<string, unknown>): EmDashPageRuntime | undefined {\n\tconst emdash = locals.emdash;\n\tif (\n\t\temdash &&\n\t\ttypeof emdash === \"object\" &&\n\t\t\"collectPageMetadata\" in emdash &&\n\t\t\"collectPageFragments\" in emdash\n\t) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- structural check above confirms presence of required methods\n\t\treturn emdash as EmDashPageRuntime;\n\t}\n\treturn undefined;\n}\n\n// Astro render components are exported from \"emdash/ui\":\n// import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from \"emdash/ui\";\n"],"mappings":"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"}
@@ -265,4 +265,4 @@ function downsample(src, srcW, srcH, dstW, dstH) {
265
265
 
266
266
  //#endregion
267
267
  export { normalizeMediaValue as n, generatePlaceholder as t };
268
- //# sourceMappingURL=placeholder-DntBEQo7.mjs.map
268
+ //# sourceMappingURL=placeholder-C-fk5hYI.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"placeholder-DntBEQo7.mjs","names":[],"sources":["../src/media/normalize.ts","../src/media/placeholder.ts"],"sourcesContent":["/**\n * Media Value Normalization\n *\n * Normalizes media field values into a consistent shape regardless of\n * creation path (seed scripts, media picker, WP import, URL input).\n *\n * Called at content create/update time when a media provider is available,\n * filling in missing dimensions, storageKey, mimeType, and filename from\n * the provider's `get()` method.\n */\n\nimport type { MediaProvider, MediaProviderItem, MediaValue } from \"./types.js\";\n\nconst INTERNAL_MEDIA_PREFIX = \"/_emdash/api/media/file/\";\nconst URL_PATTERN = /^https?:\\/\\//;\n\n/**\n * Normalize a media field value into a consistent MediaValue shape.\n *\n * - `null`/`undefined` → `null`\n * - Bare URL string → `{ provider: \"external\", id: \"\", src: url }`\n * - Bare internal media URL → resolved via local provider's `get()`\n * - Object with `provider` + `id` → enriched with missing fields from provider\n */\nexport async function normalizeMediaValue(\n\tvalue: unknown,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue | null> {\n\tif (value == null) return null;\n\n\t// Bare string URL\n\tif (typeof value === \"string\") {\n\t\treturn normalizeStringUrl(value, getProvider);\n\t}\n\n\t// Not an object — can't normalize\n\tif (!isRecord(value)) return null;\n\n\t// Must have at least an id to be a valid media value\n\tif (!(\"id\" in value) && !(\"src\" in value)) return null;\n\n\tconst provider = (typeof value.provider === \"string\" ? value.provider : undefined) || \"local\";\n\tconst id = typeof value.id === \"string\" ? value.id : \"\";\n\n\t// External URLs — return as-is, no server-side dimension detection\n\tif (provider === \"external\") {\n\t\treturn recordToMediaValue(value);\n\t}\n\n\t// Build the base value from the input\n\tconst result: MediaValue = { ...recordToMediaValue(value), provider };\n\n\t// For local media, strip `src` — it's derived at display time from storageKey\n\tif (provider === \"local\") {\n\t\tdelete result.src;\n\t}\n\n\t// Determine if we need to call the provider\n\tconst needsDimensions = result.width == null || result.height == null;\n\tconst needsStorageKey = provider === \"local\" && !result.meta?.storageKey;\n\tconst needsFileInfo = !result.mimeType || !result.filename;\n\tconst needsLookup = needsDimensions || needsStorageKey || needsFileInfo;\n\n\tif (!needsLookup || !id) return result;\n\n\t// Try to enrich from provider\n\tconst mediaProvider = getProvider(provider);\n\tif (!mediaProvider?.get) return result;\n\n\tlet providerItem: MediaProviderItem | null;\n\ttry {\n\t\tproviderItem = await mediaProvider.get(id);\n\t} catch {\n\t\treturn result;\n\t}\n\n\tif (!providerItem) return result;\n\n\treturn mergeProviderData(result, providerItem);\n}\n\nfunction normalizeStringUrl(\n\turl: string,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue | null> {\n\t// Internal media URL — try to resolve via local provider\n\tif (url.startsWith(INTERNAL_MEDIA_PREFIX)) {\n\t\treturn resolveInternalUrl(url, getProvider);\n\t}\n\n\t// External HTTP(S) URL\n\tif (URL_PATTERN.test(url)) {\n\t\treturn Promise.resolve({\n\t\t\tprovider: \"external\",\n\t\t\tid: \"\",\n\t\t\tsrc: url,\n\t\t});\n\t}\n\n\t// Unrecognized string — treat as external\n\treturn Promise.resolve({\n\t\tprovider: \"external\",\n\t\tid: \"\",\n\t\tsrc: url,\n\t});\n}\n\nasync function resolveInternalUrl(\n\turl: string,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue> {\n\tconst storageKey = url.slice(INTERNAL_MEDIA_PREFIX.length);\n\tconst localProvider = getProvider(\"local\");\n\n\tif (!localProvider?.get) {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\tlet item: MediaProviderItem | null;\n\ttry {\n\t\titem = await localProvider.get(storageKey);\n\t} catch {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\tif (!item) {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\treturn {\n\t\tprovider: \"local\",\n\t\tid: item.id,\n\t\tfilename: item.filename,\n\t\tmimeType: item.mimeType,\n\t\twidth: item.width,\n\t\theight: item.height,\n\t\talt: item.alt,\n\t\tmeta: item.meta,\n\t};\n}\n\n/**\n * Merge provider data into an existing MediaValue, preserving caller-supplied fields.\n * Caller `alt` takes priority over provider `alt` (per-usage, not per-image).\n */\nfunction mergeProviderData(existing: MediaValue, item: MediaProviderItem): MediaValue {\n\tconst result = { ...existing };\n\n\t// Fill missing dimensions\n\tif (result.width == null && item.width != null) result.width = item.width;\n\tif (result.height == null && item.height != null) result.height = item.height;\n\n\t// Fill missing file info\n\tif (!result.filename && item.filename) result.filename = item.filename;\n\tif (!result.mimeType && item.mimeType) result.mimeType = item.mimeType;\n\n\t// Fill missing alt (provider alt is fallback, not override)\n\tif (!result.alt && item.alt) result.alt = item.alt;\n\n\t// Fill missing meta (merge, don't replace)\n\tif (item.meta) {\n\t\tresult.meta = { ...item.meta, ...result.meta };\n\t}\n\n\treturn result;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Extract known MediaValue fields from a runtime-checked record.\n * Avoids unsafe `as MediaValue` cast by reading each property explicitly.\n */\nfunction recordToMediaValue(obj: Record<string, unknown>): MediaValue {\n\tconst result: MediaValue = {\n\t\tid: typeof obj.id === \"string\" ? obj.id : \"\",\n\t};\n\tif (typeof obj.provider === \"string\") result.provider = obj.provider;\n\tif (typeof obj.src === \"string\") result.src = obj.src;\n\tif (typeof obj.previewUrl === \"string\") result.previewUrl = obj.previewUrl;\n\tif (typeof obj.filename === \"string\") result.filename = obj.filename;\n\tif (typeof obj.mimeType === \"string\") result.mimeType = obj.mimeType;\n\tif (typeof obj.width === \"number\") result.width = obj.width;\n\tif (typeof obj.height === \"number\") result.height = obj.height;\n\tif (typeof obj.alt === \"string\") result.alt = obj.alt;\n\tif (isRecord(obj.meta)) result.meta = obj.meta;\n\treturn result;\n}\n","/**\n * Image Placeholder Generation\n *\n * Generates blurhash and dominant color from image buffers for LQIP support.\n * Decodes images via jpeg-js (pure JS) and upng-js (pure JS, uses pako for\n * deflate). No Node-specific dependencies — works in Workers and Node SSR.\n */\n\nimport { encode } from \"blurhash\";\nimport { imageSize } from \"image-size\";\n\nexport interface PlaceholderData {\n\tblurhash: string;\n\tdominantColor: string;\n}\n\nconst SUPPORTED_TYPES: Record<string, \"jpeg\" | \"png\"> = {\n\t\"image/jpeg\": \"jpeg\",\n\t\"image/jpg\": \"jpeg\",\n\t\"image/png\": \"png\",\n};\n\n/** Max width for blurhash input. Encode is O(w*h*components), so downsample first. */\nconst MAX_ENCODE_WIDTH = 32;\n\n/** Max decoded RGBA size (32 MB). Images exceeding this skip placeholder generation. */\nconst MAX_DECODED_BYTES = 32 * 1024 * 1024;\n\ninterface DecodedImage {\n\twidth: number;\n\theight: number;\n\tdata: Uint8Array;\n}\n\n/**\n * Decode a JPEG buffer into raw RGBA pixel data.\n */\nasync function decodeJpeg(buffer: Uint8Array): Promise<DecodedImage> {\n\tconst { decode } = await import(\"jpeg-js\");\n\tconst result = decode(buffer, { useTArray: true });\n\treturn { width: result.width, height: result.height, data: result.data };\n}\n\n/**\n * Decode a PNG buffer into raw RGBA pixel data.\n * Uses upng-js (pure JS with pako deflate) — no Node zlib dependency.\n */\nasync function decodePng(buffer: Uint8Array): Promise<DecodedImage> {\n\t// @ts-expect-error -- upng-js has no type declarations\n\tconst UPNG = (await import(\"upng-js\")).default;\n\tconst img = UPNG.decode(buffer.buffer);\n\t// toRGBA8 returns an array of frames; take the first frame\n\tconst frames: ArrayBuffer[] = UPNG.toRGBA8(img);\n\tconst rgba = new Uint8Array(frames[0]);\n\treturn { width: img.width, height: img.height, data: rgba };\n}\n\n/**\n * Extract the dominant color from RGBA pixel data.\n * Simple average of all non-transparent pixels.\n */\nfunction extractDominantColor(data: Uint8Array, width: number, height: number): string {\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tlet count = 0;\n\n\tconst len = width * height * 4;\n\tfor (let i = 0; i < len; i += 4) {\n\t\tconst a = data[i + 3];\n\t\tif (a < 128) continue; // skip mostly-transparent pixels\n\t\tr += data[i];\n\t\tg += data[i + 1];\n\t\tb += data[i + 2];\n\t\tcount++;\n\t}\n\n\tif (count === 0) return \"rgb(0,0,0)\";\n\n\tconst avgR = Math.round(r / count);\n\tconst avgG = Math.round(g / count);\n\tconst avgB = Math.round(b / count);\n\treturn `rgb(${avgR},${avgG},${avgB})`;\n}\n\n/**\n * Read image dimensions from headers without decoding pixel data.\n */\nfunction getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {\n\ttry {\n\t\tconst result = imageSize(buffer);\n\t\tif (result.width != null && result.height != null) {\n\t\t\treturn { width: result.width, height: result.height };\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Generate blurhash and dominant color from an image buffer.\n * Returns null for non-image MIME types or on failure.\n *\n * @param dimensions - Optional pre-known dimensions. Used as a fallback when\n * image-size cannot parse the buffer (e.g. truncated headers). When the\n * decoded size (width * height * 4) exceeds MAX_DECODED_BYTES, placeholder\n * generation is skipped to avoid OOM on memory-constrained runtimes.\n */\nexport async function generatePlaceholder(\n\tbuffer: Uint8Array,\n\tmimeType: string,\n\tdimensions?: { width: number; height: number },\n): Promise<PlaceholderData | null> {\n\tconst format = SUPPORTED_TYPES[mimeType];\n\tif (!format) return null;\n\n\ttry {\n\t\t// Safety net: skip decode if the image would exceed the memory budget\n\t\tconst dims = getImageDimensions(buffer) ?? dimensions;\n\t\tif (dims && dims.width * dims.height * 4 > MAX_DECODED_BYTES) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst imageData = format === \"jpeg\" ? await decodeJpeg(buffer) : await decodePng(buffer);\n\t\tconst { width, height, data } = imageData;\n\n\t\tif (width === 0 || height === 0) return null;\n\n\t\t// Downsample for blurhash encoding if needed\n\t\tlet encodePixels: Uint8ClampedArray;\n\t\tlet encodeWidth: number;\n\t\tlet encodeHeight: number;\n\n\t\tif (width > MAX_ENCODE_WIDTH) {\n\t\t\tconst scale = MAX_ENCODE_WIDTH / width;\n\t\t\tencodeWidth = MAX_ENCODE_WIDTH;\n\t\t\tencodeHeight = Math.max(1, Math.round(height * scale));\n\t\t\tencodePixels = downsample(data, width, height, encodeWidth, encodeHeight);\n\t\t} else {\n\t\t\tencodeWidth = width;\n\t\t\tencodeHeight = height;\n\t\t\tencodePixels = new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);\n\t\t}\n\n\t\tconst blurhash = encode(encodePixels, encodeWidth, encodeHeight, 4, 3);\n\t\tconst dominantColor = extractDominantColor(data, width, height);\n\n\t\treturn { blurhash, dominantColor };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Nearest-neighbor downsample of RGBA pixel data.\n */\nfunction downsample(\n\tsrc: Uint8Array,\n\tsrcW: number,\n\tsrcH: number,\n\tdstW: number,\n\tdstH: number,\n): Uint8ClampedArray {\n\tconst dst = new Uint8ClampedArray(dstW * dstH * 4);\n\n\tfor (let y = 0; y < dstH; y++) {\n\t\tconst srcY = Math.floor((y * srcH) / dstH);\n\t\tfor (let x = 0; x < dstW; x++) {\n\t\t\tconst srcX = Math.floor((x * srcW) / dstW);\n\t\t\tconst srcIdx = (srcY * srcW + srcX) * 4;\n\t\t\tconst dstIdx = (y * dstW + x) * 4;\n\t\t\tdst[dstIdx] = src[srcIdx]!;\n\t\t\tdst[dstIdx + 1] = src[srcIdx + 1]!;\n\t\t\tdst[dstIdx + 2] = src[srcIdx + 2]!;\n\t\t\tdst[dstIdx + 3] = src[srcIdx + 3]!;\n\t\t}\n\t}\n\n\treturn dst;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"placeholder-C-fk5hYI.mjs","names":[],"sources":["../src/media/normalize.ts","../src/media/placeholder.ts"],"sourcesContent":["/**\n * Media Value Normalization\n *\n * Normalizes media field values into a consistent shape regardless of\n * creation path (seed scripts, media picker, WP import, URL input).\n *\n * Called at content create/update time when a media provider is available,\n * filling in missing dimensions, storageKey, mimeType, and filename from\n * the provider's `get()` method.\n */\n\nimport type { MediaProvider, MediaProviderItem, MediaValue } from \"./types.js\";\n\nconst INTERNAL_MEDIA_PREFIX = \"/_emdash/api/media/file/\";\nconst URL_PATTERN = /^https?:\\/\\//;\n\n/**\n * Normalize a media field value into a consistent MediaValue shape.\n *\n * - `null`/`undefined` → `null`\n * - Bare URL string → `{ provider: \"external\", id: \"\", src: url }`\n * - Bare internal media URL → resolved via local provider's `get()`\n * - Object with `provider` + `id` → enriched with missing fields from provider\n */\nexport async function normalizeMediaValue(\n\tvalue: unknown,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue | null> {\n\tif (value == null) return null;\n\n\t// Bare string URL\n\tif (typeof value === \"string\") {\n\t\treturn normalizeStringUrl(value, getProvider);\n\t}\n\n\t// Not an object — can't normalize\n\tif (!isRecord(value)) return null;\n\n\t// Must have at least an id to be a valid media value\n\tif (!(\"id\" in value) && !(\"src\" in value)) return null;\n\n\tconst provider = (typeof value.provider === \"string\" ? value.provider : undefined) || \"local\";\n\tconst id = typeof value.id === \"string\" ? value.id : \"\";\n\n\t// External URLs — return as-is, no server-side dimension detection\n\tif (provider === \"external\") {\n\t\treturn recordToMediaValue(value);\n\t}\n\n\t// Build the base value from the input\n\tconst result: MediaValue = { ...recordToMediaValue(value), provider };\n\n\t// For local media, strip `src` — it's derived at display time from storageKey\n\tif (provider === \"local\") {\n\t\tdelete result.src;\n\t}\n\n\t// Determine if we need to call the provider\n\tconst needsDimensions = result.width == null || result.height == null;\n\tconst needsStorageKey = provider === \"local\" && !result.meta?.storageKey;\n\tconst needsFileInfo = !result.mimeType || !result.filename;\n\tconst needsLookup = needsDimensions || needsStorageKey || needsFileInfo;\n\n\tif (!needsLookup || !id) return result;\n\n\t// Try to enrich from provider\n\tconst mediaProvider = getProvider(provider);\n\tif (!mediaProvider?.get) return result;\n\n\tlet providerItem: MediaProviderItem | null;\n\ttry {\n\t\tproviderItem = await mediaProvider.get(id);\n\t} catch {\n\t\treturn result;\n\t}\n\n\tif (!providerItem) return result;\n\n\treturn mergeProviderData(result, providerItem);\n}\n\nfunction normalizeStringUrl(\n\turl: string,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue | null> {\n\t// Internal media URL — try to resolve via local provider\n\tif (url.startsWith(INTERNAL_MEDIA_PREFIX)) {\n\t\treturn resolveInternalUrl(url, getProvider);\n\t}\n\n\t// External HTTP(S) URL\n\tif (URL_PATTERN.test(url)) {\n\t\treturn Promise.resolve({\n\t\t\tprovider: \"external\",\n\t\t\tid: \"\",\n\t\t\tsrc: url,\n\t\t});\n\t}\n\n\t// Unrecognized string — treat as external\n\treturn Promise.resolve({\n\t\tprovider: \"external\",\n\t\tid: \"\",\n\t\tsrc: url,\n\t});\n}\n\nasync function resolveInternalUrl(\n\turl: string,\n\tgetProvider: (id: string) => MediaProvider | undefined,\n): Promise<MediaValue> {\n\tconst storageKey = url.slice(INTERNAL_MEDIA_PREFIX.length);\n\tconst localProvider = getProvider(\"local\");\n\n\tif (!localProvider?.get) {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\tlet item: MediaProviderItem | null;\n\ttry {\n\t\titem = await localProvider.get(storageKey);\n\t} catch {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\tif (!item) {\n\t\treturn { provider: \"external\", id: \"\", src: url };\n\t}\n\n\treturn {\n\t\tprovider: \"local\",\n\t\tid: item.id,\n\t\tfilename: item.filename,\n\t\tmimeType: item.mimeType,\n\t\twidth: item.width,\n\t\theight: item.height,\n\t\talt: item.alt,\n\t\tmeta: item.meta,\n\t};\n}\n\n/**\n * Merge provider data into an existing MediaValue, preserving caller-supplied fields.\n * Caller `alt` takes priority over provider `alt` (per-usage, not per-image).\n */\nfunction mergeProviderData(existing: MediaValue, item: MediaProviderItem): MediaValue {\n\tconst result = { ...existing };\n\n\t// Fill missing dimensions\n\tif (result.width == null && item.width != null) result.width = item.width;\n\tif (result.height == null && item.height != null) result.height = item.height;\n\n\t// Fill missing file info\n\tif (!result.filename && item.filename) result.filename = item.filename;\n\tif (!result.mimeType && item.mimeType) result.mimeType = item.mimeType;\n\n\t// Fill missing alt (provider alt is fallback, not override)\n\tif (!result.alt && item.alt) result.alt = item.alt;\n\n\t// Fill missing meta (merge, don't replace)\n\tif (item.meta) {\n\t\tresult.meta = { ...item.meta, ...result.meta };\n\t}\n\n\treturn result;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Extract known MediaValue fields from a runtime-checked record.\n * Avoids unsafe `as MediaValue` cast by reading each property explicitly.\n */\nfunction recordToMediaValue(obj: Record<string, unknown>): MediaValue {\n\tconst result: MediaValue = {\n\t\tid: typeof obj.id === \"string\" ? obj.id : \"\",\n\t};\n\tif (typeof obj.provider === \"string\") result.provider = obj.provider;\n\tif (typeof obj.src === \"string\") result.src = obj.src;\n\tif (typeof obj.previewUrl === \"string\") result.previewUrl = obj.previewUrl;\n\tif (typeof obj.filename === \"string\") result.filename = obj.filename;\n\tif (typeof obj.mimeType === \"string\") result.mimeType = obj.mimeType;\n\tif (typeof obj.width === \"number\") result.width = obj.width;\n\tif (typeof obj.height === \"number\") result.height = obj.height;\n\tif (typeof obj.alt === \"string\") result.alt = obj.alt;\n\tif (isRecord(obj.meta)) result.meta = obj.meta;\n\treturn result;\n}\n","/**\n * Image Placeholder Generation\n *\n * Generates blurhash and dominant color from image buffers for LQIP support.\n * Decodes images via jpeg-js (pure JS) and upng-js (pure JS, uses pako for\n * deflate). No Node-specific dependencies — works in Workers and Node SSR.\n */\n\nimport { encode } from \"blurhash\";\nimport { imageSize } from \"image-size\";\n\nexport interface PlaceholderData {\n\tblurhash: string;\n\tdominantColor: string;\n}\n\nconst SUPPORTED_TYPES: Record<string, \"jpeg\" | \"png\"> = {\n\t\"image/jpeg\": \"jpeg\",\n\t\"image/jpg\": \"jpeg\",\n\t\"image/png\": \"png\",\n};\n\n/** Max width for blurhash input. Encode is O(w*h*components), so downsample first. */\nconst MAX_ENCODE_WIDTH = 32;\n\n/** Max decoded RGBA size (32 MB). Images exceeding this skip placeholder generation. */\nconst MAX_DECODED_BYTES = 32 * 1024 * 1024;\n\ninterface DecodedImage {\n\twidth: number;\n\theight: number;\n\tdata: Uint8Array;\n}\n\n/**\n * Decode a JPEG buffer into raw RGBA pixel data.\n */\nasync function decodeJpeg(buffer: Uint8Array): Promise<DecodedImage> {\n\tconst { decode } = await import(\"jpeg-js\");\n\tconst result = decode(buffer, { useTArray: true });\n\treturn { width: result.width, height: result.height, data: result.data };\n}\n\n/**\n * Decode a PNG buffer into raw RGBA pixel data.\n * Uses upng-js (pure JS with pako deflate) — no Node zlib dependency.\n */\nasync function decodePng(buffer: Uint8Array): Promise<DecodedImage> {\n\t// @ts-expect-error -- upng-js has no type declarations\n\tconst UPNG = (await import(\"upng-js\")).default;\n\tconst img = UPNG.decode(buffer.buffer);\n\t// toRGBA8 returns an array of frames; take the first frame\n\tconst frames: ArrayBuffer[] = UPNG.toRGBA8(img);\n\tconst rgba = new Uint8Array(frames[0]);\n\treturn { width: img.width, height: img.height, data: rgba };\n}\n\n/**\n * Extract the dominant color from RGBA pixel data.\n * Simple average of all non-transparent pixels.\n */\nfunction extractDominantColor(data: Uint8Array, width: number, height: number): string {\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tlet count = 0;\n\n\tconst len = width * height * 4;\n\tfor (let i = 0; i < len; i += 4) {\n\t\tconst a = data[i + 3];\n\t\tif (a < 128) continue; // skip mostly-transparent pixels\n\t\tr += data[i];\n\t\tg += data[i + 1];\n\t\tb += data[i + 2];\n\t\tcount++;\n\t}\n\n\tif (count === 0) return \"rgb(0,0,0)\";\n\n\tconst avgR = Math.round(r / count);\n\tconst avgG = Math.round(g / count);\n\tconst avgB = Math.round(b / count);\n\treturn `rgb(${avgR},${avgG},${avgB})`;\n}\n\n/**\n * Read image dimensions from headers without decoding pixel data.\n */\nfunction getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {\n\ttry {\n\t\tconst result = imageSize(buffer);\n\t\tif (result.width != null && result.height != null) {\n\t\t\treturn { width: result.width, height: result.height };\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Generate blurhash and dominant color from an image buffer.\n * Returns null for non-image MIME types or on failure.\n *\n * @param dimensions - Optional pre-known dimensions. Used as a fallback when\n * image-size cannot parse the buffer (e.g. truncated headers). When the\n * decoded size (width * height * 4) exceeds MAX_DECODED_BYTES, placeholder\n * generation is skipped to avoid OOM on memory-constrained runtimes.\n */\nexport async function generatePlaceholder(\n\tbuffer: Uint8Array,\n\tmimeType: string,\n\tdimensions?: { width: number; height: number },\n): Promise<PlaceholderData | null> {\n\tconst format = SUPPORTED_TYPES[mimeType];\n\tif (!format) return null;\n\n\ttry {\n\t\t// Safety net: skip decode if the image would exceed the memory budget\n\t\tconst dims = getImageDimensions(buffer) ?? dimensions;\n\t\tif (dims && dims.width * dims.height * 4 > MAX_DECODED_BYTES) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst imageData = format === \"jpeg\" ? await decodeJpeg(buffer) : await decodePng(buffer);\n\t\tconst { width, height, data } = imageData;\n\n\t\tif (width === 0 || height === 0) return null;\n\n\t\t// Downsample for blurhash encoding if needed\n\t\tlet encodePixels: Uint8ClampedArray;\n\t\tlet encodeWidth: number;\n\t\tlet encodeHeight: number;\n\n\t\tif (width > MAX_ENCODE_WIDTH) {\n\t\t\tconst scale = MAX_ENCODE_WIDTH / width;\n\t\t\tencodeWidth = MAX_ENCODE_WIDTH;\n\t\t\tencodeHeight = Math.max(1, Math.round(height * scale));\n\t\t\tencodePixels = downsample(data, width, height, encodeWidth, encodeHeight);\n\t\t} else {\n\t\t\tencodeWidth = width;\n\t\t\tencodeHeight = height;\n\t\t\tencodePixels = new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);\n\t\t}\n\n\t\tconst blurhash = encode(encodePixels, encodeWidth, encodeHeight, 4, 3);\n\t\tconst dominantColor = extractDominantColor(data, width, height);\n\n\t\treturn { blurhash, dominantColor };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Nearest-neighbor downsample of RGBA pixel data.\n */\nfunction downsample(\n\tsrc: Uint8Array,\n\tsrcW: number,\n\tsrcH: number,\n\tdstW: number,\n\tdstH: number,\n): Uint8ClampedArray {\n\tconst dst = new Uint8ClampedArray(dstW * dstH * 4);\n\n\tfor (let y = 0; y < dstH; y++) {\n\t\tconst srcY = Math.floor((y * srcH) / dstH);\n\t\tfor (let x = 0; x < dstW; x++) {\n\t\t\tconst srcX = Math.floor((x * srcW) / dstW);\n\t\t\tconst srcIdx = (srcY * srcW + srcX) * 4;\n\t\t\tconst dstIdx = (y * dstW + x) * 4;\n\t\t\tdst[dstIdx] = src[srcIdx]!;\n\t\t\tdst[dstIdx + 1] = src[srcIdx + 1]!;\n\t\t\tdst[dstIdx + 2] = src[srcIdx + 2]!;\n\t\t\tdst[dstIdx + 3] = src[srcIdx + 3]!;\n\t\t}\n\t}\n\n\treturn dst;\n}\n"],"mappings":"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"}
@@ -281,4 +281,4 @@ declare function generatePlaceholder(buffer: Uint8Array, mimeType: string, dimen
281
281
  }): Promise<PlaceholderData | null>;
282
282
  //#endregion
283
283
  export { MediaValue as _, ComponentEmbed as a, mediaItemToValue as b, EmbedResult as c, MediaListResult as d, MediaProvider as f, MediaUploadInput as g, MediaProviderItem as h, AudioEmbed as i, ImageEmbed as l, MediaProviderDescriptor as m, generatePlaceholder as n, CreateMediaProviderFn as o, MediaProviderCapabilities as p, normalizeMediaValue as r, EmbedOptions as s, PlaceholderData as t, MediaListOptions as u, ThumbnailOptions as v, VideoEmbed as y };
284
- //# sourceMappingURL=placeholder-BBCtpTES.d.mts.map
284
+ //# sourceMappingURL=placeholder-tzpqGWII.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"placeholder-BBCtpTES.d.mts","names":[],"sources":["../src/media/types.ts","../src/media/normalize.ts","../src/media/placeholder.ts"],"mappings":"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"}
1
+ {"version":3,"file":"placeholder-tzpqGWII.d.mts","names":[],"sources":["../src/media/types.ts","../src/media/normalize.ts","../src/media/placeholder.ts"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
- import "../types-B6BzlZxx.mjs";
2
- import { fn as PluginDescriptor } from "../index-CCWzlriB.mjs";
3
- import "../runner-DYv3rX8P.mjs";
4
- import { X as ResolvedPlugin, tt as StandardPluginDefinition } from "../types-C3ronwXb.mjs";
5
- import "../validate-Db1yNL3i.mjs";
1
+ import "../types-C2v0c34j.mjs";
2
+ import { mn as PluginDescriptor } from "../index-De6_Xv3v.mjs";
3
+ import "../runner-BR2xKwhn.mjs";
4
+ import { X as ResolvedPlugin, tt as StandardPluginDefinition } from "../types-DgrIP0tF.mjs";
5
+ import "../validate-kM8Pjuf7.mjs";
6
6
 
7
7
  //#region src/plugins/adapt-sandbox-entry.d.ts
8
8
  /**
@@ -1,4 +1,4 @@
1
- import { n as PLUGIN_CAPABILITIES, t as HOOK_NAMES } from "../manifest-schema-BsXINkQD.mjs";
1
+ import { n as PLUGIN_CAPABILITIES, t as HOOK_NAMES } from "../manifest-schema-V30qsMft.mjs";
2
2
 
3
3
  //#region src/plugins/adapt-sandbox-entry.ts
4
4
  /**
@@ -1,6 +1,8 @@
1
1
  import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
2
- import { n as getI18nConfig, r as isI18nEnabled, t as getFallbackChain } from "./config-DkxPrM9l.mjs";
3
2
  import { getRequestContext } from "./request-context.mjs";
3
+ import { n as getI18nConfig, r as isI18nEnabled, t as getFallbackChain } from "./config-BXwuX8Bx.mjs";
4
+ import { t as isMissingTableError } from "./db-errors-D0UT85nC.mjs";
5
+ import { n as requestCached } from "./request-cache-DiR961CV.mjs";
4
6
 
5
7
  //#region src/visual-editing/editable.ts
6
8
  /**
@@ -167,6 +169,35 @@ function entryEditOptions(entry) {
167
169
  * ```
168
170
  */
169
171
  async function getEmDashCollection(type, filter) {
172
+ return requestCached(collectionCacheKey(type, filter), () => getEmDashCollectionUncached(type, filter));
173
+ }
174
+ /**
175
+ * Build a canonical cache key for `getEmDashCollection`.
176
+ *
177
+ * `JSON.stringify` is insertion-order-sensitive, so two callers passing
178
+ * semantically identical filters with different key orders would miss
179
+ * the cache. We fix the top-level field order and sort `where` keys
180
+ * (order there is irrelevant), while preserving `orderBy` key order
181
+ * because that's the sort priority.
182
+ */
183
+ function collectionCacheKey(type, filter) {
184
+ if (!filter) return `collection:${type}:`;
185
+ return `collection:${type}:${[
186
+ filter.status ?? "",
187
+ filter.limit ?? "",
188
+ filter.cursor ?? "",
189
+ filter.where ? stableStringify(filter.where) : "",
190
+ filter.orderBy ? JSON.stringify(filter.orderBy) : "",
191
+ filter.locale ?? ""
192
+ ].join("|")}`;
193
+ }
194
+ function stableStringify(value) {
195
+ const keys = Object.keys(value).toSorted();
196
+ const ordered = {};
197
+ for (const k of keys) ordered[k] = value[k];
198
+ return JSON.stringify(ordered);
199
+ }
200
+ async function getEmDashCollectionUncached(type, filter) {
170
201
  const { getLiveCollection } = await import("astro:content");
171
202
  const ctx = getRequestContext();
172
203
  const i18nConfig = getI18nConfig();
@@ -197,7 +228,7 @@ async function getEmDashCollection(type, filter) {
197
228
  edit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop()
198
229
  };
199
230
  });
200
- await hydrateEntryBylines(type, entriesWithEdit);
231
+ await Promise.all([hydrateEntryBylines(type, entriesWithEdit), hydrateEntryTerms(type, entriesWithEdit)]);
201
232
  return {
202
233
  entries: entriesWithEdit,
203
234
  nextCursor,
@@ -249,9 +280,9 @@ async function getEmDashEntry(type, id, options) {
249
280
  return status === "published" || !!(status === "scheduled" && scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date());
250
281
  }
251
282
  const localeChain = requestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];
252
- /** Return a successful EntryResult with bylines hydrated */
283
+ /** Return a successful EntryResult with bylines and taxonomy terms hydrated */
253
284
  async function successResult(wrapped, opts) {
254
- await hydrateEntryBylines(type, [wrapped]);
285
+ await Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);
255
286
  return {
256
287
  entry: wrapped,
257
288
  isPreview: opts.isPreview,
@@ -350,7 +381,7 @@ async function getEmDashEntry(type, id, options) {
350
381
  async function hydrateEntryBylines(type, entries) {
351
382
  if (entries.length === 0) return;
352
383
  try {
353
- const { getBylinesForEntries } = await import("./bylines-C_Wsnz4L.mjs").then((n) => n.t);
384
+ const { getBylinesForEntries } = await import("./bylines-hPTW79hw.mjs").then((n) => n.t);
354
385
  const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
355
386
  if (ids.length === 0) return;
356
387
  const bylinesMap = await getBylinesForEntries(type, ids);
@@ -363,8 +394,44 @@ async function hydrateEntryBylines(type, entries) {
363
394
  data.byline = credits[0]?.byline ?? null;
364
395
  }
365
396
  } catch (err) {
366
- const msg = err instanceof Error ? err.message : "";
367
- if (!msg.includes("no such table")) console.warn("[emdash] Failed to hydrate bylines:", msg);
397
+ if (!isMissingTableError(err)) {
398
+ const msg = err instanceof Error ? err.message : String(err);
399
+ console.warn("[emdash] Failed to hydrate bylines:", msg);
400
+ }
401
+ }
402
+ }
403
+ /**
404
+ * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.
405
+ *
406
+ * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm
407
+ * values) to each entry's data object. Uses a single batched JOIN query across
408
+ * all taxonomies so the cost is O(1) regardless of the number of entries or
409
+ * taxonomies on the site.
410
+ *
411
+ * This eliminates the common N+1 pattern where templates loop over list
412
+ * results and call getEntryTerms() per entry. With hydration, the list page
413
+ * stays at a single round-trip for term data.
414
+ *
415
+ * Fails silently if the taxonomy tables don't exist yet (pre-migration).
416
+ */
417
+ async function hydrateEntryTerms(type, entries) {
418
+ if (entries.length === 0) return;
419
+ try {
420
+ const { getAllTermsForEntries } = await import("./taxonomies-K2z0Uhnj.mjs").then((n) => n.u);
421
+ const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
422
+ if (ids.length === 0) return;
423
+ const termsMap = await getAllTermsForEntries(type, ids);
424
+ for (const entry of entries) {
425
+ const data = entryData(entry);
426
+ const dbId = dataStr(data, "id");
427
+ if (!dbId) continue;
428
+ data.terms = termsMap.get(dbId) ?? {};
429
+ }
430
+ } catch (err) {
431
+ if (!isMissingTableError(err)) {
432
+ const msg = err instanceof Error ? err.message : String(err);
433
+ console.warn("[emdash] Failed to hydrate terms:", msg);
434
+ }
368
435
  }
369
436
  }
370
437
  /**
@@ -384,9 +451,9 @@ async function hydrateEntryBylines(type, entries) {
384
451
  */
385
452
  async function getTranslations(type, id) {
386
453
  try {
387
- const db = (await import("./loader-BYzwzORf.mjs").then((n) => n.r)).getDb;
454
+ const db = (await import("./loader-DeiBJEMe.mjs").then((n) => n.r)).getDb;
388
455
  const dbInstance = await db();
389
- const { ContentRepository } = await import("./content-BsBoyj8G.mjs").then((n) => n.n);
456
+ const { ContentRepository } = await import("./content-D7J5y73J.mjs").then((n) => n.n);
390
457
  const repo = new ContentRepository(dbInstance);
391
458
  const item = await repo.findByIdOrSlug(type, id);
392
459
  if (!item) return {
@@ -455,8 +522,8 @@ function invalidateUrlPatternCache() {
455
522
  */
456
523
  async function resolveEmDashPath(path) {
457
524
  if (!cachedUrlPatterns) {
458
- const { getDb } = await import("./loader-BYzwzORf.mjs").then((n) => n.r);
459
- const { SchemaRegistry } = await import("./registry-BgnP3ysR.mjs").then((n) => n.r);
525
+ const { getDb } = await import("./loader-DeiBJEMe.mjs").then((n) => n.r);
526
+ const { SchemaRegistry } = await import("./registry-Ci3WxVAr.mjs").then((n) => n.r);
460
527
  const collections = await new SchemaRegistry(await getDb()).listCollections();
461
528
  cachedUrlPatterns = [];
462
529
  for (const collection of collections) {
@@ -488,4 +555,4 @@ async function resolveEmDashPath(path) {
488
555
 
489
556
  //#endregion
490
557
  export { invalidateUrlPatternCache as a, createEditable as c, getTranslations as i, createNoop as l, getEmDashCollection as n, query_exports as o, getEmDashEntry as r, resolveEmDashPath as s, getEditMeta as t };
491
- //# sourceMappingURL=query-B6Vu0d2i.mjs.map
558
+ //# sourceMappingURL=query-g4Ug-9j9.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-g4Ug-9j9.mjs","names":[],"sources":["../src/visual-editing/editable.ts","../src/query.ts"],"sourcesContent":["/**\n * Visual editing annotation system\n *\n * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements.\n */\n\nexport interface CMSAnnotation {\n\tcollection: string;\n\tid: string;\n\tfield?: string;\n\t/** Entry status — only present on entry-level annotations (not field-level) */\n\tstatus?: string;\n\t/** Whether the entry has unpublished draft changes */\n\thasDraft?: boolean;\n}\n\n/** The shape returned when spreading an edit annotation onto an element */\nexport interface FieldAnnotation {\n\t\"data-emdash-ref\": string;\n}\n\nexport interface EditableOptions {\n\t/** Entry status: \"draft\", \"published\", \"scheduled\" */\n\tstatus?: string;\n\t/** true when draftRevisionId exists and differs from liveRevisionId */\n\thasDraft?: boolean;\n}\n\n/**\n * Create an editable proxy for an entry.\n *\n * Usage:\n * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft)\n * - `{...entry.edit.title}` - field-level annotation\n * - `{...entry.edit['nested.field']}` - nested field (bracket notation)\n */\nexport function createEditable(\n\tcollection: string,\n\tid: string,\n\toptions?: EditableOptions,\n): EditProxy {\n\tconst base: CMSAnnotation = {\n\t\tcollection,\n\t\tid,\n\t\t...(options?.status && { status: options.status }),\n\t\t...(options?.hasDraft && { hasDraft: true }),\n\t};\n\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (prop === \"toJSON\") return () => ({ \"data-emdash-ref\": JSON.stringify(base) });\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\n\t\t\t// data-emdash-ref access returns the entry-level string\n\t\t\tif (prop === \"data-emdash-ref\") return JSON.stringify(base);\n\n\t\t\t// Field-level: return a FieldAnnotation for the specific field\n\t\t\treturn {\n\t\t\t\t\"data-emdash-ref\": JSON.stringify({ ...base, field: String(prop) }),\n\t\t\t} satisfies FieldAnnotation;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [\"data-emdash-ref\"];\n\t\t},\n\t\tgetOwnPropertyDescriptor(_, prop) {\n\t\t\tif (prop === \"data-emdash-ref\") {\n\t\t\t\treturn {\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tvalue: JSON.stringify(base),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Create a noop proxy for production mode.\n * Spreading this produces no attributes.\n */\nexport function createNoop(): EditProxy {\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\t\t\t// All property access returns undefined in noop mode\n\t\t\treturn undefined;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [];\n\t\t},\n\t\tgetOwnPropertyDescriptor() {\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Visual editing proxy type.\n *\n * Spread directly onto elements for entry-level annotations: `{...entry.edit}`\n * Access a field for field-level annotations: `{...entry.edit.title}`\n *\n * In production, spreading produces no attributes (noop).\n */\nexport type EditProxy = {\n\treadonly [field: string]: Partial<FieldAnnotation>;\n};\n","/**\n * Query functions for EmDash content\n *\n * These wrap Astro's getLiveCollection/getLiveEntry with type filtering.\n * Use these instead of calling Astro's functions directly.\n *\n * Error handling follows Astro's pattern - returns { entries/entry, error }\n * so callers can gracefully handle errors (including 404s).\n *\n * Preview mode is handled implicitly via ALS request context —\n * no parameters needed. The middleware verifies the preview token\n * and sets the context; query functions read it automatically.\n */\n\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./i18n/config.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport {\n\tcreateEditable,\n\tcreateNoop,\n\ttype EditProxy,\n\ttype EditableOptions,\n} from \"./visual-editing/editable.js\";\n\n/**\n * Collection type registry for type-safe queries.\n *\n * This interface is extended by the generated emdash-env.d.ts file\n * to provide type inference for collection names and their data shapes.\n *\n * @example\n * ```ts\n * // In emdash-env.d.ts (generated):\n * declare module \"emdash\" {\n * interface EmDashCollections {\n * posts: { title: string; content: PortableTextBlock[]; };\n * pages: { title: string; body: PortableTextBlock[]; };\n * }\n * }\n *\n * // Then in your code:\n * const { entries } = await getEmDashCollection(\"posts\");\n * // entries[0].data.title is typed as string\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EmDashCollections {}\n\n/**\n * Helper type to infer the data type for a collection.\n * Returns the registered type if known, otherwise falls back to Record<string, unknown>.\n */\nexport type InferCollectionData<T extends string> = T extends keyof EmDashCollections\n\t? EmDashCollections[T]\n\t: Record<string, unknown>;\n\n/**\n * Sort direction\n */\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Order by specification - field name to direction\n * @example { created_at: \"desc\" } - Sort by created_at descending\n * @example { title: \"asc\" } - Sort by title ascending\n * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n */\nexport type OrderBySpec = Record<string, SortDirection>;\n\nexport interface CollectionFilter {\n\tstatus?: \"draft\" | \"published\" | \"archived\";\n\tlimit?: number;\n\t/**\n\t * Opaque cursor for keyset pagination.\n\t * Pass the `nextCursor` value from a previous result to fetch the next page.\n\t * @example\n\t * ```ts\n\t * const cursor = Astro.url.searchParams.get(\"cursor\") ?? undefined;\n\t * const { entries, nextCursor } = await getEmDashCollection(\"posts\", {\n\t * limit: 10,\n\t * cursor,\n\t * });\n\t * ```\n\t */\n\tcursor?: string;\n\t/**\n\t * Filter by field values or taxonomy terms\n\t * @example { category: 'news' } - Filter by taxonomy term\n\t * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)\n\t */\n\twhere?: Record<string, string | string[]>;\n\t/**\n\t * Order results by field(s)\n\t * @default { created_at: \"desc\" }\n\t * @example { created_at: \"desc\" } - Sort by created_at descending (default)\n\t * @example { title: \"asc\" } - Sort by title ascending\n\t * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n\t */\n\torderBy?: OrderBySpec;\n\t/**\n\t * Filter by locale. When set, only returns entries in this locale.\n\t * Only relevant when i18n is configured.\n\t * @example \"en\" — English entries only\n\t * @example \"fr\" — French entries only\n\t */\n\tlocale?: string;\n}\n\nexport interface ContentEntry<T = Record<string, unknown>> {\n\tid: string;\n\tdata: T;\n\t/** Visual editing annotations. Spread onto elements: {...entry.edit.title} */\n\tedit: EditProxy;\n}\n\n/** Cache hint returned by the content loader for route caching */\nexport interface CacheHint {\n\ttags?: string[];\n\tlastModified?: Date;\n}\n\n/**\n * Result from getEmDashCollection\n */\nexport interface CollectionResult<T> {\n\t/** The entries (empty array if error or none found) */\n\tentries: ContentEntry<T>[];\n\t/** Error if the query failed */\n\terror?: Error;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n\t/**\n\t * Opaque cursor for the next page.\n\t * Undefined when there are no more results.\n\t * Pass this as `cursor` in the next query to get the next page.\n\t */\n\tnextCursor?: string;\n}\n\n/**\n * Result from getEmDashEntry\n */\nexport interface EntryResult<T> {\n\t/** The entry, or null if not found */\n\tentry: ContentEntry<T> | null;\n\t/** Error if the query failed (not set for \"not found\", only for actual errors) */\n\terror?: Error;\n\t/** Whether we're in preview mode (valid token was provided) */\n\tisPreview: boolean;\n\t/** Set when a fallback locale was used instead of the requested locale */\n\tfallbackLocale?: string;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n}\n\nconst COLLECTION_NAME = \"_emdash\";\n\n/** Symbol key for edit metadata on PT arrays — avoids collision with user data */\nconst EMDASH_EDIT = Symbol.for(\"__emdash\");\n\n/** Edit metadata attached to PT arrays in edit mode */\nexport interface EditFieldMeta {\n\tcollection: string;\n\tid: string;\n\tfield: string;\n}\n\n/** Type guard for EditFieldMeta */\nfunction isEditFieldMeta(value: unknown): value is EditFieldMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"collection\" in value) || !(\"id\" in value) || !(\"field\" in value)) return false;\n\t// After `in` checks, TS narrows to Record<\"collection\" | \"id\" | \"field\", unknown>\n\tconst { collection, id, field } = value;\n\treturn typeof collection === \"string\" && typeof id === \"string\" && typeof field === \"string\";\n}\n\n/**\n * Read edit metadata from a value (returns undefined if not tagged).\n * Uses Object.getOwnPropertyDescriptor to access Symbol-keyed property\n * without an unsafe type assertion.\n */\nexport function getEditMeta(value: unknown): EditFieldMeta | undefined {\n\tif (value && typeof value === \"object\") {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, EMDASH_EDIT);\n\t\tconst meta: unknown = desc?.value;\n\t\tif (isEditFieldMeta(meta)) {\n\t\t\treturn meta;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Tag PT-like arrays in entry data with edit metadata (non-enumerable).\n * A PT array is identified by: is an array, first element has _type property.\n */\nfunction tagEditableFields(data: Record<string, unknown>, collection: string, id: string): void {\n\tfor (const [field, value] of Object.entries(data)) {\n\t\tif (\n\t\t\tArray.isArray(value) &&\n\t\t\tvalue.length > 0 &&\n\t\t\tvalue[0] &&\n\t\t\ttypeof value[0] === \"object\" &&\n\t\t\t\"_type\" in value[0]\n\t\t) {\n\t\t\tObject.defineProperty(value, EMDASH_EDIT, {\n\t\t\t\tvalue: { collection, id, field } satisfies EditFieldMeta,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/** Safely read a string field from a Record, with optional fallback */\nfunction dataStr(data: Record<string, unknown>, key: string, fallback = \"\"): string {\n\tconst val = data[key];\n\treturn typeof val === \"string\" ? val : fallback;\n}\n\n/** Type guard for Record<string, unknown> */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Extract data as Record from an Astro entry (which is any-typed) */\nfunction entryData(entry: { data?: unknown }): Record<string, unknown> {\n\treturn isRecord(entry.data) ? entry.data : {};\n}\n\n/** Extract the database ID from entry data (data.id is the ULID, entry.id is the slug) */\nfunction entryDatabaseId(entry: { id: string; data?: unknown }): string {\n\tconst d = entryData(entry);\n\treturn dataStr(d, \"id\") || entry.id;\n}\n\n/** Extract edit options from entry data for the proxy */\nfunction entryEditOptions(entry: { data?: unknown }): EditableOptions {\n\tconst data = entryData(entry);\n\tconst status = dataStr(data, \"status\", \"draft\");\n\tconst draftRevisionId = dataStr(data, \"draftRevisionId\") || undefined;\n\tconst liveRevisionId = dataStr(data, \"liveRevisionId\") || undefined;\n\tconst hasDraft = !!draftRevisionId && draftRevisionId !== liveRevisionId;\n\treturn { status, hasDraft };\n}\n\n/**\n * Get all entries of a content type\n *\n * Returns { entries, error } for graceful error handling.\n *\n * When emdash-env.d.ts is generated, the collection name will be\n * type-checked and the return type will be inferred automatically.\n *\n * @example\n * ```ts\n * import { getEmDashCollection } from \"emdash\";\n *\n * const { entries: posts, error } = await getEmDashCollection(\"posts\");\n * if (error) {\n * console.error(\"Failed to load posts:\", error);\n * return;\n * }\n * // posts[0].data.title is typed (if emdash-env.d.ts exists)\n *\n * // With filters\n * const { entries: drafts } = await getEmDashCollection(\"posts\", { status: \"draft\" });\n * ```\n */\nexport async function getEmDashCollection<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Cache per (type, filter) within a single request. Edit mode and\n\t// preview are request-scoped and stable, so they don't need to be\n\t// part of the key. Widgets and layouts frequently request the same\n\t// collection shape as the page itself (e.g. a \"recent posts\" list\n\t// appears on the home page AND in the sidebar) — caching collapses\n\t// those duplicate queries, along with the bylines and taxonomy-term\n\t// hydration each call would otherwise re-do.\n\treturn requestCached(collectionCacheKey(type, filter), () =>\n\t\tgetEmDashCollectionUncached<T, D>(type, filter),\n\t);\n}\n\n/**\n * Build a canonical cache key for `getEmDashCollection`.\n *\n * `JSON.stringify` is insertion-order-sensitive, so two callers passing\n * semantically identical filters with different key orders would miss\n * the cache. We fix the top-level field order and sort `where` keys\n * (order there is irrelevant), while preserving `orderBy` key order\n * because that's the sort priority.\n */\nfunction collectionCacheKey(type: string, filter?: CollectionFilter): string {\n\tif (!filter) return `collection:${type}:`;\n\tconst parts = [\n\t\tfilter.status ?? \"\",\n\t\tfilter.limit ?? \"\",\n\t\tfilter.cursor ?? \"\",\n\t\tfilter.where ? stableStringify(filter.where) : \"\",\n\t\tfilter.orderBy ? JSON.stringify(filter.orderBy) : \"\",\n\t\tfilter.locale ?? \"\",\n\t];\n\treturn `collection:${type}:${parts.join(\"|\")}`;\n}\n\nfunction stableStringify(value: Record<string, unknown>): string {\n\tconst keys = Object.keys(value).toSorted();\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const k of keys) ordered[k] = value[k];\n\treturn JSON.stringify(ordered);\n}\n\nasync function getEmDashCollectionUncached<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveCollection } = await import(\"astro:content\");\n\n\t// Resolve locale: explicit filter > ALS context > defaultLocale (when i18n enabled)\n\t// Without this, queries return all locale rows, producing broken IDs\n\tconst ctx = getRequestContext();\n\tconst i18nConfig = getI18nConfig();\n\tconst resolvedLocale =\n\t\tfilter?.locale ?? ctx?.locale ?? (isI18nEnabled() ? i18nConfig!.defaultLocale : undefined);\n\n\tconst result = await getLiveCollection(COLLECTION_NAME, {\n\t\ttype,\n\t\tstatus: filter?.status,\n\t\tlimit: filter?.limit,\n\t\tcursor: filter?.cursor,\n\t\twhere: filter?.where,\n\t\torderBy: filter?.orderBy,\n\t\tlocale: resolvedLocale,\n\t});\n\n\tconst { entries, error, cacheHint } = result;\n\t// nextCursor is returned by the emdash loader but not part of Astro's base\n\t// LiveLoader return type. Extract it safely via property descriptor to avoid\n\t// an unsafe type assertion on the `any`-typed result object.\n\tconst rawCursor = Object.getOwnPropertyDescriptor(result, \"nextCursor\")?.value;\n\tconst nextCursor: string | undefined = typeof rawCursor === \"string\" ? rawCursor : undefined;\n\n\tif (error) {\n\t\treturn { entries: [], error, cacheHint: {} };\n\t}\n\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst entriesWithEdit = entries.map((entry: ContentEntry<D>) => {\n\t\tconst dbId = entryDatabaseId(entry);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(entry), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...entry,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop(),\n\t\t};\n\t});\n\n\t// Eagerly hydrate bylines and taxonomy terms for all entries in parallel.\n\t// Both are independent queries, so running them concurrently halves the\n\t// round-trip cost on remote databases (D1 replicas, etc.).\n\tawait Promise.all([\n\t\thydrateEntryBylines(type, entriesWithEdit),\n\t\thydrateEntryTerms(type, entriesWithEdit),\n\t]);\n\n\treturn { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };\n}\n\n/**\n * Get a single entry by type and ID/slug\n *\n * Returns { entry, error, isPreview } for graceful error handling.\n * - entry is null if not found (not an error)\n * - error is set only for actual errors (db issues, etc.)\n *\n * Preview mode is detected automatically from request context (ALS).\n * When the URL has a valid `_preview` token, the middleware sets preview\n * context and this function serves draft revision data if available.\n *\n * @example\n * ```ts\n * import { getEmDashEntry } from \"emdash\";\n *\n * // Simple usage — preview just works via middleware\n * const { entry: post, isPreview, error } = await getEmDashEntry(\"posts\", \"my-slug\");\n * if (!post) return Astro.redirect(\"/404\");\n * ```\n */\nexport async function getEmDashEntry<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tid: string,\n\toptions?: { locale?: string },\n): Promise<EntryResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveEntry } = await import(\"astro:content\");\n\n\t// Check ALS for preview and edit mode context\n\tconst ctx = getRequestContext();\n\tconst preview = ctx?.preview;\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst isPreviewMode = !!preview && preview.collection === type;\n\t// Edit mode implies preview — editors should see draft content\n\tconst serveDrafts = isPreviewMode || isEditMode;\n\n\t// Resolve locale: explicit option > ALS context > undefined (no filter)\n\tconst requestedLocale = options?.locale ?? ctx?.locale;\n\n\t/** Wrap a raw Astro entry with edit proxy, tagging editable fields if needed */\n\tfunction wrapEntry(raw: ContentEntry<D>): ContentEntry<D> {\n\t\tconst dbId = entryDatabaseId(raw);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(raw), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...raw,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(raw)) : createNoop(),\n\t\t};\n\t}\n\n\t/** Check if an entry is publicly visible (published or scheduled past its time) */\n\tfunction isVisible(entry: ContentEntry<D>): boolean {\n\t\tconst data = entryData(entry);\n\t\tconst status = dataStr(data, \"status\");\n\t\tconst scheduledAt = dataStr(data, \"scheduledAt\") || undefined;\n\t\tconst isPublished = status === \"published\";\n\t\tconst isScheduledAndReady =\n\t\t\tstatus === \"scheduled\" && scheduledAt && new Date(scheduledAt) <= new Date();\n\t\treturn isPublished || !!isScheduledAndReady;\n\t}\n\n\t// Build the fallback chain: [requestedLocale, fallback1, ..., defaultLocale]\n\t// When i18n is disabled or no locale requested, just use a single-element chain\n\tconst localeChain =\n\t\trequestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];\n\n\t/** Return a successful EntryResult with bylines and taxonomy terms hydrated */\n\tasync function successResult(\n\t\twrapped: ContentEntry<D>,\n\t\topts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },\n\t): Promise<EntryResult<D>> {\n\t\tawait Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);\n\t\treturn {\n\t\t\tentry: wrapped,\n\t\t\tisPreview: opts.isPreview,\n\t\t\tfallbackLocale: opts.fallbackLocale,\n\t\t\tcacheHint: opts.cacheHint,\n\t\t};\n\t}\n\n\tif (serveDrafts) {\n\t\t// Draft mode: try each locale in the fallback chain\n\t\tfor (let i = 0; i < localeChain.length; i++) {\n\t\t\tconst locale = localeChain[i];\n\t\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\t\tconst {\n\t\t\t\tentry: baseEntry,\n\t\t\t\terror: baseError,\n\t\t\t\tcacheHint,\n\t\t\t} = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\ttype,\n\t\t\t\tid,\n\t\t\t\tlocale,\n\t\t\t});\n\n\t\t\tif (baseError) {\n\t\t\t\treturn { entry: null, error: baseError, isPreview: serveDrafts, cacheHint: {} };\n\t\t\t}\n\n\t\t\tif (!baseEntry) continue; // Try next locale in chain\n\n\t\t\t// Preview tokens are item-scoped: verify the resolved entry matches.\n\t\t\t// Edit mode (authenticated editors) has collection-wide draft access.\n\t\t\tif (isPreviewMode && !isEditMode) {\n\t\t\t\tconst dbId = entryDatabaseId(baseEntry);\n\t\t\t\tif (preview!.id !== dbId && preview!.id !== id) {\n\t\t\t\t\t// Token doesn't match — serve only if publicly visible, without draft access\n\t\t\t\t\tif (isVisible(baseEntry)) {\n\t\t\t\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\t\t\t\tisPreview: false,\n\t\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Not visible — try next locale in fallback chain\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if entry has a draft revision — if so, re-fetch with revision data\n\t\t\tconst baseData = entryData(baseEntry);\n\t\t\tconst draftRevisionId = dataStr(baseData, \"draftRevisionId\") || undefined;\n\n\t\t\tif (draftRevisionId) {\n\t\t\t\tconst { entry: draftEntry, error: draftError } = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\t\ttype,\n\t\t\t\t\tid,\n\t\t\t\t\trevisionId: draftRevisionId,\n\t\t\t\t\tlocale,\n\t\t\t\t});\n\n\t\t\t\tif (!draftError && draftEntry) {\n\t\t\t\t\treturn successResult(wrapEntry(draftEntry), {\n\t\t\t\t\t\tisPreview: serveDrafts,\n\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\tisPreview: serveDrafts,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\n\t\t// No entry found in any locale\n\t\treturn { entry: null, isPreview: serveDrafts, cacheHint: {} };\n\t}\n\n\t// Normal mode: try each locale in the fallback chain, only return published content\n\tfor (let i = 0; i < localeChain.length; i++) {\n\t\tconst locale = localeChain[i];\n\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\tconst { entry, error, cacheHint } = await getLiveEntry(COLLECTION_NAME, { type, id, locale });\n\t\tif (error) {\n\t\t\treturn { entry: null, error, isPreview: false, cacheHint: {} };\n\t\t}\n\n\t\tif (entry && isVisible(entry)) {\n\t\t\treturn successResult(wrapEntry(entry), {\n\t\t\t\tisPreview: false,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\t\t// Entry not found or not visible in this locale — try next\n\t}\n\n\treturn { entry: null, isPreview: false, cacheHint: {} };\n}\n\n/**\n * Eagerly hydrate byline data onto entry.data for one or more entries.\n *\n * Attaches `bylines` (array of ContentBylineCredit) and `byline`\n * (primary BylineSummary or null) to each entry's data object.\n * Uses batch queries to avoid N+1.\n *\n * Fails silently if the byline tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getBylinesForEntries } = await import(\"./bylines/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst bylinesMap = await getBylinesForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tconst credits = bylinesMap.get(dbId) ?? [];\n\t\t\tdata.bylines = credits;\n\t\t\tdata.byline = credits[0]?.byline ?? null;\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate bylines:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.\n *\n * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm\n * values) to each entry's data object. Uses a single batched JOIN query across\n * all taxonomies so the cost is O(1) regardless of the number of entries or\n * taxonomies on the site.\n *\n * This eliminates the common N+1 pattern where templates loop over list\n * results and call getEntryTerms() per entry. With hydration, the list page\n * stays at a single round-trip for term data.\n *\n * Fails silently if the taxonomy tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getAllTermsForEntries } = await import(\"./taxonomies/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst termsMap = await getAllTermsForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tdata.terms = termsMap.get(dbId) ?? {};\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate terms:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Translation summary for a single locale variant\n */\nexport interface TranslationSummary {\n\t/** Content item ID */\n\tid: string;\n\t/** Locale code (e.g. \"en\", \"fr\") */\n\tlocale: string;\n\t/** URL slug */\n\tslug: string | null;\n\t/** Current status */\n\tstatus: string;\n}\n\n/**\n * Result from getTranslations\n */\nexport interface TranslationsResult {\n\t/** The translation group ID (shared across locales) */\n\ttranslationGroup: string;\n\t/** All locale variants in this group */\n\ttranslations: TranslationSummary[];\n\t/** Error if the query failed */\n\terror?: Error;\n}\n\n/**\n * Get all translations of a content item.\n *\n * Given a content entry, returns all locale variants that share the same\n * translation group. This is useful for building language switcher UI.\n *\n * @example\n * ```ts\n * import { getEmDashEntry, getTranslations } from \"emdash\";\n *\n * const { entry: post } = await getEmDashEntry(\"posts\", \"hello-world\", { locale: \"en\" });\n * const { translations } = await getTranslations(\"posts\", post.data.id);\n * // translations = [{ id: \"...\", locale: \"en\", slug: \"hello-world\", status: \"published\" }, ...]\n * ```\n */\nexport async function getTranslations(type: string, id: string): Promise<TranslationsResult> {\n\ttry {\n\t\tconst db = (await import(\"./loader.js\")).getDb;\n\t\tconst dbInstance = await db();\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(dbInstance);\n\n\t\t// Find the item to get its translation group\n\t\tconst item = await repo.findByIdOrSlug(type, id);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\ttranslationGroup: \"\",\n\t\t\t\ttranslations: [],\n\t\t\t\terror: new Error(`Content item not found: ${id}`),\n\t\t\t};\n\t\t}\n\n\t\tconst group = item.translationGroup || item.id;\n\t\tconst translations = await repo.findTranslations(type, group);\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\tid: t.id,\n\t\t\t\tlocale: t.locale || \"en\",\n\t\t\t\tslug: t.slug,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\ttranslationGroup: \"\",\n\t\t\ttranslations: [],\n\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t};\n\t}\n}\n\n/**\n * Result from resolveEmDashPath\n */\nexport interface ResolvePathResult<T = Record<string, unknown>> {\n\t/** The matched entry */\n\tentry: ContentEntry<T>;\n\t/** The collection slug that matched */\n\tcollection: string;\n\t/** Extracted parameters from the URL pattern (e.g. { slug: \"my-post\" }) */\n\tparams: Record<string, string>;\n}\n\n/** Matches `{paramName}` placeholders in URL patterns */\nconst URL_PARAM_PATTERN = /\\{(\\w+)\\}/g;\n\n/** Convert a URL pattern like \"/blog/{slug}\" to a regex and param name list */\nfunction patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {\n\tconst paramNames: string[] = [];\n\tconst regexStr = pattern.replace(URL_PARAM_PATTERN, (_match, name: string) => {\n\t\tparamNames.push(name);\n\t\treturn \"([^/]+)\";\n\t});\n\treturn { regex: new RegExp(`^${regexStr}$`), paramNames };\n}\n\n/** Cached compiled URL patterns for resolveEmDashPath */\ninterface CachedPattern {\n\tslug: string;\n\tregex: RegExp;\n\tparamNames: string[];\n}\nlet cachedUrlPatterns: CachedPattern[] | null = null;\n\n/**\n * Invalidate the cached URL patterns used by resolveEmDashPath.\n * Call when collection URL patterns change (schema updates).\n */\nexport function invalidateUrlPatternCache(): void {\n\tcachedUrlPatterns = null;\n}\n\n/**\n * Resolve a URL path to a content entry by matching against collection URL patterns.\n *\n * Loads all collections with a `urlPattern` set, converts each pattern to a regex,\n * and tests the given path. On match, extracts the slug and fetches the entry.\n *\n * @example\n * ```ts\n * import { resolveEmDashPath } from \"emdash\";\n *\n * // Given pages with urlPattern \"/{slug}\" and posts with \"/blog/{slug}\":\n * const result = await resolveEmDashPath(\"/blog/hello-world\");\n * if (result) {\n * console.log(result.collection); // \"posts\"\n * console.log(result.params.slug); // \"hello-world\"\n * console.log(result.entry.data); // post data\n * }\n * ```\n */\nexport async function resolveEmDashPath<T = Record<string, unknown>>(\n\tpath: string,\n): Promise<ResolvePathResult<T> | null> {\n\t// Build and cache compiled patterns on first call\n\tif (!cachedUrlPatterns) {\n\t\tconst { getDb } = await import(\"./loader.js\");\n\t\tconst { SchemaRegistry } = await import(\"./schema/registry.js\");\n\t\tconst db = await getDb();\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collections = await registry.listCollections();\n\n\t\tcachedUrlPatterns = [];\n\t\tfor (const collection of collections) {\n\t\t\tif (!collection.urlPattern) continue;\n\t\t\tconst { regex, paramNames } = patternToRegex(collection.urlPattern);\n\t\t\tcachedUrlPatterns.push({ slug: collection.slug, regex, paramNames });\n\t\t}\n\t}\n\n\tfor (const pattern of cachedUrlPatterns) {\n\t\tconst match = path.match(pattern.regex);\n\t\tif (!match) continue;\n\n\t\t// Extract params\n\t\tconst params: Record<string, string> = {};\n\t\tfor (let i = 0; i < pattern.paramNames.length; i++) {\n\t\t\tparams[pattern.paramNames[i]] = match[i + 1];\n\t\t}\n\n\t\t// Look up entry by slug (most common pattern)\n\t\tconst slug = params.slug;\n\t\tif (!slug) continue;\n\n\t\tconst { entry } = await getEmDashEntry<string, T>(pattern.slug, slug);\n\t\tif (entry) {\n\t\t\treturn { entry, collection: pattern.slug, params };\n\t\t}\n\t}\n\n\treturn null;\n}\n"],"mappings":"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"}