@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.
- package/dist/cli/index.mjs +23 -12
- package/dist/client/client.d.mts +18 -15
- package/dist/client/client.mjs +8 -3
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/errors.d.mts +35 -13
- package/dist/client/errors.mjs +23 -13
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +140 -90
- package/dist/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/core/analytics/cart-tracker.mjs +18 -10
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/events.d.mts +17 -11
- package/dist/core/analytics/events.mjs +13 -4
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/types.d.mts +207 -118
- package/dist/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/core/cache/key.d.mts +2 -0
- package/dist/core/cache/key.mjs.map +1 -1
- package/dist/core/cache/run-with-cache.d.mts +56 -8
- package/dist/core/cache/run-with-cache.mjs +17 -6
- package/dist/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/core/cache/store.d.mts +8 -6
- package/dist/core/cache/store.mjs.map +1 -1
- package/dist/core/cache/strategies.d.mts +27 -2
- package/dist/core/cache/strategies.mjs +6 -0
- package/dist/core/cache/strategies.mjs.map +1 -1
- package/dist/core/cart/actions.d.mts +37 -30
- package/dist/core/cart/actions.mjs +21 -9
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/buyer-identity-sync.d.mts +7 -6
- package/dist/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +59 -46
- package/dist/core/cart/cart.mjs +46 -29
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +8 -4
- package/dist/core/cart/cookie.mjs +8 -4
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +34 -20
- package/dist/core/cart/form.mjs +6 -4
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +27 -15
- package/dist/core/cart/get-cart.mjs +13 -8
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/quantity.d.mts +16 -5
- package/dist/core/cart/quantity.mjs +16 -5
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +25 -19
- package/dist/core/cart/queries.mjs +15 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +82 -45
- package/dist/core/cart/server-handlers.mjs +4 -0
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +110 -64
- package/dist/core/cart/state.mjs +24 -6
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +52 -53
- package/dist/core/collection/collection.mjs +5 -3
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/reconciler.d.mts +23 -16
- package/dist/core/collection/reconciler.mjs +8 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +31 -26
- package/dist/core/collection/state.mjs +4 -3
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +70 -29
- package/dist/core/collection/url.mjs +63 -25
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -0
- package/dist/core/constants.mjs +1 -0
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/logging/logging.d.mts +14 -8
- package/dist/core/logging/logging.mjs +15 -10
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/logging/types.d.mts +28 -11
- package/dist/core/logging/types.mjs.map +1 -1
- package/dist/core/money/format.d.mts +6 -4
- package/dist/core/money/format.mjs +19 -9
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/money/types.d.mts +41 -25
- package/dist/core/predictive-search/form.d.mts +37 -7
- package/dist/core/predictive-search/form.mjs +21 -3
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/queries.d.mts +18 -30
- package/dist/core/predictive-search/queries.mjs +7 -3
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +25 -27
- package/dist/core/predictive-search/search.mjs +20 -19
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +65 -24
- package/dist/core/predictive-search/store.mjs +12 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +36 -37
- package/dist/core/predictive-search/url.mjs +14 -6
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +63 -16
- package/dist/core/product/form.mjs +8 -2
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/options.d.mts +8 -10
- package/dist/core/product/options.mjs +8 -10
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +77 -29
- package/dist/core/product/product-form.mjs +32 -10
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +55 -32
- package/dist/core/product/url.d.mts +19 -13
- package/dist/core/product/url.mjs +10 -10
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +77 -19
- package/dist/core/request-context.mjs +5 -1
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.d.mts +5 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/core/request-routing/registered-routes.mjs +12 -0
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +43 -18
- package/dist/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +15 -5
- package/dist/core/shopify-scripts/index.mjs +15 -5
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +58 -25
- package/dist/core/standard-routes/build.d.mts +11 -12
- package/dist/core/standard-routes/build.mjs +11 -12
- package/dist/core/standard-routes/build.mjs.map +1 -1
- package/dist/core/standard-routes/types.d.mts +30 -24
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +36 -69
- package/dist/customer-account/client.mjs +7 -8
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +45 -36
- package/dist/customer-account/errors.mjs +21 -36
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +11 -23
- package/dist/customer-account/graphql.mjs +3 -9
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +130 -100
- package/dist/customer-account/session.mjs +32 -10
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +7 -8
- package/dist/development/client/client.d.mts +18 -15
- package/dist/development/client/client.mjs +8 -3
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/errors.d.mts +35 -13
- package/dist/development/client/errors.mjs +23 -13
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +140 -90
- package/dist/development/core/analytics/cart-tracker.d.mts +19 -10
- package/dist/development/core/analytics/cart-tracker.mjs +18 -10
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/events.d.mts +17 -11
- package/dist/development/core/analytics/events.mjs +13 -4
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/types.d.mts +207 -118
- package/dist/development/core/analytics/utils/flatten-connection.d.mts +11 -9
- package/dist/development/core/analytics/utils/flatten-connection.mjs +7 -6
- package/dist/development/core/analytics/utils/flatten-connection.mjs.map +1 -1
- package/dist/development/core/cache/fetch-with-cache.d.mts +24 -4
- package/dist/development/core/cache/fetch-with-cache.mjs +17 -2
- package/dist/development/core/cache/fetch-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/key.d.mts +2 -0
- package/dist/development/core/cache/key.mjs.map +1 -1
- package/dist/development/core/cache/run-with-cache.d.mts +56 -8
- package/dist/development/core/cache/run-with-cache.mjs +17 -6
- package/dist/development/core/cache/run-with-cache.mjs.map +1 -1
- package/dist/development/core/cache/store.d.mts +8 -6
- package/dist/development/core/cache/store.mjs.map +1 -1
- package/dist/development/core/cache/strategies.d.mts +27 -2
- package/dist/development/core/cache/strategies.mjs +6 -0
- package/dist/development/core/cache/strategies.mjs.map +1 -1
- package/dist/development/core/cart/actions.d.mts +37 -30
- package/dist/development/core/cart/actions.mjs +21 -9
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs +8 -7
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/buyer-identity-sync.d.mts +6 -5
- package/dist/development/core/cart/buyer-identity-sync.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +59 -46
- package/dist/development/core/cart/cart.mjs +46 -29
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +8 -4
- package/dist/development/core/cart/cookie.mjs +8 -4
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +34 -20
- package/dist/development/core/cart/form.mjs +6 -4
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +27 -15
- package/dist/development/core/cart/get-cart.mjs +13 -8
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/quantity.d.mts +16 -5
- package/dist/development/core/cart/quantity.mjs +16 -5
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +25 -19
- package/dist/development/core/cart/queries.mjs +15 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +82 -45
- package/dist/development/core/cart/server-handlers.mjs +4 -0
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +110 -64
- package/dist/development/core/cart/state.mjs +24 -6
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +52 -53
- package/dist/development/core/collection/collection.mjs +5 -3
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/reconciler.d.mts +23 -16
- package/dist/development/core/collection/reconciler.mjs +8 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +31 -26
- package/dist/development/core/collection/state.mjs +4 -3
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +70 -29
- package/dist/development/core/collection/url.mjs +63 -25
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/logging/logging.d.mts +14 -8
- package/dist/development/core/logging/logging.mjs +15 -10
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/logging/types.d.mts +28 -11
- package/dist/development/core/logging/types.mjs.map +1 -1
- package/dist/development/core/money/format.d.mts +6 -4
- package/dist/development/core/money/format.mjs +19 -9
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/money/types.d.mts +41 -25
- package/dist/development/core/predictive-search/form.d.mts +37 -7
- package/dist/development/core/predictive-search/form.mjs +21 -3
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +18 -30
- package/dist/development/core/predictive-search/queries.mjs +7 -3
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +25 -27
- package/dist/development/core/predictive-search/search.mjs +20 -19
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +25 -26
- package/dist/development/core/predictive-search/server-handlers.mjs +4 -0
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +65 -24
- package/dist/development/core/predictive-search/store.mjs +12 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +36 -37
- package/dist/development/core/predictive-search/url.mjs +14 -6
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +63 -16
- package/dist/development/core/product/form.mjs +8 -2
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/options.d.mts +8 -10
- package/dist/development/core/product/options.mjs +8 -10
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +77 -29
- package/dist/development/core/product/product-form.mjs +32 -10
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +55 -32
- package/dist/development/core/product/url.d.mts +19 -13
- package/dist/development/core/product/url.mjs +10 -10
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +77 -19
- package/dist/development/core/request-context.mjs +5 -1
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-redirects.d.mts +12 -4
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs +9 -2
- package/dist/development/core/request-routing/handle-shopify-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.d.mts +9 -1
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs +7 -0
- package/dist/development/core/request-routing/handle-shopify-routes.development.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +7 -5
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +79 -43
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs +69 -0
- package/dist/development/core/request-routing/interceptors/ucp-profile-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs +6 -1
- package/dist/development/core/request-routing/interceptors/url-redirects.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs +12 -0
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +43 -18
- package/dist/development/core/shop-pay/shop-pay.d.mts +71 -35
- package/dist/development/core/shop-pay/shop-pay.mjs +36 -10
- package/dist/development/core/shop-pay/shop-pay.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +15 -5
- package/dist/development/core/shopify-scripts/index.mjs +15 -5
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs +12 -4
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +58 -25
- package/dist/development/core/standard-routes/build.d.mts +11 -12
- package/dist/development/core/standard-routes/build.mjs +11 -12
- package/dist/development/core/standard-routes/build.mjs.map +1 -1
- package/dist/development/core/standard-routes/types.d.mts +30 -24
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +45 -54
- package/dist/development/customer-account/session.mjs +20 -4
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +20 -11
- package/dist/development/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/development/graphql/graphql.d.mts +17 -16
- package/dist/development/graphql/graphql.mjs +7 -8
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +15 -8
- package/dist/development/react/cart.d.mts +78 -32
- package/dist/development/react/cart.mjs +56 -29
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +40 -22
- package/dist/development/react/collection.mjs +27 -7
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/predictive-search.d.mts +65 -50
- package/dist/development/react/predictive-search.mjs +23 -21
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +45 -16
- package/dist/development/react/product.mjs +13 -11
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shop-pay.d.mts +15 -0
- package/dist/development/react/shop-pay.mjs +9 -0
- package/dist/development/react/shop-pay.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.d.mts +23 -2
- package/dist/development/react/shopify-scripts.mjs +9 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +28 -23
- package/dist/development/vue/cart.mjs +42 -25
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +20 -12
- package/dist/development/vue/collection.mjs +15 -7
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +2 -2
- package/dist/development/vue/index.mjs +2 -2
- package/dist/development/vue/predictive-search.d.mts +31 -36
- package/dist/development/vue/predictive-search.mjs +16 -22
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +16 -15
- package/dist/development/vue/product.mjs +11 -11
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shop-pay.d.mts +14 -17
- package/dist/development/vue/shop-pay.mjs +8 -0
- package/dist/development/vue/shop-pay.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +11 -0
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +20 -11
- package/dist/graphql/generated/storefront-api-types.d.mts +127 -1
- package/dist/graphql/graphql.d.mts +17 -16
- package/dist/graphql/graphql.mjs +7 -8
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +15 -8
- package/dist/react/cart.d.mts +78 -32
- package/dist/react/cart.mjs +56 -29
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +40 -22
- package/dist/react/collection.mjs +27 -7
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/predictive-search.d.mts +65 -50
- package/dist/react/predictive-search.mjs +23 -21
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +45 -16
- package/dist/react/product.mjs +13 -11
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shop-pay.d.mts +15 -0
- package/dist/react/shop-pay.mjs +9 -0
- package/dist/react/shop-pay.mjs.map +1 -1
- package/dist/react/shopify-scripts.d.mts +23 -2
- package/dist/react/shopify-scripts.mjs +9 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/ts-plugin/index.cjs +6 -0
- package/dist/ts-plugin/index.cjs.map +1 -1
- package/dist/ts-plugin/index.d.cts +6 -0
- package/dist/vite/local-https.d.mts +51 -17
- package/dist/vite/local-https.mjs +26 -5
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vue/cart.d.mts +28 -23
- package/dist/vue/cart.mjs +42 -25
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +20 -12
- package/dist/vue/collection.mjs +15 -7
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/index.mjs +2 -2
- package/dist/vue/predictive-search.d.mts +31 -36
- package/dist/vue/predictive-search.mjs +16 -22
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +16 -15
- package/dist/vue/product.mjs +11 -11
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shop-pay.d.mts +14 -17
- package/dist/vue/shop-pay.mjs +8 -0
- package/dist/vue/shop-pay.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +11 -0
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +3 -1
- package/skills/hydrogen-analytics/SKILL.md +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +28 -2
- package/skills/hydrogen-collection-browser/SKILL.md +9 -1
- package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
- package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
- package/skills/hydrogen-collection-browser/references/react.md +151 -32
- package/skills/hydrogen-customer-account/SKILL.md +2 -0
- package/skills/hydrogen-request-handlers/SKILL.md +21 -11
- package/skills/hydrogen-setup/references/analytics.md +14 -8
- package/skills/hydrogen-setup/steps/1-inspect-app.md +2 -2
- package/skills/hydrogen-setup/steps/6-product-detail-page.md +21 -11
- package/skills/hydrogen-storefront-client/SKILL.md +1 -1
- package/skills/hydrogen-storefront-client/references/caching.md +2 -0
- package/skills/hydrogen-variant-form/SKILL.md +25 -7
- package/skills/hydrogen-variant-form/references/nextjs.md +39 -4
- package/skills/hydrogen-variant-form/references/nuxt.md +83 -42
- 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
|
-
|
|
138
|
+
currencyCode={currencyCode}
|
|
128
139
|
/>
|
|
129
|
-
<select name="sort_by"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
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({
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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. `
|
|
100
|
-
2. `
|
|
101
|
-
3. `GET /api/
|
|
102
|
-
4. `GET /
|
|
103
|
-
5. `GET /
|
|
104
|
-
6.
|
|
105
|
-
7.
|
|
106
|
-
8.
|
|
107
|
-
9.
|
|
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`.
|
|
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
|
|
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
|
|
417
|
-
product.priceRange.minVariantPrice
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|