@shopify/hydrogen 2026.10.0-preview.4 → 2026.10.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 (437) hide show
  1. package/dist/cli/index.mjs +23 -12
  2. package/dist/client/client.d.mts +18 -15
  3. package/dist/client/client.mjs +8 -3
  4. package/dist/client/client.mjs.map +1 -1
  5. package/dist/client/errors.d.mts +35 -13
  6. package/dist/client/errors.mjs +23 -13
  7. package/dist/client/errors.mjs.map +1 -1
  8. package/dist/client/types.d.mts +140 -90
  9. package/dist/core/analytics/cart-tracker.d.mts +19 -10
  10. package/dist/core/analytics/cart-tracker.mjs +18 -10
  11. package/dist/core/analytics/cart-tracker.mjs.map +1 -1
  12. package/dist/core/analytics/events.d.mts +17 -11
  13. package/dist/core/analytics/events.mjs +13 -4
  14. package/dist/core/analytics/events.mjs.map +1 -1
  15. package/dist/core/analytics/types.d.mts +207 -118
  16. package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
  17. package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
  18. package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
  19. package/dist/core/cache/fetch-with-cache.d.mts +24 -4
  20. package/dist/core/cache/fetch-with-cache.mjs +17 -2
  21. package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
  22. package/dist/core/cache/key.d.mts +2 -0
  23. package/dist/core/cache/key.mjs.map +1 -1
  24. package/dist/core/cache/run-with-cache.d.mts +56 -8
  25. package/dist/core/cache/run-with-cache.mjs +17 -6
  26. package/dist/core/cache/run-with-cache.mjs.map +1 -1
  27. package/dist/core/cache/store.d.mts +8 -6
  28. package/dist/core/cache/store.mjs.map +1 -1
  29. package/dist/core/cache/strategies.d.mts +27 -2
  30. package/dist/core/cache/strategies.mjs +6 -0
  31. package/dist/core/cache/strategies.mjs.map +1 -1
  32. package/dist/core/cart/actions.d.mts +37 -30
  33. package/dist/core/cart/actions.mjs +21 -9
  34. package/dist/core/cart/actions.mjs.map +1 -1
  35. package/dist/core/cart/attach-quantity-input.d.mts +8 -7
  36. package/dist/core/cart/attach-quantity-input.mjs +8 -7
  37. package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
  38. package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
  39. package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
  40. package/dist/core/cart/cart.d.mts +59 -46
  41. package/dist/core/cart/cart.mjs +46 -29
  42. package/dist/core/cart/cart.mjs.map +1 -1
  43. package/dist/core/cart/cookie.d.mts +8 -4
  44. package/dist/core/cart/cookie.mjs +8 -4
  45. package/dist/core/cart/cookie.mjs.map +1 -1
  46. package/dist/core/cart/form.d.mts +34 -20
  47. package/dist/core/cart/form.mjs +6 -4
  48. package/dist/core/cart/form.mjs.map +1 -1
  49. package/dist/core/cart/get-cart.d.mts +27 -15
  50. package/dist/core/cart/get-cart.mjs +13 -8
  51. package/dist/core/cart/get-cart.mjs.map +1 -1
  52. package/dist/core/cart/quantity.d.mts +16 -5
  53. package/dist/core/cart/quantity.mjs +16 -5
  54. package/dist/core/cart/quantity.mjs.map +1 -1
  55. package/dist/core/cart/queries.d.mts +25 -19
  56. package/dist/core/cart/queries.mjs +15 -1
  57. package/dist/core/cart/queries.mjs.map +1 -1
  58. package/dist/core/cart/server-handlers.d.mts +82 -45
  59. package/dist/core/cart/server-handlers.mjs +4 -0
  60. package/dist/core/cart/server-handlers.mjs.map +1 -1
  61. package/dist/core/cart/state.d.mts +110 -64
  62. package/dist/core/cart/state.mjs +24 -6
  63. package/dist/core/cart/state.mjs.map +1 -1
  64. package/dist/core/collection/collection.d.mts +52 -53
  65. package/dist/core/collection/collection.mjs +5 -3
  66. package/dist/core/collection/collection.mjs.map +1 -1
  67. package/dist/core/collection/reconciler.d.mts +23 -16
  68. package/dist/core/collection/reconciler.mjs +8 -4
  69. package/dist/core/collection/reconciler.mjs.map +1 -1
  70. package/dist/core/collection/state.d.mts +31 -26
  71. package/dist/core/collection/state.mjs +4 -3
  72. package/dist/core/collection/state.mjs.map +1 -1
  73. package/dist/core/collection/url.d.mts +70 -29
  74. package/dist/core/collection/url.mjs +63 -25
  75. package/dist/core/collection/url.mjs.map +1 -1
  76. package/dist/core/constants.d.mts +1 -0
  77. package/dist/core/constants.mjs +1 -0
  78. package/dist/core/constants.mjs.map +1 -1
  79. package/dist/core/logging/logging.d.mts +14 -8
  80. package/dist/core/logging/logging.mjs +15 -10
  81. package/dist/core/logging/logging.mjs.map +1 -1
  82. package/dist/core/logging/types.d.mts +28 -11
  83. package/dist/core/logging/types.mjs.map +1 -1
  84. package/dist/core/money/format.d.mts +6 -4
  85. package/dist/core/money/format.mjs +19 -9
  86. package/dist/core/money/format.mjs.map +1 -1
  87. package/dist/core/money/types.d.mts +41 -25
  88. package/dist/core/predictive-search/form.d.mts +37 -7
  89. package/dist/core/predictive-search/form.mjs +21 -3
  90. package/dist/core/predictive-search/form.mjs.map +1 -1
  91. package/dist/core/predictive-search/queries.d.mts +18 -30
  92. package/dist/core/predictive-search/queries.mjs +7 -3
  93. package/dist/core/predictive-search/queries.mjs.map +1 -1
  94. package/dist/core/predictive-search/search.d.mts +25 -27
  95. package/dist/core/predictive-search/search.mjs +20 -19
  96. package/dist/core/predictive-search/search.mjs.map +1 -1
  97. package/dist/core/predictive-search/server-handlers.d.mts +25 -26
  98. package/dist/core/predictive-search/server-handlers.mjs +4 -0
  99. package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
  100. package/dist/core/predictive-search/store.d.mts +65 -24
  101. package/dist/core/predictive-search/store.mjs +12 -1
  102. package/dist/core/predictive-search/store.mjs.map +1 -1
  103. package/dist/core/predictive-search/url.d.mts +36 -37
  104. package/dist/core/predictive-search/url.mjs +14 -6
  105. package/dist/core/predictive-search/url.mjs.map +1 -1
  106. package/dist/core/product/form.d.mts +63 -16
  107. package/dist/core/product/form.mjs +8 -2
  108. package/dist/core/product/form.mjs.map +1 -1
  109. package/dist/core/product/options.d.mts +8 -10
  110. package/dist/core/product/options.mjs +8 -10
  111. package/dist/core/product/options.mjs.map +1 -1
  112. package/dist/core/product/product-form.d.mts +77 -29
  113. package/dist/core/product/product-form.mjs +32 -10
  114. package/dist/core/product/product-form.mjs.map +1 -1
  115. package/dist/core/product/state.d.mts +55 -32
  116. package/dist/core/product/url.d.mts +19 -13
  117. package/dist/core/product/url.mjs +10 -10
  118. package/dist/core/product/url.mjs.map +1 -1
  119. package/dist/core/request-context.d.mts +77 -19
  120. package/dist/core/request-context.mjs +5 -1
  121. package/dist/core/request-context.mjs.map +1 -1
  122. package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
  123. package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
  124. package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
  125. package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
  126. package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
  127. package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  128. package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
  129. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  130. package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
  131. package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
  132. package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
  133. package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
  134. package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
  135. package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
  136. package/dist/core/request-routing/registered-routes.d.mts +12 -0
  137. package/dist/core/request-routing/registered-routes.mjs +12 -0
  138. package/dist/core/request-routing/registered-routes.mjs.map +1 -1
  139. package/dist/core/request-routing/route-types.d.mts +43 -18
  140. package/dist/core/shop-pay/shop-pay.d.mts +71 -35
  141. package/dist/core/shop-pay/shop-pay.mjs +36 -10
  142. package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
  143. package/dist/core/shopify-scripts/index.d.mts +15 -5
  144. package/dist/core/shopify-scripts/index.mjs +15 -5
  145. package/dist/core/shopify-scripts/index.mjs.map +1 -1
  146. package/dist/core/shopify-scripts/initialize.d.mts +12 -4
  147. package/dist/core/shopify-scripts/initialize.mjs +12 -4
  148. package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
  149. package/dist/core/shopify-scripts/types.d.mts +58 -25
  150. package/dist/core/standard-routes/build.d.mts +11 -12
  151. package/dist/core/standard-routes/build.mjs +11 -12
  152. package/dist/core/standard-routes/build.mjs.map +1 -1
  153. package/dist/core/standard-routes/types.d.mts +30 -24
  154. package/dist/core/url.mjs +2 -1
  155. package/dist/core/url.mjs.map +1 -1
  156. package/dist/customer-account/client.d.mts +36 -69
  157. package/dist/customer-account/client.mjs +7 -8
  158. package/dist/customer-account/client.mjs.map +1 -1
  159. package/dist/customer-account/errors.d.mts +45 -36
  160. package/dist/customer-account/errors.mjs +21 -36
  161. package/dist/customer-account/errors.mjs.map +1 -1
  162. package/dist/customer-account/graphql.d.mts +11 -23
  163. package/dist/customer-account/graphql.mjs +3 -9
  164. package/dist/customer-account/graphql.mjs.map +1 -1
  165. package/dist/customer-account/session.d.mts +130 -100
  166. package/dist/customer-account/session.mjs +32 -10
  167. package/dist/customer-account/session.mjs.map +1 -1
  168. package/dist/customer-account/type-resolver.d.mts +7 -8
  169. package/dist/development/client/client.d.mts +18 -15
  170. package/dist/development/client/client.mjs +8 -3
  171. package/dist/development/client/client.mjs.map +1 -1
  172. package/dist/development/client/errors.d.mts +35 -13
  173. package/dist/development/client/errors.mjs +23 -13
  174. package/dist/development/client/errors.mjs.map +1 -1
  175. package/dist/development/client/types.d.mts +140 -90
  176. package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
  177. package/dist/development/core/analytics/cart-tracker.mjs +18 -10
  178. package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
  179. package/dist/development/core/analytics/events.d.mts +17 -11
  180. package/dist/development/core/analytics/events.mjs +13 -4
  181. package/dist/development/core/analytics/events.mjs.map +1 -1
  182. package/dist/development/core/analytics/types.d.mts +207 -118
  183. package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
  184. package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
  185. package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
  186. package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
  187. package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
  188. package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
  189. package/dist/development/core/cache/key.d.mts +2 -0
  190. package/dist/development/core/cache/key.mjs.map +1 -1
  191. package/dist/development/core/cache/run-with-cache.d.mts +56 -8
  192. package/dist/development/core/cache/run-with-cache.mjs +17 -6
  193. package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
  194. package/dist/development/core/cache/store.d.mts +8 -6
  195. package/dist/development/core/cache/store.mjs.map +1 -1
  196. package/dist/development/core/cache/strategies.d.mts +27 -2
  197. package/dist/development/core/cache/strategies.mjs +6 -0
  198. package/dist/development/core/cache/strategies.mjs.map +1 -1
  199. package/dist/development/core/cart/actions.d.mts +37 -30
  200. package/dist/development/core/cart/actions.mjs +21 -9
  201. package/dist/development/core/cart/actions.mjs.map +1 -1
  202. package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
  203. package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
  204. package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
  205. package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
  206. package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
  207. package/dist/development/core/cart/cart.d.mts +59 -46
  208. package/dist/development/core/cart/cart.mjs +46 -29
  209. package/dist/development/core/cart/cart.mjs.map +1 -1
  210. package/dist/development/core/cart/cookie.d.mts +8 -4
  211. package/dist/development/core/cart/cookie.mjs +8 -4
  212. package/dist/development/core/cart/cookie.mjs.map +1 -1
  213. package/dist/development/core/cart/form.d.mts +34 -20
  214. package/dist/development/core/cart/form.mjs +6 -4
  215. package/dist/development/core/cart/form.mjs.map +1 -1
  216. package/dist/development/core/cart/get-cart.d.mts +27 -15
  217. package/dist/development/core/cart/get-cart.mjs +13 -8
  218. package/dist/development/core/cart/get-cart.mjs.map +1 -1
  219. package/dist/development/core/cart/quantity.d.mts +16 -5
  220. package/dist/development/core/cart/quantity.mjs +16 -5
  221. package/dist/development/core/cart/quantity.mjs.map +1 -1
  222. package/dist/development/core/cart/queries.d.mts +25 -19
  223. package/dist/development/core/cart/queries.mjs +15 -1
  224. package/dist/development/core/cart/queries.mjs.map +1 -1
  225. package/dist/development/core/cart/server-handlers.d.mts +82 -45
  226. package/dist/development/core/cart/server-handlers.mjs +4 -0
  227. package/dist/development/core/cart/server-handlers.mjs.map +1 -1
  228. package/dist/development/core/cart/state.d.mts +110 -64
  229. package/dist/development/core/cart/state.mjs +24 -6
  230. package/dist/development/core/cart/state.mjs.map +1 -1
  231. package/dist/development/core/collection/collection.d.mts +52 -53
  232. package/dist/development/core/collection/collection.mjs +5 -3
  233. package/dist/development/core/collection/collection.mjs.map +1 -1
  234. package/dist/development/core/collection/reconciler.d.mts +23 -16
  235. package/dist/development/core/collection/reconciler.mjs +8 -4
  236. package/dist/development/core/collection/reconciler.mjs.map +1 -1
  237. package/dist/development/core/collection/state.d.mts +31 -26
  238. package/dist/development/core/collection/state.mjs +4 -3
  239. package/dist/development/core/collection/state.mjs.map +1 -1
  240. package/dist/development/core/collection/url.d.mts +70 -29
  241. package/dist/development/core/collection/url.mjs +63 -25
  242. package/dist/development/core/collection/url.mjs.map +1 -1
  243. package/dist/development/core/constants.mjs.map +1 -1
  244. package/dist/development/core/logging/logging.d.mts +14 -8
  245. package/dist/development/core/logging/logging.mjs +15 -10
  246. package/dist/development/core/logging/logging.mjs.map +1 -1
  247. package/dist/development/core/logging/types.d.mts +28 -11
  248. package/dist/development/core/logging/types.mjs.map +1 -1
  249. package/dist/development/core/money/format.d.mts +6 -4
  250. package/dist/development/core/money/format.mjs +19 -9
  251. package/dist/development/core/money/format.mjs.map +1 -1
  252. package/dist/development/core/money/types.d.mts +41 -25
  253. package/dist/development/core/predictive-search/form.d.mts +37 -7
  254. package/dist/development/core/predictive-search/form.mjs +21 -3
  255. package/dist/development/core/predictive-search/form.mjs.map +1 -1
  256. package/dist/development/core/predictive-search/queries.d.mts +18 -30
  257. package/dist/development/core/predictive-search/queries.mjs +7 -3
  258. package/dist/development/core/predictive-search/queries.mjs.map +1 -1
  259. package/dist/development/core/predictive-search/search.d.mts +25 -27
  260. package/dist/development/core/predictive-search/search.mjs +20 -19
  261. package/dist/development/core/predictive-search/search.mjs.map +1 -1
  262. package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
  263. package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
  264. package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
  265. package/dist/development/core/predictive-search/store.d.mts +65 -24
  266. package/dist/development/core/predictive-search/store.mjs +12 -1
  267. package/dist/development/core/predictive-search/store.mjs.map +1 -1
  268. package/dist/development/core/predictive-search/url.d.mts +36 -37
  269. package/dist/development/core/predictive-search/url.mjs +14 -6
  270. package/dist/development/core/predictive-search/url.mjs.map +1 -1
  271. package/dist/development/core/product/form.d.mts +63 -16
  272. package/dist/development/core/product/form.mjs +8 -2
  273. package/dist/development/core/product/form.mjs.map +1 -1
  274. package/dist/development/core/product/options.d.mts +8 -10
  275. package/dist/development/core/product/options.mjs +8 -10
  276. package/dist/development/core/product/options.mjs.map +1 -1
  277. package/dist/development/core/product/product-form.d.mts +77 -29
  278. package/dist/development/core/product/product-form.mjs +32 -10
  279. package/dist/development/core/product/product-form.mjs.map +1 -1
  280. package/dist/development/core/product/state.d.mts +55 -32
  281. package/dist/development/core/product/url.d.mts +19 -13
  282. package/dist/development/core/product/url.mjs +10 -10
  283. package/dist/development/core/product/url.mjs.map +1 -1
  284. package/dist/development/core/request-context.d.mts +77 -19
  285. package/dist/development/core/request-context.mjs +5 -1
  286. package/dist/development/core/request-context.mjs.map +1 -1
  287. package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
  288. package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
  289. package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
  290. package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
  291. package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
  292. package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
  293. package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
  294. package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  295. package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
  296. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  297. package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
  298. package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
  299. package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
  300. package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
  301. package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
  302. package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
  303. package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
  304. package/dist/development/core/request-routing/registered-routes.mjs +12 -0
  305. package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
  306. package/dist/development/core/request-routing/route-types.d.mts +43 -18
  307. package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
  308. package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
  309. package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
  310. package/dist/development/core/shopify-scripts/index.d.mts +15 -5
  311. package/dist/development/core/shopify-scripts/index.mjs +15 -5
  312. package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
  313. package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
  314. package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
  315. package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
  316. package/dist/development/core/shopify-scripts/types.d.mts +58 -25
  317. package/dist/development/core/standard-routes/build.d.mts +11 -12
  318. package/dist/development/core/standard-routes/build.mjs +11 -12
  319. package/dist/development/core/standard-routes/build.mjs.map +1 -1
  320. package/dist/development/core/standard-routes/types.d.mts +30 -24
  321. package/dist/development/core/url.mjs +2 -1
  322. package/dist/development/core/url.mjs.map +1 -1
  323. package/dist/development/customer-account/session.d.mts +45 -54
  324. package/dist/development/customer-account/session.mjs +20 -4
  325. package/dist/development/customer-account/session.mjs.map +1 -1
  326. package/dist/development/globals.d.mts +20 -11
  327. package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
  328. package/dist/development/graphql/graphql.d.mts +17 -16
  329. package/dist/development/graphql/graphql.mjs +7 -8
  330. package/dist/development/graphql/graphql.mjs.map +1 -1
  331. package/dist/development/graphql/type-resolver.d.mts +15 -8
  332. package/dist/development/react/cart.d.mts +78 -32
  333. package/dist/development/react/cart.mjs +56 -29
  334. package/dist/development/react/cart.mjs.map +1 -1
  335. package/dist/development/react/collection.d.mts +40 -22
  336. package/dist/development/react/collection.mjs +27 -7
  337. package/dist/development/react/collection.mjs.map +1 -1
  338. package/dist/development/react/predictive-search.d.mts +65 -50
  339. package/dist/development/react/predictive-search.mjs +23 -21
  340. package/dist/development/react/predictive-search.mjs.map +1 -1
  341. package/dist/development/react/product.d.mts +45 -16
  342. package/dist/development/react/product.mjs +13 -11
  343. package/dist/development/react/product.mjs.map +1 -1
  344. package/dist/development/react/shop-pay.d.mts +15 -0
  345. package/dist/development/react/shop-pay.mjs +9 -0
  346. package/dist/development/react/shop-pay.mjs.map +1 -1
  347. package/dist/development/react/shopify-scripts.d.mts +23 -2
  348. package/dist/development/react/shopify-scripts.mjs +9 -0
  349. package/dist/development/react/shopify-scripts.mjs.map +1 -1
  350. package/dist/development/vue/cart.d.mts +28 -23
  351. package/dist/development/vue/cart.mjs +42 -25
  352. package/dist/development/vue/cart.mjs.map +1 -1
  353. package/dist/development/vue/collection.d.mts +20 -12
  354. package/dist/development/vue/collection.mjs +15 -7
  355. package/dist/development/vue/collection.mjs.map +1 -1
  356. package/dist/development/vue/index.d.mts +2 -2
  357. package/dist/development/vue/index.mjs +2 -2
  358. package/dist/development/vue/predictive-search.d.mts +31 -36
  359. package/dist/development/vue/predictive-search.mjs +16 -22
  360. package/dist/development/vue/predictive-search.mjs.map +1 -1
  361. package/dist/development/vue/product.d.mts +16 -15
  362. package/dist/development/vue/product.mjs +11 -11
  363. package/dist/development/vue/product.mjs.map +1 -1
  364. package/dist/development/vue/shop-pay.d.mts +14 -17
  365. package/dist/development/vue/shop-pay.mjs +8 -0
  366. package/dist/development/vue/shop-pay.mjs.map +1 -1
  367. package/dist/development/vue/shopify-scripts.d.mts +11 -0
  368. package/dist/development/vue/shopify-scripts.mjs +6 -0
  369. package/dist/development/vue/shopify-scripts.mjs.map +1 -1
  370. package/dist/globals.d.mts +20 -11
  371. package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
  372. package/dist/graphql/graphql.d.mts +17 -16
  373. package/dist/graphql/graphql.mjs +7 -8
  374. package/dist/graphql/graphql.mjs.map +1 -1
  375. package/dist/graphql/type-resolver.d.mts +15 -8
  376. package/dist/react/cart.d.mts +78 -32
  377. package/dist/react/cart.mjs +56 -29
  378. package/dist/react/cart.mjs.map +1 -1
  379. package/dist/react/collection.d.mts +40 -22
  380. package/dist/react/collection.mjs +27 -7
  381. package/dist/react/collection.mjs.map +1 -1
  382. package/dist/react/predictive-search.d.mts +65 -50
  383. package/dist/react/predictive-search.mjs +23 -21
  384. package/dist/react/predictive-search.mjs.map +1 -1
  385. package/dist/react/product.d.mts +45 -16
  386. package/dist/react/product.mjs +13 -11
  387. package/dist/react/product.mjs.map +1 -1
  388. package/dist/react/shop-pay.d.mts +15 -0
  389. package/dist/react/shop-pay.mjs +9 -0
  390. package/dist/react/shop-pay.mjs.map +1 -1
  391. package/dist/react/shopify-scripts.d.mts +23 -2
  392. package/dist/react/shopify-scripts.mjs +9 -0
  393. package/dist/react/shopify-scripts.mjs.map +1 -1
  394. package/dist/ts-plugin/index.cjs +6 -0
  395. package/dist/ts-plugin/index.cjs.map +1 -1
  396. package/dist/ts-plugin/index.d.cts +6 -0
  397. package/dist/vite/local-https.d.mts +51 -17
  398. package/dist/vite/local-https.mjs +26 -5
  399. package/dist/vite/local-https.mjs.map +1 -1
  400. package/dist/vue/cart.d.mts +28 -23
  401. package/dist/vue/cart.mjs +42 -25
  402. package/dist/vue/cart.mjs.map +1 -1
  403. package/dist/vue/collection.d.mts +20 -12
  404. package/dist/vue/collection.mjs +15 -7
  405. package/dist/vue/collection.mjs.map +1 -1
  406. package/dist/vue/index.d.mts +2 -2
  407. package/dist/vue/index.mjs +2 -2
  408. package/dist/vue/predictive-search.d.mts +31 -36
  409. package/dist/vue/predictive-search.mjs +16 -22
  410. package/dist/vue/predictive-search.mjs.map +1 -1
  411. package/dist/vue/product.d.mts +16 -15
  412. package/dist/vue/product.mjs +11 -11
  413. package/dist/vue/product.mjs.map +1 -1
  414. package/dist/vue/shop-pay.d.mts +14 -17
  415. package/dist/vue/shop-pay.mjs +8 -0
  416. package/dist/vue/shop-pay.mjs.map +1 -1
  417. package/dist/vue/shopify-scripts.d.mts +11 -0
  418. package/dist/vue/shopify-scripts.mjs +6 -0
  419. package/dist/vue/shopify-scripts.mjs.map +1 -1
  420. package/package.json +3 -1
  421. package/skills/hydrogen-analytics/SKILL.md +1 -1
  422. package/skills/hydrogen-cart-ui/SKILL.md +28 -2
  423. package/skills/hydrogen-collection-browser/SKILL.md +9 -1
  424. package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
  425. package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
  426. package/skills/hydrogen-collection-browser/references/react.md +151 -32
  427. package/skills/hydrogen-customer-account/SKILL.md +2 -0
  428. package/skills/hydrogen-request-handlers/SKILL.md +21 -11
  429. package/skills/hydrogen-setup/references/analytics.md +14 -8
  430. package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
  431. package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
  432. package/skills/hydrogen-storefront-client/SKILL.md +1 -1
  433. package/skills/hydrogen-storefront-client/references/caching.md +2 -0
  434. package/skills/hydrogen-variant-form/SKILL.md +25 -7
  435. package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
  436. package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
  437. package/skills/hydrogen-variant-form/references/react.md +45 -9
@@ -12,6 +12,7 @@ Use the React binding:
12
12
 
13
13
  ```tsx
14
14
  import {
15
+ formatMoney,
15
16
  getFilterRemovalUrl,
16
17
  getSortByValue,
17
18
  isFilterInputActive,
@@ -19,10 +20,12 @@ import {
19
20
  serializeCollectionParams,
20
21
  type AvailableFilter,
21
22
  type CollectionState,
23
+ type MoneyV2,
22
24
  type ProductFilter,
23
25
  } from "@shopify/hydrogen";
24
26
  import { CollectionProvider, useCollection, useCollectionForm } from "@shopify/hydrogen/react";
25
27
  import type { ProductFilter as StorefrontApiProductFilter } from "@shopify/hydrogen/storefront-api-types";
28
+ import { useEffect, useRef } from "react";
26
29
  ```
27
30
 
28
31
  Use `getSortByValue(...)` for option values so the `sort_by` query param round-trips through `parseCollectionParams()`:
@@ -73,12 +76,20 @@ export async function loader({ context, params, request }: Route.LoaderArgs) {
73
76
  collection: data.collection,
74
77
  products: data.collection.products.nodes,
75
78
  availableFilters: data.collection.products.filters,
79
+ // Market-aware stores: `data.localization.country.currency.isoCode` under `@inContext`.
80
+ currencyCode: data.shop.paymentSettings.currencyCode,
76
81
  dataSearch: url.searchParams.toString(),
77
82
  };
78
83
  }
79
84
  ```
80
85
 
81
- Keep `dataSearch` exactly aligned with the query used for the server data.
86
+ Keep `dataSearch` exactly aligned with the query used for the server data. Load `currencyCode` in the same query so it matches the product prices.
87
+
88
+ For the single-currency loader above, include this root-level selection in `COLLECTION_QUERY`:
89
+
90
+ ```graphql
91
+ shop { paymentSettings { currencyCode } }
92
+ ```
82
93
 
83
94
  ## Provider
84
95
 
@@ -114,7 +125,7 @@ export default function CollectionRoute({ loaderData }: Route.ComponentProps) {
114
125
  Inside the browse UI:
115
126
 
116
127
  ```tsx
117
- function CollectionPage({ availableFilters, products }: Props) {
128
+ function CollectionPage({ availableFilters, currencyCode, products }: Props) {
118
129
  const state = useCollection();
119
130
  const { formProps } = useCollectionForm();
120
131
  const isLoading = state.status === "loading";
@@ -124,9 +135,9 @@ function CollectionPage({ availableFilters, products }: Props) {
124
135
  <FilterSidebar
125
136
  availableFilters={availableFilters}
126
137
  activeFilters={state.filters}
127
- disabled={isLoading}
138
+ currencyCode={currencyCode}
128
139
  />
129
- <select name="sort_by" defaultValue={currentSortValue(state)} onChange={requestFormSubmit}>
140
+ <select name="sort_by" value={currentSortValue(state)} onChange={requestFormSubmit}>
130
141
  {COLLECTION_SORT_OPTIONS.map((option) => (
131
142
  <option key={option.label} value={option.value}>
132
143
  {option.label}
@@ -145,7 +156,7 @@ function requestFormSubmit(event: React.ChangeEvent<HTMLInputElement | HTMLSelec
145
156
 
146
157
  Pass `method="get"` and an explicit `action={collectionPath}` (the current `/collections/:handle` or `/search` route URL) literally — `formProps()` only wires the submit handler (see the SKILL.md UI rule).
147
158
 
148
- Use uncontrolled form controls. When a route needs to remount them after external navigation, put `key={serializeCollectionParams({ filters: state.filters, sortKey: undefined, reverse: false }).toString()}` on the filter subtree (for search, include the term in the key) — keyed by serialized **filter state**, not the live URL. The URL clears before the `CollectionProvider` reconciler settles `state.filters`, so a URL-keyed remount bakes in stale defaults. This resets checkbox and price-input DOM state without coupling active filter chips to the form remount.
159
+ Keep every filter control mounted and enabled during submission and loading, and show loading on the product grid (`pending`). Keep the filter subtree key stable across filter updates, so focused controls stay mounted. Bind the sort `value` and checkbox `checked` to browse state so chips and back/forward navigation update them in place. Keep price inputs uncontrolled and sync external bounds through input refs, as below.
149
160
 
150
161
  ## Filters
151
162
 
@@ -154,13 +165,15 @@ Choose the control at the filter-group level. `PRICE_RANGE` uses min/max number
154
165
  ```tsx
155
166
  function FilterGroup({
156
167
  activeFilters,
168
+ currencyCode,
157
169
  filter,
158
170
  }: {
159
171
  activeFilters: ProductFilter[];
172
+ currencyCode: MoneyV2["currencyCode"];
160
173
  filter: AvailableFilter;
161
174
  }) {
162
175
  if (filter.type === "PRICE_RANGE") {
163
- return <PriceRangeInput activeFilters={activeFilters} />;
176
+ return <PriceRangeInput activeFilters={activeFilters} currencyCode={currencyCode} />;
164
177
  }
165
178
 
166
179
  return filter.values.map((value) => (
@@ -173,38 +186,130 @@ function FilterGroup({
173
186
  ));
174
187
  }
175
188
 
176
- function PriceRangeInput({ activeFilters }: { activeFilters: ProductFilter[] }) {
189
+ // Use the resolved market locale in market-aware stores.
190
+ const LOCALE = "en-US";
191
+
192
+ function PriceRangeInput({
193
+ activeFilters,
194
+ currencyCode,
195
+ }: {
196
+ activeFilters: ProductFilter[];
197
+ currencyCode: MoneyV2["currencyCode"];
198
+ }) {
199
+ // One timer for both inputs, so editing min then max submits once.
200
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
201
+ const minInput = useRef<HTMLInputElement>(null);
202
+ const maxInput = useRef<HTMLInputElement>(null);
177
203
  const price = activeFilters.find((filter) => filter.price)?.price;
204
+ const min = price?.min ?? "";
205
+ const max = price?.max ?? "";
206
+ const symbol = formatMoney(
207
+ { amount: "0", currencyCode },
208
+ { locale: LOCALE },
209
+ ).currencyNarrowSymbol;
210
+
211
+ useEffect(() => {
212
+ for (const [input, value] of [
213
+ [minInput.current, min],
214
+ [maxInput.current, max],
215
+ ] as const) {
216
+ if (!input) continue;
217
+ const matches =
218
+ value === "" ? input.value === "" : input.value !== "" && Number(input.value) === value;
219
+ if (!matches) {
220
+ if (timer.current) clearTimeout(timer.current);
221
+ timer.current = null;
222
+ input.value = String(value);
223
+ }
224
+ }
225
+ }, [min, max]);
226
+
227
+ useEffect(() => {
228
+ function cancelPendingSubmit() {
229
+ if (timer.current) clearTimeout(timer.current);
230
+ timer.current = null;
231
+ }
232
+
233
+ function restoreHistoryPrice() {
234
+ // Cancel drafts before history data loads, even if the price is unchanged.
235
+ cancelPendingSubmit();
236
+ const { filters } = parseCollectionParams(new URLSearchParams(window.location.search));
237
+ const price = filters.find((filter) => filter.price)?.price;
238
+ if (minInput.current) minInput.current.value = String(price?.min ?? "");
239
+ if (maxInput.current) maxInput.current.value = String(price?.max ?? "");
240
+ }
241
+
242
+ // Any form submission (native Enter included) already carries the typed price.
243
+ const form = minInput.current?.form;
244
+ form?.addEventListener("submit", cancelPendingSubmit);
245
+ window.addEventListener("popstate", restoreHistoryPrice);
246
+ return () => {
247
+ form?.removeEventListener("submit", cancelPendingSubmit);
248
+ window.removeEventListener("popstate", restoreHistoryPrice);
249
+ cancelPendingSubmit();
250
+ };
251
+ }, []);
252
+
253
+ function submitAfterTyping(event: React.ChangeEvent<HTMLInputElement>) {
254
+ if (timer.current) clearTimeout(timer.current);
255
+ const form = event.currentTarget.form;
256
+ timer.current = setTimeout(() => {
257
+ timer.current = null;
258
+ form?.requestSubmit();
259
+ }, 350);
260
+ }
261
+
178
262
  return (
179
263
  <>
180
- <PriceInput name="filter.v.price.gte" defaultValue={price?.min} label="Minimum price" />
181
- <PriceInput name="filter.v.price.lte" defaultValue={price?.max} label="Maximum price" />
264
+ <PriceInput
265
+ inputRef={minInput}
266
+ name="filter.v.price.gte"
267
+ defaultValue={min}
268
+ label={`Minimum price in ${currencyCode}`}
269
+ symbol={symbol}
270
+ onChange={submitAfterTyping}
271
+ />
272
+ <PriceInput
273
+ inputRef={maxInput}
274
+ name="filter.v.price.lte"
275
+ defaultValue={max}
276
+ label={`Maximum price in ${currencyCode}`}
277
+ symbol={symbol}
278
+ onChange={submitAfterTyping}
279
+ />
182
280
  </>
183
281
  );
184
282
  }
185
283
 
186
- function PriceInput({ name, defaultValue, label }: { name: string; defaultValue?: number; label: string }) {
187
- const initialValue = defaultValue == null ? "" : String(defaultValue);
284
+ function PriceInput({
285
+ inputRef,
286
+ name,
287
+ defaultValue,
288
+ label,
289
+ symbol,
290
+ onChange,
291
+ }: {
292
+ inputRef: React.Ref<HTMLInputElement>;
293
+ name: string;
294
+ defaultValue: number | "";
295
+ label: string;
296
+ symbol: string;
297
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
298
+ }) {
188
299
  return (
189
- <input
190
- type="number"
191
- name={name}
192
- defaultValue={defaultValue}
193
- aria-label={label}
194
- min="0"
195
- step="any"
196
- onBlur={(event) => {
197
- if (event.currentTarget.value !== initialValue) {
198
- event.currentTarget.form?.requestSubmit();
199
- }
200
- }}
201
- onKeyDown={(event) => {
202
- if (event.key === "Enter") {
203
- event.preventDefault();
204
- event.currentTarget.blur();
205
- }
206
- }}
207
- />
300
+ <span className="price-input">
301
+ <span aria-hidden="true">{symbol}</span>
302
+ <input
303
+ ref={inputRef}
304
+ type="number"
305
+ name={name}
306
+ defaultValue={defaultValue}
307
+ aria-label={label}
308
+ min="0"
309
+ step="any"
310
+ onChange={onChange}
311
+ />
312
+ </span>
208
313
  );
209
314
  }
210
315
 
@@ -241,7 +346,7 @@ function CheckboxFilterValue({
241
346
  type="checkbox"
242
347
  name={name}
243
348
  value={paramValue}
244
- defaultChecked={isFilterInputActive(activeFilters, value.input)}
349
+ checked={isFilterInputActive(activeFilters, value.input)}
245
350
  onChange={(event) => {
246
351
  if (isMutuallyExclusive(filter) && event.currentTarget.checked) {
247
352
  uncheckSiblings(event.currentTarget);
@@ -253,6 +358,20 @@ function CheckboxFilterValue({
253
358
  }
254
359
  ```
255
360
 
361
+ The symbol prefix sits outside the input, so the submitted value stays a plain number. Keep the wrapper simple so the prefix and input share one line at all widths:
362
+
363
+ ```css
364
+ .price-input {
365
+ display: inline-flex;
366
+ align-items: center;
367
+ gap: 0.25rem;
368
+ }
369
+
370
+ .price-input input {
371
+ min-width: 0;
372
+ }
373
+ ```
374
+
256
375
  Use `filter.type` only to choose the control shape. For checkbox values, keep `filterValueInputParamEntries` as thin app glue: `JSON.parse(value.input)` plus `serializeCollectionParams(...)`. Do not derive checkbox params from IDs, labels, or filter types.
257
376
 
258
377
  For active chips, use parsed active `ProductFilter` values from collection state:
@@ -298,4 +417,4 @@ Use the same binding with a synthetic handle:
298
417
  </CollectionProvider>
299
418
  ```
300
419
 
301
- When the search term changes, include it in the browse form `key` so old unchecked/checked inputs do not carry over.
420
+ Use a stable form identity while filters update, so controls keep focus. Use the submitted search term as the key when the form must reset for a new search. Keep that identity unchanged while the shopper types.
@@ -100,6 +100,8 @@ Hydrogen ships no account UI yet — the app owns it. A storefront with the hand
100
100
 
101
101
  Use `gql` and `createCustomerAccountClient` from `@shopify/hydrogen/customer-account` in server code only. Pass the access token per GraphQL call.
102
102
 
103
+ `requestContext.signal`, per-call `signal`, and the client's timeout signal are raced with `AbortSignal.any`. The Next.js edge sandbox does not have it, so an edge route or middleware that calls this client needs an `AbortSignal.any` polyfill installed before the first request.
104
+
103
105
  The same `@shopify/hydrogen/ts-plugin` and `hydrogen gql check` setup from the `hydrogen-storefront-client` skill validates Customer Account API documents too. If the check is not already chained into the app's `typecheck` package script, add it there (create the script if the app has none), then run it before treating setup as complete — framework typecheck commands do not validate `gql()` documents on their own.
104
106
 
105
107
  ## Local OAuth
@@ -4,7 +4,8 @@ description: >
4
4
  Guide for wiring Hydrogen request handlers in server frameworks. Use when
5
5
  adding, modifying, or reviewing handleShopifyRoutes, handleShopifyRedirects,
6
6
  SFAPI proxy routes, cart, predictive search, and Customer Account server handlers,
7
- checkout redirects, cart and UCP buy permalinks, AJAX cart proxy routes, /admin redirects, Storefront URL redirects,
7
+ UCP business profiles, checkout redirects, cart and UCP buy permalinks, AJAX cart proxy routes,
8
+ /admin redirects, Storefront URL redirects,
8
9
  requestContext response-header propagation, or framework middleware,
9
10
  not-found, and catch-all integration.
10
11
  ---
@@ -27,7 +28,15 @@ Request
27
28
  -> framework 404 page
28
29
  ```
29
30
 
30
- `handleShopifyRoutes` owns Hydrogen routes the framework should never see: SFAPI proxy URLs, the generic `/__shopify/*` API proxy, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, UCP buy permalinks like `/buy/{itemId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product URLs, and app-registered handler groups such as `createCartServerHandlers()` or `createCustomerAccountServerHandlers()`.
31
+ `handleShopifyRoutes` owns Hydrogen routes the framework should never see: SFAPI proxy URLs, the generic `/__shopify/*` API proxy, `/.well-known/ucp`, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, UCP buy permalinks like `/buy/{itemId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/api/ucp/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product URLs, and app-registered handler groups such as `createCartServerHandlers()` or `createCustomerAccountServerHandlers()`.
32
+
33
+ ## UCP Business Profile
34
+
35
+ `GET /.well-known/ucp` serves Shopify's managed UCP business profile from the headless storefront origin; `HEAD` returns the response headers without a body. Hydrogen forwards `If-None-Match` and `If-Modified-Since`, but not shopper cookies or authorization, and preserves `304 Not Modified` responses. Response headers are filtered through the small `UCP_RESPONSE_HEADERS` allowlist in the profile interceptor; upstream `Vary` is omitted because Hydrogen always requests JSON.
36
+
37
+ Successful profiles and `304` responses use public caching with a 60-second freshness lifetime and a 300-second stale-if-error window. Unpublished profiles return a JSON `404`, and other non-JSON upstream errors become JSON errors with the upstream status. Redirects and non-JSON successes return `502`; fetch timeouts return `504`. All errors use `Cache-Control: no-store`. Methods other than `GET` and `HEAD` continue to framework routing.
38
+
39
+ Serve the storefront over HTTPS and wire `handleShopifyRoutes` before framework routing so the profile is available without an app-owned route. Return the matched response directly without adding session headers or applying request-context headers again.
31
40
 
32
41
  ## Variant Id Redirects
33
42
 
@@ -96,12 +105,13 @@ import { createCustomerAccountServerHandlers } from "@shopify/hydrogen/customer-
96
105
 
97
106
  Run the app in dev and production modes, then check:
98
107
 
99
- 1. `POST /api/{api-version}/graphql.json` returns Storefront API JSON, not the app 404.
100
- 2. `GET /api/cart` returns cart handler JSON when cart handlers are registered. With no cart id, the body is `{cart: null}` and no Storefront API cart lookup is made.
101
- 3. `GET /api/predictive-search?q=snow` returns predictive search JSON when predictive search handlers are registered.
102
- 4. `GET /account/login`, `GET /account/authorize`, `GET /account/refresh`, and `POST /account/logout` are handled before the app router when Customer Account handlers are registered.
103
- 5. `GET /admin` returns a redirect to the shop admin URL.
104
- 6. An unknown path returns the framework 404 when no Shopify redirect exists.
105
- 7. `GET /products/{handle}?variant={numeric id}` returns a 302 to the option-params URL when `routeTemplates` is passed to `handleShopifyRoutes`; `?variant=garbage` falls through to the product page.
106
- 8. Cart, Customer Account, and consent responses preserve eligible `Set-Cookie` headers. Consent responses remain private and non-cacheable even when personal state is returned only in the body.
107
- 9. Authenticated Customer Account responses do not preserve public or CDN cache-control headers.
108
+ 1. `GET /.well-known/ucp` returns Shopify's UCP business profile with a public cache policy and no shopper cookies. `HEAD` returns headers without a body. A conditional request with a matching `If-None-Match` returns `304`; an unpublished profile returns an uncached JSON `404`.
109
+ 2. `POST /api/{api-version}/graphql.json` returns Storefront API JSON, not the app 404.
110
+ 3. `GET /api/cart` returns cart handler JSON when cart handlers are registered. With no cart id, the body is `{cart: null}` and no Storefront API cart lookup is made.
111
+ 4. `GET /api/predictive-search?q=snow` returns predictive search JSON when predictive search handlers are registered.
112
+ 5. `GET /account/login`, `GET /account/authorize`, `GET /account/refresh`, and `POST /account/logout` are handled before the app router when Customer Account handlers are registered.
113
+ 6. `GET /admin` returns a redirect to the shop admin URL.
114
+ 7. An unknown path returns the framework 404 when no Shopify redirect exists.
115
+ 8. `GET /products/{handle}?variant={numeric id}` returns a 302 to the option-params URL when `routeTemplates` is passed to `handleShopifyRoutes`; `?variant=garbage` falls through to the product page.
116
+ 9. Cart, Customer Account, and consent responses preserve eligible `Set-Cookie` headers. Consent responses remain private and non-cacheable even when personal state is returned only in the body.
117
+ 10. Authenticated Customer Account responses do not preserve public or CDN cache-control headers.
@@ -60,6 +60,7 @@ What it does **not** do:
60
60
  - Server-side analytics dispatch.
61
61
  - Third-party destination integrations (GA4, Meta Pixel, Klaviyo) — wire those with `addDestination()`.
62
62
  - Cart event publishing without `trackCartAnalytics()`. App code should not manually publish `cart_updated` etc.
63
+ - Forwarding `cart_viewed`, `cart_updated`, or `product_removed_from_cart` to Monorail. They stay on the bus for your own destinations.
63
64
  - DOM event ingestion or Standard Events. Explicit `publish()` is the API.
64
65
 
65
66
  ---
@@ -109,11 +110,11 @@ Resolve shop metadata on the server and pass it to ShopifyScripts. Shopify analy
109
110
 
110
111
  ### `i18n`
111
112
 
112
- Pass the app's resolved `country` and `language` market values. Optional `currency` sets `window.Shopify.currency.active` for Shopify runtime scripts and Shopify analytics. Shopify analytics reads its content language from `window.Shopify.locale`.
113
+ Pass the app's resolved `country` and `language` market values. Optional `currency` sets `window.Shopify.currency.active` for Shopify runtime scripts and Shopify analytics. For `product_viewed`, Shopify analytics reads the currency from `products[].price.currencyCode`. For `product_added_to_cart`, it reads the cart line's `cost.amountPerQuantity.currencyCode`. Both fall back to `window.Shopify.currency.active`. Page, collection, and search views take only the global. When it is unset, Shopify analytics sends them without a currency. The cart tracker also writes the global from cart cost once a cart exists. Shopify analytics reads its content language from `window.Shopify.locale`.
113
114
 
114
115
  ### `analytics`
115
116
 
116
- The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData`. Shopify analytics reads currency from `window.Shopify.currency.active`, which is seeded by `i18n.currency` and updated from cart currency when available.
117
+ The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData`.
117
118
 
118
119
  ### `consent`
119
120
 
@@ -320,7 +321,7 @@ Per-page view events go in the route's `$effect`. The route loader must include
320
321
  products: [{
321
322
  id: data.product.id,
322
323
  title: data.product.title,
323
- price: variant?.price.amount ?? data.product.priceRange.minVariantPrice.amount,
324
+ price: variant?.price ?? data.product.priceRange.minVariantPrice,
324
325
  vendor: data.product.vendor,
325
326
  variantId: variant?.id ?? data.product.id,
326
327
  variantTitle: variant?.title ?? data.product.title,
@@ -386,6 +387,7 @@ Per-page trackers are thin client components that take server-resolved data and
386
387
  // app/components/ProductViewedTracker.tsx
387
388
  "use client";
388
389
  import { useEffect } from "react";
390
+ import type { CurrencyCode } from "@shopify/hydrogen/storefront-api-types";
389
391
  import { getAnalytics, AnalyticsEvent } from "../lib/analytics";
390
392
 
391
393
  type Props = {
@@ -397,10 +399,10 @@ type Props = {
397
399
  selectedOrFirstAvailableVariant: {
398
400
  id: string;
399
401
  title: string;
400
- price: { amount: string };
402
+ price: { amount: string; currencyCode: CurrencyCode };
401
403
  sku?: string | null;
402
404
  } | null;
403
- priceRange: { minVariantPrice: { amount: string } };
405
+ priceRange: { minVariantPrice: { amount: string; currencyCode: CurrencyCode } };
404
406
  };
405
407
  };
406
408
 
@@ -413,8 +415,8 @@ export function ProductViewedTracker({ product }: Props) {
413
415
  id: product.id,
414
416
  title: product.title,
415
417
  price:
416
- product.selectedOrFirstAvailableVariant?.price.amount ??
417
- product.priceRange.minVariantPrice.amount,
418
+ product.selectedOrFirstAvailableVariant?.price ??
419
+ product.priceRange.minVariantPrice,
418
420
  vendor: product.vendor,
419
421
  variantId: product.selectedOrFirstAvailableVariant?.id ?? product.id,
420
422
  variantTitle:
@@ -482,6 +484,7 @@ const product = await fetchProduct(Astro.params.handle);
482
484
  data-title={product.title}
483
485
  data-vendor={product.vendor}
484
486
  data-price={product.selectedOrFirstAvailableVariant?.price.amount ?? product.priceRange.minVariantPrice.amount}
487
+ data-currency-code={product.selectedOrFirstAvailableVariant?.price.currencyCode ?? product.priceRange.minVariantPrice.currencyCode}
485
488
  data-variant-id={product.selectedOrFirstAvailableVariant?.id ?? product.id}
486
489
  data-variant-title={product.selectedOrFirstAvailableVariant?.title ?? product.title}
487
490
  data-sku={product.selectedOrFirstAvailableVariant?.sku ?? ""}
@@ -489,6 +492,7 @@ const product = await fetchProduct(Astro.params.handle);
489
492
  ></div>
490
493
 
491
494
  <script>
495
+ import type { CurrencyCode } from "@shopify/hydrogen/storefront-api-types";
492
496
  import { getAnalytics, AnalyticsEvent } from "../../lib/analytics";
493
497
  const el = document.getElementById("product-analytics");
494
498
  const analytics = getAnalytics();
@@ -497,7 +501,8 @@ const product = await fetchProduct(Astro.params.handle);
497
501
  products: [{
498
502
  id: el.dataset.id ?? "",
499
503
  title: el.dataset.title ?? "",
500
- price: el.dataset.price ?? "",
504
+ // data-* values are strings. This one was rendered from the Storefront API CurrencyCode.
505
+ price: { amount: el.dataset.price ?? "", currencyCode: el.dataset.currencyCode as CurrencyCode },
501
506
  vendor: el.dataset.vendor ?? "",
502
507
  variantId: el.dataset.variantId ?? el.dataset.id ?? "",
503
508
  variantTitle: el.dataset.variantTitle ?? el.dataset.title ?? "",
@@ -609,6 +614,7 @@ For production, re-verify against the production bundle. Several gotchas only ap
609
614
  - **Astro inline scripts cannot reference component scope.** Astro hoists `<script>` tags at build time. Bridge SSR data through hidden DOM (`data-*` attributes) and read it from the script. Trying to interpolate `{product.id}` directly into a script body silently fails — the script ships as a static string.
610
615
  - **Astro page-view fires only on full loads.** Astro is MPA-by-default. If you adopt View Transitions, listen for `astro:after-swap` instead of relying on the inline-script-runs-on-load behavior — otherwise SPA-nav transitions skip `page_viewed`.
611
616
  - **Required product fields silently drop the Monorail leg.** Missing `id`/`title`/`vendor`/`variantId`/`variantTitle`/`price` causes the Shopify analytics subscriber to skip Monorail dispatch and log a field-specific error. The bus event still fires for your subscribers — the loss is only in Shopify analytics. Watch the console.
617
+ - **A product event without a currency logs a warning. Shopify analytics still sends it.** `[shopify:warn:analytics] product_added_to_cart was sent without a currency` means the cart fragment's `cost.amountPerQuantity` has no `currencyCode` and `window.Shopify.currency.active` is unset. Select `currencyCode` in the fragment or pass `i18n.currency`. `product_viewed` cannot reach this warning through the typed `ProductPayload`, whose `price` is `MoneyV2`.
612
618
  - **Register destinations once per page lifetime.** Hydrogen owns the shared bus. Removing and re-adding a destination resets its replay position, so component remounts can deliver retained events again.
613
619
  - **Lighthouse skip is silent.** Monorail dispatch is skipped for Chrome Lighthouse user-agents. If your synthetic monitoring runs Lighthouse, you will see no Monorail requests in those runs — this is intentional.
614
620
 
@@ -6,12 +6,12 @@ Read `package.json` in the current directory.
6
6
 
7
7
  If it does not exist, stop and tell the user this skill must run in a javascript application with a `package.json` at its root.
8
8
 
9
- If it does not contain `@shopify/hydrogen@preview` (or a more specific hash) in its dependencies, ask the user to install it. Do not assume package managers or install for the user.
9
+ If it does not contain `@shopify/hydrogen` 2026.10.0 or later in its dependencies, ask the user to install it. Do not assume package managers or install for the user.
10
10
 
11
11
  ### Continue when
12
12
 
13
13
  - [ ] Current directory has `package.json`
14
- - [ ] Current project includes `@shopify/hydrogen@preview` or similar in its dependencies
14
+ - [ ] Current project includes `@shopify/hydrogen` 2026.10.0 or later in its dependencies
15
15
 
16
16
  ## Styling
17
17
 
@@ -207,21 +207,27 @@ export const { ProductProvider, useProduct, useProductForm } =
207
207
  Wrap the purchase UI in `ProductProvider`. Put same-product URL navigation in `onSelect`, not inside each option control:
208
208
 
209
209
  ```tsx
210
+ import { useLocation, useNavigate } from "react-router";
210
211
  import { ProductProvider, useProductForm } from "~/lib/product";
212
+ import type { ProductData } from "~/lib/product-query";
211
213
 
212
214
  export function ProductDetails({ product }: { product: ProductData }) {
213
- const navigate = useNavigate(); // or Next.js router.replace
215
+ const navigate = useNavigate();
216
+ const location = useLocation();
214
217
 
215
218
  return (
216
219
  <ProductProvider
217
220
  product={product}
218
221
  onSelect={(result) => {
219
- const targetHandle =
220
- result.selectedVariant?.product?.handle ?? product.handle;
221
- void navigate(variantUrl(targetHandle, result.selectedOptions), {
222
- replace: true,
223
- preventScrollReset: true,
224
- });
222
+ void navigate(
223
+ variantUrl(
224
+ product,
225
+ result.selectedOptions,
226
+ result.selectedVariant?.product?.handle,
227
+ new URLSearchParams(location.search),
228
+ ),
229
+ { replace: true, preventScrollReset: true },
230
+ );
225
231
  }}
226
232
  >
227
233
  <ProductPurchasePanel product={product} />
@@ -230,6 +236,8 @@ export function ProductDetails({ product }: { product: ProductData }) {
230
236
  }
231
237
  ```
232
238
 
239
+ This sample uses React Router. `variantUrl(product, selectedOptions, handle, base)` is the URL builder from the `hydrogen-variant-form` React Router reference. Use the same current-query `base` for option link URLs. In Next.js, use `useRouter().replace` with the `useSearchParams()` base, as the `hydrogen-variant-form` Next.js reference shows.
240
+
233
241
  `useProduct` is for read-only selection state (price, gallery, analytics). `useProductForm` adds form bindings (`register`, `formProps`, `pending`):
234
242
 
235
243
  ```tsx
@@ -272,8 +280,8 @@ function ProductPurchasePanel({ product }: { product: ProductData }) {
272
280
 
273
281
  Framework notes:
274
282
 
275
- - Next.js App Router: fetch in the server page; put `ProductProvider` and the form in a `"use client"` component. Use `router.replace(url, { scroll: false })` in `onSelect`.
276
- - React Router: put URL sync in provider `onSelect`. Same-product option values are GET `<Link>`s that spread `register("optionValue", ...)` (no-JS fallback). Cross-product values use `<Link preventScrollReset>`. See the `hydrogen-variant-form` React reference.
283
+ - Next.js App Router: fetch in the server page; put `ProductProvider` and the form in a `"use client"` component. Use `router.replace(url, { scroll: false })` in `onSelect`. For existing same-product values, render GET links and use `onNavigate` to prevent Link navigation before calling the registered handler. A cross-product combined-listing navigation remounts the page component, so restore focus to the activated value as the `hydrogen-variant-form` Next.js reference shows. A no-JS fallback also requires the product content to render without client-side scripts; a streamed Suspense boundary can prevent this.
284
+ - React Router: put URL sync in provider `onSelect`. Same-product option values are GET `<Link>`s for the no-JS fallback. On an unmodified primary click, prevent Link navigation and call the registered `onClick`; leave modified and other-target clicks alone. Cross-product values use `<Link preventScrollReset>`. See the `hydrogen-variant-form` React Router reference.
277
285
 
278
286
  ### Vue
279
287
 
@@ -394,7 +402,7 @@ const addable = computed(() => canAddToCart(props.product, form.options));
394
402
  </template>
395
403
  ```
396
404
 
397
- Same-product option values should be `NuxtLink` (or the app's link component) GET links that `v-bind` `form.register('optionValue', ...)`. Cross-product combined-listing values navigate to the other product. See the `hydrogen-variant-form` Nuxt reference.
405
+ For existing same-product option values, use `NuxtLink` custom mode to render a native anchor with its resolved `href`. On an unmodified primary click, prevent the anchor's default navigation and call the registered `onClick`; the provider's `onSelect` owns navigation. Leave modified and other-target clicks alone. Cross-product combined-listing values navigate to the other product without selecting in the current provider. See the `hydrogen-variant-form` Nuxt reference.
398
406
 
399
407
  ### Other Framework Gotchas
400
408
 
@@ -402,6 +410,8 @@ Same-product option values should be `NuxtLink` (or the app's link component) GE
402
410
  - Astro: only build this route when the app has server output or a server adapter. Put the interactive product form in a hydrated island or client script that owns the store lifecycle.
403
411
  - SolidStart: manage the core store lifecycle inside the client component unless a local binding already exists.
404
412
 
413
+ For SolidStart and SvelteKit, render existing same-product options as GET anchors. On an unmodified primary click, prevent default navigation, select through the core store, and navigate once through the existing selection callback. Use the same query-parameter base for the anchor URL and the callback. Leave modified and other-target clicks alone. Keep nonexistent combinations as disabled buttons.
414
+
405
415
  ## Continue when
406
416
 
407
417
  - [ ] Directly loading `/products/{handle}` renders product data and a selected or first available variant when one exists
@@ -411,6 +421,6 @@ Same-product option values should be `NuxtLink` (or the app's link component) GE
411
421
  - [ ] An invalid or failing product query (e.g.: invalid field) logs the GraphQL error server-side and returns 500, not 404
412
422
  - [ ] A valid query for a missing product returns 404
413
423
  - [ ] Selecting a variant and refreshing the page persists the same variant selected
414
- - [ ] With javascript disabled, clicking on a variant option navigates to the selected variant
424
+ - [ ] With JavaScript disabled, clicking a variant link loads its selected variant when the product page supports no-JS rendering; record any rendering limitation rather than claim a link alone provides that fallback
415
425
  - [ ] Cross-product option values use the framework's client-side link component when the app has one.
416
426
  - [ ] The product variants form passes the `hydrogen-variant-form` skill's user acceptance tests.
@@ -258,7 +258,7 @@ const { data } = await client.graphql(QUERY);
258
258
  const { data } = await client.graphql(QUERY, { signal });
259
259
  ```
260
260
 
261
- `requestContext.signal`, per-call `signal`, and the client's timeout signal are raced automatically. At the final response boundary, append committed session headers first, then call `requestContext.applyResponseHeaders(response.headers)`.
261
+ `requestContext.signal`, per-call `signal`, and the client's timeout signal are raced automatically. They are combined with `AbortSignal.any`, which the Next.js edge sandbox does not have, so an edge route or middleware that calls this client needs an `AbortSignal.any` polyfill installed before the first request. At the final response boundary, append committed session headers first, then call `requestContext.applyResponseHeaders(response.headers)`.
262
262
 
263
263
  Read `references/caching.md` to cache catalog reads across sub-requests on Oxygen-style runtimes.
264
264
 
@@ -20,6 +20,8 @@ const { data, errors } = await client.graphql(PRODUCTS, { variables, cache: Cach
20
20
 
21
21
  **Caching only engages when you pass `cache` per query.** Configuring `cache` on the client alone caches nothing — queries without a `cache` strategy (and all mutations) are never cached.
22
22
 
23
+ A stale-while-revalidate refresh runs under `waitUntil` after the caller already has the stale result, so request and per-call aborts don't cancel it. It is bounded by the client's `defaultTimeoutInMs`, or 30 seconds when that is `0`. A custom `runWithCache` passed to `createFetchWithCache` should call `run({ background: true })` for these refreshes so the `backgroundSignal` cache option applies.
24
+
23
25
  ## Never cache private data
24
26
 
25
27
  Do not pass a `cache` strategy to personalized reads (cart, customer); keep them on a request-scoped client. Cache mode `private` is unsupported — for buyer-specific caching use `createRunWithCache` with an explicit private key.