@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.4
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/README.md +34 -3
- package/dist/cli/index.mjs +578 -85
- package/dist/client/client.mjs +2 -2
- package/dist/client/errors.d.mts +19 -19
- package/dist/client/errors.mjs +19 -16
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +91 -10
- package/dist/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/core/analytics/cart-tracker.mjs +15 -1
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/custom-consent.mjs +12 -0
- package/dist/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/core/analytics/events.d.mts +7 -0
- package/dist/core/analytics/events.mjs +7 -0
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/index.d.mts +1 -1
- package/dist/core/analytics/types.d.mts +152 -16
- package/dist/core/cart/actions.d.mts +47 -3
- package/dist/core/cart/actions.mjs +29 -0
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +106 -6
- package/dist/core/cart/cart.mjs +57 -6
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +15 -0
- package/dist/core/cart/cookie.mjs +15 -0
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +61 -0
- package/dist/core/cart/form.mjs +18 -0
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +43 -2
- package/dist/core/cart/get-cart.mjs +33 -0
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/index.d.mts +1 -1
- package/dist/core/cart/quantity.d.mts +17 -0
- package/dist/core/cart/quantity.mjs +17 -0
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +402 -369
- package/dist/core/cart/queries.mjs +2 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +69 -0
- package/dist/core/cart/server-handlers.mjs +2 -10
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +133 -0
- package/dist/core/cart/state.mjs +8 -0
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +18 -2
- package/dist/core/collection/collection.mjs +11 -0
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/index.d.mts +1 -1
- package/dist/core/collection/reconciler.d.mts +13 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +7 -2
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +3 -0
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -1
- package/dist/core/constants.mjs +2 -2
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/headers.mjs +2 -2
- package/dist/core/headers.mjs.map +1 -1
- package/dist/core/index.d.mts +5 -5
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/money/format.mjs +6 -3
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/observable.mjs +0 -3
- package/dist/core/observable.mjs.map +1 -1
- package/dist/core/predictive-search/form.d.mts +1 -0
- package/dist/core/predictive-search/form.mjs +1 -0
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/index.d.mts +1 -1
- package/dist/core/predictive-search/queries.d.mts +40 -4
- package/dist/core/predictive-search/queries.mjs +7 -1
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +43 -10
- package/dist/core/predictive-search/search.mjs +34 -0
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +3 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +50 -8
- package/dist/core/predictive-search/url.mjs +9 -0
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +21 -2
- package/dist/core/product/form.mjs +13 -0
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/index.d.mts +1 -1
- package/dist/core/product/options.d.mts +2 -2
- package/dist/core/product/options.mjs +2 -2
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +33 -1
- package/dist/core/product/product-form.mjs +30 -0
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +41 -0
- package/dist/core/product/url.d.mts +6 -0
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +8 -5
- package/dist/core/request-context.mjs +17 -39
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/core/shopify-scripts/constants.mjs +4 -2
- package/dist/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/core/shopify-scripts/global.mjs +9 -4
- package/dist/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +1 -0
- package/dist/core/shopify-scripts/index.mjs +14 -12
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +6 -0
- package/dist/core/url.mjs +21 -2
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +128 -4
- package/dist/customer-account/client.mjs +27 -1
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +46 -0
- package/dist/customer-account/errors.mjs +46 -0
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +68 -0
- package/dist/customer-account/graphql.mjs +47 -0
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +114 -23
- package/dist/customer-account/session.mjs +27 -12
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +23 -0
- package/dist/customer-account-api-types.d.ts +570 -374
- package/dist/customer-account.schema.json +2981 -1527
- package/dist/development/client/client.mjs +2 -2
- package/dist/development/client/errors.d.mts +19 -19
- package/dist/development/client/errors.mjs +20 -20
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +91 -10
- package/dist/development/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/development/core/analytics/cart-tracker.mjs +15 -1
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/custom-consent.mjs +12 -0
- package/dist/development/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/development/core/analytics/events.d.mts +7 -0
- package/dist/development/core/analytics/events.mjs +7 -0
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/index.d.mts +1 -1
- package/dist/development/core/analytics/types.d.mts +152 -16
- package/dist/development/core/cart/actions.d.mts +47 -3
- package/dist/development/core/cart/actions.mjs +29 -0
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +106 -6
- package/dist/development/core/cart/cart.mjs +57 -6
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +15 -0
- package/dist/development/core/cart/cookie.mjs +15 -0
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +61 -0
- package/dist/development/core/cart/form.mjs +18 -0
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +43 -2
- package/dist/development/core/cart/get-cart.mjs +33 -0
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/index.d.mts +1 -1
- package/dist/development/core/cart/quantity.d.mts +17 -0
- package/dist/development/core/cart/quantity.mjs +17 -0
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +345 -312
- package/dist/development/core/cart/queries.mjs +2 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +69 -0
- package/dist/development/core/cart/server-handlers.mjs +2 -10
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +133 -0
- package/dist/development/core/cart/state.mjs +8 -0
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +18 -2
- package/dist/development/core/collection/collection.mjs +11 -0
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/index.d.mts +1 -1
- package/dist/development/core/collection/reconciler.d.mts +13 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +7 -2
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +3 -0
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/development.d.mts +5 -5
- package/dist/development/core/headers.mjs +2 -2
- package/dist/development/core/headers.mjs.map +1 -1
- package/dist/development/core/index.d.mts +4 -4
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/money/format.mjs +6 -3
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/observable.mjs +0 -3
- package/dist/development/core/observable.mjs.map +1 -1
- package/dist/development/core/predictive-search/form.d.mts +1 -0
- package/dist/development/core/predictive-search/form.mjs +1 -0
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/index.d.mts +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +38 -2
- package/dist/development/core/predictive-search/queries.mjs +7 -1
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +43 -10
- package/dist/development/core/predictive-search/search.mjs +34 -0
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/development/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +3 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +50 -8
- package/dist/development/core/predictive-search/url.mjs +9 -0
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +21 -2
- package/dist/development/core/product/form.mjs +13 -0
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/index.d.mts +1 -1
- package/dist/development/core/product/options.d.mts +2 -2
- package/dist/development/core/product/options.mjs +2 -2
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +33 -1
- package/dist/development/core/product/product-form.mjs +30 -0
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +41 -0
- package/dist/development/core/product/url.d.mts +6 -0
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +8 -5
- package/dist/development/core/request-context.mjs +17 -39
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/constants.mjs +4 -2
- package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/global.mjs +9 -4
- package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +1 -0
- package/dist/development/core/shopify-scripts/index.mjs +14 -12
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/development/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +6 -0
- package/dist/development/core/url.mjs +21 -2
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +69 -13
- package/dist/development/customer-account/session.mjs +4 -0
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +6 -5
- package/dist/development/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/development/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/development/graphql/graphql.d.mts +33 -0
- package/dist/development/graphql/graphql.mjs +30 -0
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +14 -0
- package/dist/development/react/cart.d.mts +85 -14
- package/dist/development/react/cart.mjs +123 -3
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +2 -4
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/index.d.mts +8 -6
- package/dist/development/react/index.mjs +2 -2
- package/dist/development/react/predictive-search.d.mts +68 -7
- package/dist/development/react/predictive-search.mjs +54 -12
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +1 -0
- package/dist/development/react/product.mjs +5 -2
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.mjs +1 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +85 -6
- package/dist/development/vue/cart.mjs +95 -0
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +83 -5
- package/dist/development/vue/collection.mjs +52 -0
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +6 -4
- package/dist/development/vue/predictive-search.d.mts +53 -6
- package/dist/development/vue/predictive-search.mjs +33 -0
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +6 -2
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +30 -3
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +6 -5
- package/dist/graphql/generated/customer-account-graphql-env.d.mts +1717 -1090
- package/dist/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/graphql/graphql.d.mts +33 -0
- package/dist/graphql/graphql.mjs +30 -0
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +14 -0
- package/dist/react/cart.d.mts +85 -14
- package/dist/react/cart.mjs +123 -3
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +2 -4
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/index.d.mts +8 -6
- package/dist/react/index.mjs +2 -2
- package/dist/react/predictive-search.d.mts +68 -7
- package/dist/react/predictive-search.mjs +54 -12
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +1 -0
- package/dist/react/product.mjs +5 -2
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shopify-scripts.mjs +1 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/storefront-api-types.d.ts +165 -20
- package/dist/storefront.schema.json +782 -57
- package/dist/vite/customer-account.mjs +191 -0
- package/dist/vite/customer-account.mjs.map +1 -0
- package/dist/vite/local-https.mjs +13 -29
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vite/mkcert.mjs +1 -1
- package/dist/vue/cart.d.mts +85 -6
- package/dist/vue/cart.mjs +95 -0
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +83 -5
- package/dist/vue/collection.mjs +52 -0
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +6 -4
- package/dist/vue/predictive-search.d.mts +53 -6
- package/dist/vue/predictive-search.mjs +33 -0
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +6 -2
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +31 -4
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +5 -2
- package/skills/hydrogen-analytics/SKILL.md +6 -5
- package/skills/hydrogen-analytics/references/custom-consent.md +52 -0
- package/skills/hydrogen-analytics/references/react.md +1 -1
- package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
- package/skills/hydrogen-cart-ui/SKILL.md +2 -1
- package/skills/hydrogen-customer-account/SKILL.md +3 -3
- package/skills/hydrogen-local-https/SKILL.md +22 -7
- package/skills/hydrogen-request-handlers/SKILL.md +5 -5
- package/skills/hydrogen-request-handlers/references/nuxt.md +0 -6
- package/skills/hydrogen-setup/references/analytics.md +24 -23
- package/skills/hydrogen-setup/steps/1-inspect-app.md +1 -1
- package/skills/hydrogen-setup/steps/11-verify.md +1 -0
- package/skills/hydrogen-setup/steps/2-scaffold.md +22 -1
- package/skills/hydrogen-smoke-test/SKILL.md +2 -2
- package/skills/hydrogen-storefront-client/SKILL.md +4 -2
- package/skills/hydrogen-variant-form/SKILL.md +10 -4
- package/ts-plugin/package.json +5 -0
- package/dist/core/shopify-scripts/consent.mjs +0 -17
- package/dist/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/uuid.mjs +0 -17
- package/dist/core/shopify-scripts/utils/uuid.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/consent.mjs +0 -17
- package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/uuid.mjs +0 -17
- package/dist/development/core/shopify-scripts/utils/uuid.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ShopifyRoutesOptions, ShopifyScriptTagsOptions, ShopifyScriptsAnalyticsConfig, ShopifyScriptsI18n, ShopifyScriptsShop } from "../core/shopify-scripts/types.mjs";
|
|
2
|
-
import { ConsentConfig } from "../core/analytics/types.mjs";
|
|
2
|
+
import { ConsentConfig, ConsentSetup } from "../core/analytics/types.mjs";
|
|
3
3
|
import * as _$vue from "vue";
|
|
4
4
|
|
|
5
5
|
//#region src/vue/shopify-scripts.d.ts
|
|
@@ -26,7 +26,16 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
26
26
|
default: undefined;
|
|
27
27
|
};
|
|
28
28
|
consent: {
|
|
29
|
-
type: (new (...args: any[]) =>
|
|
29
|
+
type: (new (...args: any[]) => {
|
|
30
|
+
mode?: "no-banner";
|
|
31
|
+
setup?: never;
|
|
32
|
+
} | {
|
|
33
|
+
mode: "default-banner";
|
|
34
|
+
setup?: never;
|
|
35
|
+
} | {
|
|
36
|
+
mode: "custom-banner";
|
|
37
|
+
setup: ConsentSetup;
|
|
38
|
+
}) | (() => ConsentConfig | undefined);
|
|
30
39
|
default: undefined;
|
|
31
40
|
};
|
|
32
41
|
analytics: {
|
|
@@ -49,6 +58,10 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
49
58
|
type: () => boolean | undefined;
|
|
50
59
|
default: undefined;
|
|
51
60
|
};
|
|
61
|
+
account: {
|
|
62
|
+
type: () => boolean | undefined;
|
|
63
|
+
default: undefined;
|
|
64
|
+
};
|
|
52
65
|
shopifyAnalytics: {
|
|
53
66
|
type: BooleanConstructor;
|
|
54
67
|
default: undefined;
|
|
@@ -77,7 +90,16 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
77
90
|
default: undefined;
|
|
78
91
|
};
|
|
79
92
|
consent: {
|
|
80
|
-
type: (new (...args: any[]) =>
|
|
93
|
+
type: (new (...args: any[]) => {
|
|
94
|
+
mode?: "no-banner";
|
|
95
|
+
setup?: never;
|
|
96
|
+
} | {
|
|
97
|
+
mode: "default-banner";
|
|
98
|
+
setup?: never;
|
|
99
|
+
} | {
|
|
100
|
+
mode: "custom-banner";
|
|
101
|
+
setup: ConsentSetup;
|
|
102
|
+
}) | (() => ConsentConfig | undefined);
|
|
81
103
|
default: undefined;
|
|
82
104
|
};
|
|
83
105
|
analytics: {
|
|
@@ -100,6 +122,10 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
100
122
|
type: () => boolean | undefined;
|
|
101
123
|
default: undefined;
|
|
102
124
|
};
|
|
125
|
+
account: {
|
|
126
|
+
type: () => boolean | undefined;
|
|
127
|
+
default: undefined;
|
|
128
|
+
};
|
|
103
129
|
shopifyAnalytics: {
|
|
104
130
|
type: BooleanConstructor;
|
|
105
131
|
default: undefined;
|
|
@@ -109,6 +135,8 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
109
135
|
required: true;
|
|
110
136
|
};
|
|
111
137
|
}>> & Readonly<{}>, {
|
|
138
|
+
nonce: string;
|
|
139
|
+
account: boolean | undefined;
|
|
112
140
|
analytics: ShopifyScriptsAnalyticsConfig | undefined;
|
|
113
141
|
consent: ConsentConfig | undefined;
|
|
114
142
|
debug: {
|
|
@@ -116,7 +144,6 @@ declare const ShopifyScripts: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
|
116
144
|
} | undefined;
|
|
117
145
|
i18n: ShopifyScriptsI18n;
|
|
118
146
|
inbox: boolean | undefined;
|
|
119
|
-
nonce: string;
|
|
120
147
|
shopifyAnalytics: boolean;
|
|
121
148
|
navigate: any;
|
|
122
149
|
routes: any;
|
|
@@ -11,6 +11,7 @@ const consentProp = Object;
|
|
|
11
11
|
const analyticsProp = Object;
|
|
12
12
|
const debugProp = Object;
|
|
13
13
|
const inboxProp = Boolean;
|
|
14
|
+
const accountProp = Boolean;
|
|
14
15
|
const ShopifyScripts = defineComponent({
|
|
15
16
|
name: "ShopifyScripts",
|
|
16
17
|
props: {
|
|
@@ -50,6 +51,10 @@ const ShopifyScripts = defineComponent({
|
|
|
50
51
|
type: inboxProp,
|
|
51
52
|
default: void 0
|
|
52
53
|
},
|
|
54
|
+
account: {
|
|
55
|
+
type: accountProp,
|
|
56
|
+
default: void 0
|
|
57
|
+
},
|
|
53
58
|
shopifyAnalytics: {
|
|
54
59
|
type: Boolean,
|
|
55
60
|
default: void 0
|
|
@@ -62,6 +67,7 @@ const ShopifyScripts = defineComponent({
|
|
|
62
67
|
setup(props) {
|
|
63
68
|
onMounted(() => {
|
|
64
69
|
initializeShopifyScripts({
|
|
70
|
+
consent: props.consent,
|
|
65
71
|
navigate: props.navigate,
|
|
66
72
|
routes: props.routes,
|
|
67
73
|
webMcp: props.webMcp !== false
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shopify-scripts.mjs","names":[],"sources":["../../src/vue/shopify-scripts.ts"],"sourcesContent":["import { defineComponent, h, onMounted, type PropType } from \"vue\";\n\nimport {\n getShopifyScriptTags,\n initializeShopifyScripts,\n type ShopifyScriptsShop,\n type ShopifyScriptsI18n,\n type ShopifyScriptTagsOptions,\n type ShopifyRoutesOptions,\n} from \"../core/shopify-scripts/index\";\n\nexport type ShopifyScriptsProps = ShopifyScriptTagsOptions & {\n navigate?: ShopifyRoutesOptions[\"navigate\"];\n routes?: ShopifyRoutesOptions[\"routes\"];\n webMcp?: boolean;\n};\n\ntype CompletePropOptions<T> = {\n [K in keyof T]-?: unknown;\n};\n\nconst i18nProp: PropType<ShopifyScriptsI18n> = Object;\nconst navigateProp: PropType<NonNullable<ShopifyRoutesOptions[\"navigate\"]>> | null = null;\nconst routesProp: PropType<NonNullable<ShopifyRoutesOptions[\"routes\"]>> | null = null;\nconst shopProp: PropType<ShopifyScriptsShop> = Object;\nconst consentProp: PropType<ShopifyScriptTagsOptions[\"consent\"]> = Object;\nconst analyticsProp: PropType<ShopifyScriptTagsOptions[\"analytics\"]> = Object;\nconst debugProp: PropType<ShopifyScriptTagsOptions[\"debug\"]> = Object;\nconst inboxProp: PropType<ShopifyScriptTagsOptions[\"inbox\"]> = Boolean;\n\nexport const ShopifyScripts = defineComponent({\n name: \"ShopifyScripts\",\n props: {\n i18n: {\n type: i18nProp,\n default: undefined,\n },\n nonce: {\n type: String,\n default: undefined,\n },\n routes: {\n type: routesProp,\n default: undefined,\n },\n navigate: {\n type: navigateProp,\n default: undefined,\n },\n consent: {\n type: consentProp,\n default: undefined,\n },\n analytics: {\n type: analyticsProp,\n default: undefined,\n },\n debug: {\n type: debugProp,\n default: undefined,\n },\n webMcp: {\n type: Boolean,\n default: undefined,\n },\n inbox: {\n type: inboxProp,\n default: undefined,\n },\n shopifyAnalytics: {\n type: Boolean,\n default: undefined,\n },\n shop: {\n type: shopProp,\n required: true as const,\n },\n } satisfies CompletePropOptions<ShopifyScriptsProps>,\n setup(props) {\n onMounted(() => {\n void initializeShopifyScripts({\n navigate: props.navigate,\n routes: props.routes,\n webMcp: props.webMcp !== false,\n });\n });\n\n return () =>\n getShopifyScriptTags(props).tags.map(({ tagName, attributes, innerHTML }) =>\n h(tagName, {\n ...attributes,\n ...(innerHTML ? { innerHTML } : {}),\n }),\n );\n },\n});\n"],"mappings":";;;;;AAqBA,MAAM,WAAyC;AAC/C,MAAM,eAA+E;AACrF,MAAM,aAA2E;AACjF,MAAM,WAAyC;AAC/C,MAAM,cAA6D;AACnE,MAAM,gBAAiE;AACvE,MAAM,YAAyD;AAC/D,MAAM,YAAyD;
|
|
1
|
+
{"version":3,"file":"shopify-scripts.mjs","names":[],"sources":["../../src/vue/shopify-scripts.ts"],"sourcesContent":["import { defineComponent, h, onMounted, type PropType } from \"vue\";\n\nimport {\n getShopifyScriptTags,\n initializeShopifyScripts,\n type ShopifyScriptsShop,\n type ShopifyScriptsI18n,\n type ShopifyScriptTagsOptions,\n type ShopifyRoutesOptions,\n} from \"../core/shopify-scripts/index\";\n\nexport type ShopifyScriptsProps = ShopifyScriptTagsOptions & {\n navigate?: ShopifyRoutesOptions[\"navigate\"];\n routes?: ShopifyRoutesOptions[\"routes\"];\n webMcp?: boolean;\n};\n\ntype CompletePropOptions<T> = {\n [K in keyof T]-?: unknown;\n};\n\nconst i18nProp: PropType<ShopifyScriptsI18n> = Object;\nconst navigateProp: PropType<NonNullable<ShopifyRoutesOptions[\"navigate\"]>> | null = null;\nconst routesProp: PropType<NonNullable<ShopifyRoutesOptions[\"routes\"]>> | null = null;\nconst shopProp: PropType<ShopifyScriptsShop> = Object;\nconst consentProp: PropType<ShopifyScriptTagsOptions[\"consent\"]> = Object;\nconst analyticsProp: PropType<ShopifyScriptTagsOptions[\"analytics\"]> = Object;\nconst debugProp: PropType<ShopifyScriptTagsOptions[\"debug\"]> = Object;\nconst inboxProp: PropType<ShopifyScriptTagsOptions[\"inbox\"]> = Boolean;\nconst accountProp: PropType<ShopifyScriptTagsOptions[\"account\"]> = Boolean;\n\nexport const ShopifyScripts = defineComponent({\n name: \"ShopifyScripts\",\n props: {\n i18n: {\n type: i18nProp,\n default: undefined,\n },\n nonce: {\n type: String,\n default: undefined,\n },\n routes: {\n type: routesProp,\n default: undefined,\n },\n navigate: {\n type: navigateProp,\n default: undefined,\n },\n consent: {\n type: consentProp,\n default: undefined,\n },\n analytics: {\n type: analyticsProp,\n default: undefined,\n },\n debug: {\n type: debugProp,\n default: undefined,\n },\n webMcp: {\n type: Boolean,\n default: undefined,\n },\n inbox: {\n type: inboxProp,\n default: undefined,\n },\n account: {\n type: accountProp,\n default: undefined,\n },\n shopifyAnalytics: {\n type: Boolean,\n default: undefined,\n },\n shop: {\n type: shopProp,\n required: true as const,\n },\n } satisfies CompletePropOptions<ShopifyScriptsProps>,\n setup(props) {\n onMounted(() => {\n void initializeShopifyScripts({\n consent: props.consent,\n navigate: props.navigate,\n routes: props.routes,\n webMcp: props.webMcp !== false,\n });\n });\n\n return () =>\n getShopifyScriptTags(props).tags.map(({ tagName, attributes, innerHTML }) =>\n h(tagName, {\n ...attributes,\n ...(innerHTML ? { innerHTML } : {}),\n }),\n );\n },\n});\n"],"mappings":";;;;;AAqBA,MAAM,WAAyC;AAC/C,MAAM,eAA+E;AACrF,MAAM,aAA2E;AACjF,MAAM,WAAyC;AAC/C,MAAM,cAA6D;AACnE,MAAM,gBAAiE;AACvE,MAAM,YAAyD;AAC/D,MAAM,YAAyD;AAC/D,MAAM,cAA6D;AAEnE,MAAa,iBAAiB,gBAAgB;CAC5C,MAAM;CACN,OAAO;EACL,MAAM;GACJ,MAAM;GACN,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,SAAS;GACV;EACD,QAAQ;GACN,MAAM;GACN,SAAS;GACV;EACD,UAAU;GACR,MAAM;GACN,SAAS;GACV;EACD,SAAS;GACP,MAAM;GACN,SAAS;GACV;EACD,WAAW;GACT,MAAM;GACN,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,SAAS;GACV;EACD,QAAQ;GACN,MAAM;GACN,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,SAAS;GACV;EACD,SAAS;GACP,MAAM;GACN,SAAS;GACV;EACD,kBAAkB;GAChB,MAAM;GACN,SAAS;GACV;EACD,MAAM;GACJ,MAAM;GACN,UAAU;GACX;EACF;CACD,MAAM,OAAO;AACX,kBAAgB;AACd,GAAK,yBAAyB;IAC5B,SAAS,MAAM;IACf,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,QAAQ,MAAM,WAAW;IAC1B,CAAC;IACF;AAEF,eACE,qBAAqB,MAAM,CAAC,KAAK,KAAK,EAAE,SAAS,YAAY,gBAC3D,EAAE,SAAS;GACT,GAAG;GACH,GAAI,YAAY,EAAE,WAAW,GAAG,EAAE;GACnC,CAAC,CACH;;CAEN,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopify/hydrogen",
|
|
3
|
-
"version": "2026.10.0-preview.
|
|
3
|
+
"version": "2026.10.0-preview.4",
|
|
4
4
|
"description": "Framework-agnostic Shopify storefront SDK and agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shopify",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"bin",
|
|
18
18
|
"dist",
|
|
19
|
-
"skills"
|
|
19
|
+
"skills",
|
|
20
|
+
"ts-plugin"
|
|
20
21
|
],
|
|
21
22
|
"type": "module",
|
|
22
23
|
"sideEffects": false,
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"access": "public"
|
|
83
84
|
},
|
|
84
85
|
"dependencies": {
|
|
86
|
+
"cross-spawn": "7.0.6",
|
|
85
87
|
"gql.tada": "1.9.2"
|
|
86
88
|
},
|
|
87
89
|
"devDependencies": {
|
|
@@ -89,6 +91,7 @@
|
|
|
89
91
|
"@graphql-codegen/introspection": "^6.0.0",
|
|
90
92
|
"@graphql-codegen/typescript": "^6.0.0",
|
|
91
93
|
"@testing-library/react": "^16.3.2",
|
|
94
|
+
"@types/cross-spawn": "6.0.6",
|
|
92
95
|
"@types/react": "^19.2.14",
|
|
93
96
|
"@types/react-dom": "^19.2.3",
|
|
94
97
|
"@vue/test-utils": "^2.4.0",
|
|
@@ -9,17 +9,17 @@ description: >
|
|
|
9
9
|
|
|
10
10
|
# Storefront Analytics
|
|
11
11
|
|
|
12
|
-
Hydrogen's analytics bus owns the event API, event normalization, and consent-gated destination replay. ShopifyScripts owns Shopify consent setup
|
|
12
|
+
Hydrogen's analytics bus owns the event API, event normalization, and consent-gated destination replay. ShopifyScripts owns Shopify consent setup and analytics CDN loading. App code owns when to publish route/view events and when to call cart delta tracking.
|
|
13
13
|
|
|
14
14
|
## Framework References
|
|
15
15
|
|
|
16
16
|
Before wiring route events, check whether this skill has a reference file for the app's framework in `references/`. If one exists, read it and use that framework's route-change and lifecycle primitives. If there is no matching reference, keep the core singleton below and adapt page-view, product-view, collection-view, search-view, and cart tracking to the app's own route lifecycle.
|
|
17
17
|
|
|
18
|
-
Prerequisite: analytics depends on the same-origin SFAPI proxy (see `hydrogen-request-handlers`) so the browser can observe tracking values from Storefront API responses.
|
|
18
|
+
Prerequisite: analytics depends on the same-origin SFAPI proxy (see `hydrogen-request-handlers`) so the browser can observe tracking values from Storefront API responses. The proxy establishes the backend session cookies needed for analytics and checkout continuity; treat analytics as incomplete until it is wired. Key consent setup: Shopify Customer Privacy controls destination delivery in production. Register event consumers with `addDestination()`; live delivery and replay require analytics consent. Do not bypass Customer Privacy consent gating in production.
|
|
19
19
|
|
|
20
20
|
## Core Pattern
|
|
21
21
|
|
|
22
|
-
Render Shopify runtime scripts once from the app root/document head with the same resolved market used by Storefront API requests. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass the `shop` object matching the `ShopifyScriptsShop` type, and `i18n` matching the `ShopifyScriptsI18n` type and serialize them into ShopifyScripts. — declare them as constants with type annotations to keep errors located close to the source of writing. The analytics bus is created by default; pass `analytics` only for optional bus configuration such as `customData`. Do not pass market `country` or `language` through analytics consent config.
|
|
22
|
+
Render Shopify runtime scripts once from the app root/document head with the same resolved market used by Storefront API requests. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass the `shop` object matching the `ShopifyScriptsShop` type, and `i18n` matching the `ShopifyScriptsI18n` type and serialize them into ShopifyScripts. — declare them as constants with type annotations to keep errors located close to the source of writing. The analytics bus is created by default; pass `analytics` only for optional bus configuration such as `customData`. `customData` isn't added to events you publish; destinations read it via `getConfig().customData`. Do not pass market `country` or `language` through analytics consent config.
|
|
23
23
|
|
|
24
24
|
Read one browser-lazy singleton from the Shopify global created by `ShopifyScripts`:
|
|
25
25
|
|
|
@@ -66,7 +66,8 @@ Required product analytics fields include Shopify Product GID, ProductVariant GI
|
|
|
66
66
|
|
|
67
67
|
- Let Shopify Customer Privacy control destination delivery in production.
|
|
68
68
|
- Initialize one single bus per page lifetime through ShopifyScripts.
|
|
69
|
-
-
|
|
69
|
+
- Register event consumers with `addDestination()` and use the `subscribe` function provided to its setup callback.
|
|
70
|
+
- For third-party banners, use `consent: {mode: "custom-banner", setup}` and read [Custom consent providers](references/custom-consent.md) for readiness, synchronization, and framework integration.
|
|
70
71
|
- Render ShopifyScripts before route components publish events.
|
|
71
72
|
- Do not dispatch server-side analytics through this browser bus.
|
|
72
73
|
- Keep ShopifyScripts i18n aligned with the resolved market when the storefront uses markets.
|
|
@@ -75,6 +76,6 @@ Required product analytics fields include Shopify Product GID, ProductVariant GI
|
|
|
75
76
|
|
|
76
77
|
- Page view fires on initial load and client navigations.
|
|
77
78
|
- Product, collection, search, and cart view events fire once per relevant route data change.
|
|
78
|
-
- Confirmed cart data changes flow through `trackCartAnalytics(cartStore)` (React/Vue bindings: `useCartAnalytics()`)
|
|
79
|
+
- Confirmed cart data changes flow through `trackCartAnalytics(cartStore)` (React/Vue bindings: `useCartAnalytics()`).
|
|
79
80
|
- Consent-denied visitors do not deliver destination events.
|
|
80
81
|
- No browser module reads private or server-only env variables.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Custom Consent Providers
|
|
2
|
+
|
|
3
|
+
Read this when connecting a third-party banner or app-owned consent UI through `consent: {mode: "custom-banner", setup}`. Other consent modes do not accept `setup`.
|
|
4
|
+
|
|
5
|
+
## Setup Contract
|
|
6
|
+
|
|
7
|
+
Hydrogen calls `setup()` without arguments in the browser after Shopify's Customer Privacy API loads. Use `window.Shopify.customerPrivacy` directly. Destination events stay buffered from the inline bootstrap through hydration until the setup promise resolves, even if Shopify's regional defaults allow tracking.
|
|
8
|
+
|
|
9
|
+
The promise is the readiness signal. Subscribe to provider changes before reading saved consent, wait for a resolved provider choice or shopper interaction, and await `customerPrivacy.setTrackingConsent(choice)` before resolving setup. Registering a listener or showing a banner alone does not complete setup.
|
|
10
|
+
|
|
11
|
+
Map the provider's choice explicitly to all four boolean purposes in `ConsentPreferences`: `analytics`, `marketing`, `preferences`, and `sale_of_data`. Keep unknown provider state pending rather than submitting it as a denial. The provider owns its regional policy.
|
|
12
|
+
|
|
13
|
+
Once setup resolves, Hydrogen checks Shopify consent and replays allowed events or clears the buffer when denied. Later updates use the existing CTA consent events. This gate covers Hydrogen analytics destinations; independently loaded trackers need their own consent handling.
|
|
14
|
+
|
|
15
|
+
## Integration Pattern
|
|
16
|
+
|
|
17
|
+
The provider methods, category mapping, and error handler below are app-owned adapters, not Hydrogen or provider SDK APIs. `whenConsentResolved()` waits for saved consent or interaction; `getResolvedConsent()` reads the latest choice. The subscription continues synchronizing later acceptance, rejection, and preference changes.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<ShopifyScripts
|
|
21
|
+
shop={shop}
|
|
22
|
+
consent={{
|
|
23
|
+
mode: "custom-banner",
|
|
24
|
+
async setup() {
|
|
25
|
+
const customerPrivacy = window.Shopify!.customerPrivacy!;
|
|
26
|
+
const sync = async (choice) => {
|
|
27
|
+
await customerPrivacy.setTrackingConsent(mapProviderConsent(choice));
|
|
28
|
+
};
|
|
29
|
+
const unsubscribe = provider.subscribe((choice) => {
|
|
30
|
+
void sync(choice).catch(reportConsentError);
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await provider.whenConsentResolved();
|
|
34
|
+
await sync(provider.getResolvedConsent());
|
|
35
|
+
} catch (error) {
|
|
36
|
+
unsubscribe();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Keep the callback in client code and access browser-only provider globals inside setup. In Next.js App Router, define setup in an app-owned client component that renders `ShopifyScripts`; pass serializable `shop` and `i18n` data from the server. A function prop cannot cross the server-to-client boundary. Hydrogen serializes only the consent mode into HTML and does not expose setup to analytics destinations.
|
|
45
|
+
|
|
46
|
+
For frameworks using core script helpers, pass the same consent configuration to `getShopifyScriptTags()` / `renderShopifyScriptTags()` and `initializeShopifyScripts({consent})` during browser startup.
|
|
47
|
+
|
|
48
|
+
## Lifecycle and Failures
|
|
49
|
+
|
|
50
|
+
Setup runs once per analytics bus and resolves without a return value. The first callback remains active across component unmounts, remounts, and React Strict Mode. Hydrogen owns the bus for the page's lifetime; component unmounts do not tear down the consent integration.
|
|
51
|
+
|
|
52
|
+
If setup fails, Hydrogen logs the failure and leaves destination delivery blocked for that bus. Hydrogen does not automatically rerun setup. If the provider needs retries, perform them inside setup while its promise remains pending. Handle consent write failures in the app, including later updates, and release any subscriptions created by setup if it fails.
|
|
@@ -147,7 +147,7 @@ if (term) {
|
|
|
147
147
|
|
|
148
148
|
## Cart Updates
|
|
149
149
|
|
|
150
|
-
Use the `useCartAnalytics()` hook from the React binding — rendered inside `CartProvider
|
|
150
|
+
Use the `useCartAnalytics()` hook from the React binding — rendered inside the `CartProvider` returned by `createCartComponents()` (the React entry exports no standalone `CartProvider`), it calls `trackCartAnalytics(store)` in an effect and cleans up on unmount. Outside the binding, call `trackCartAnalytics(store)` once with the cart store from a client-only effect (`useEffect`) — never at cart-store creation time, since it throws when `window.Shopify.analytics` is missing during SSR. It subscribes itself, skips pending/revalidating/note updates, derives cart delta events from confirmed cart changes, returns an unsubscribe function, and uses the global analytics bus created by ShopifyScripts. Do not manually publish `product_added_to_cart`.
|
|
151
151
|
|
|
152
152
|
Publish `CART_VIEWED` when the cart page or drawer is viewed. The cart payload is `AnalyticsCart | null`: when a compatible cart is available, include `id`, `updatedAt`, and connection-shaped `lines`; otherwise pass `cart: null` instead of a partial cart.
|
|
153
153
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hydrogen-cart-metafields
|
|
3
|
+
description: >
|
|
4
|
+
Behavioral guide for reading and writing cart metafields with @shopify/hydrogen.
|
|
5
|
+
Use when adding, modifying, or reviewing cart metafield reads or writes, an
|
|
6
|
+
app-owned cart metafield route, cart-to-order metafield copying, or custom cart
|
|
7
|
+
data such as delivery instructions or gift preferences. Framework agnostic.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cart Metafields
|
|
11
|
+
|
|
12
|
+
Cart metafields store custom data on the cart (for example delivery instructions or a gift preference). They are **server-only**: the Storefront cart ajax API (Standard Actions) does not support them, so they never flow through Hydrogen's optimistic cart store the way line, discount, note, and attribute mutations do.
|
|
13
|
+
|
|
14
|
+
Because of that, **do not add metafields to the core cart action/intent model.** Compose three pieces Hydrogen already provides:
|
|
15
|
+
|
|
16
|
+
1. **Read** — a custom `CartFragment` on `createCartServerHandlers`, so metafields appear in cart state.
|
|
17
|
+
2. **Write** — an app-owned route (`createShopifyRouteHandler`) calling `cartMetafieldsSet` / `cartMetafieldDelete`.
|
|
18
|
+
3. **Re-sync** — `useCartActions().refresh()` after a successful write.
|
|
19
|
+
|
|
20
|
+
See `hydrogen-cart-ui` for cart state, `refresh()`, and form patterns, and `hydrogen-request-handlers` for registering the custom route.
|
|
21
|
+
|
|
22
|
+
## Golden rule: decouple the write from the read
|
|
23
|
+
|
|
24
|
+
The mutation route returns **only the mutation's `userErrors`** — it must never refetch and return the cart. Re-syncing is a separate `refresh()` call.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
POST /api/cart/metafields → cartMetafieldsSet → { userErrors } (truthful: success is success)
|
|
28
|
+
then, on success:
|
|
29
|
+
refresh() → re-reads the cart via CartFragment
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If the write and a refetch were coupled in one request, a blip on the read would mask a successful save as a "network error." Decoupled, a failed re-sync surfaces as `state.errors.network` (a soft refresh error) and preserves the saved cart — it is never reported as a failed save.
|
|
33
|
+
|
|
34
|
+
## Reading metafields
|
|
35
|
+
|
|
36
|
+
Select the metafields you need in a `CartFragment` named `CartFragment`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const CART_FRAGMENT = gql(`
|
|
40
|
+
fragment CartFragment on Cart {
|
|
41
|
+
metafields(identifiers: [{ namespace: "custom", key: "delivery_instructions" }]) {
|
|
42
|
+
namespace
|
|
43
|
+
key
|
|
44
|
+
type
|
|
45
|
+
value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
export const cartHandlers = createCartServerHandlers({ fragment: CART_FRAGMENT });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
They then appear in cart state. Read a **primitive** in the selector so referential equality can skip re-renders:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const instructions = useCart(
|
|
57
|
+
(state) =>
|
|
58
|
+
state.data.metafields?.find(
|
|
59
|
+
(m) => m?.namespace === "custom" && m.key === "delivery_instructions",
|
|
60
|
+
)?.value ?? "",
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Writing metafields (app-owned route)
|
|
65
|
+
|
|
66
|
+
Register a custom `POST` route with `createShopifyRouteHandler` in `handleShopifyRoutes`. Do **not** extend `/api/cart` or open up its intents.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
async function handleCartMetafieldsPost({ request, storefrontClient }) {
|
|
70
|
+
const body = parseRequest(await request.json()); // validate at the boundary
|
|
71
|
+
const cartId = getCartId(request); // read the owner from the cookie, never the client body
|
|
72
|
+
|
|
73
|
+
// Mutate an existing cart only; never create one here. Two concurrent empty-cart
|
|
74
|
+
// writes would each call cartCreate and race — the last Set-Cookie wins,
|
|
75
|
+
// orphaning one cart and dropping its write.
|
|
76
|
+
if (!cartId) {
|
|
77
|
+
return { type: "error", error: { code: "missing_cart", message: "No cart exists." } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (body.intent === "delete") {
|
|
81
|
+
const result = await storefrontClient.graphql(CART_METAFIELD_DELETE_MUTATION, {
|
|
82
|
+
variables: { input: { ownerId: cartId, key: body.key } },
|
|
83
|
+
});
|
|
84
|
+
return { type: "json", data: { userErrors: result.data?.cartMetafieldDelete?.userErrors ?? [] } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await storefrontClient.graphql(CART_METAFIELDS_SET_MUTATION, {
|
|
88
|
+
variables: { metafields: body.metafields.map((m) => ({ ...m, ownerId: cartId })) },
|
|
89
|
+
});
|
|
90
|
+
return { type: "json", data: { userErrors: result.data?.cartMetafieldsSet?.userErrors ?? [] } };
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **Existing cart** → `cartMetafieldsSet` with `ownerId` = the cart id from `getCartId(request)`.
|
|
95
|
+
- **No cart** → reject with a `missing_cart` error. Do **not** call `cartCreate` here: two concurrent empty-cart writes would each create a cart and race on the cookie, orphaning one and dropping its write. Require callers to add a line first.
|
|
96
|
+
- **Delete** → a single key via `cartMetafieldDelete`. The Storefront API deletes one metafield per call, so keep the contract single-key.
|
|
97
|
+
|
|
98
|
+
## Re-syncing the store
|
|
99
|
+
|
|
100
|
+
On success, call `refresh()`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const { refresh } = useCartActions();
|
|
104
|
+
// after the POST resolves ok:
|
|
105
|
+
refresh();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`refresh()` reconciles the `CartFragment` fields for the cart after a write. Never call `refresh()` after ordinary Hydrogen cart forms — their Standard Actions events already synchronize the store.
|
|
109
|
+
|
|
110
|
+
## Copying to the order
|
|
111
|
+
|
|
112
|
+
To copy a cart metafield onto the order at checkout, the store must define the metafield with the cart-to-order copyable capability:
|
|
113
|
+
https://shopify.dev/docs/apps/build/metafields/use-metafield-capabilities#cart-to-order-copyable
|
|
114
|
+
Without the definition the cart write still succeeds, but nothing is copied to the order.
|
|
115
|
+
|
|
116
|
+
## Rules
|
|
117
|
+
|
|
118
|
+
- **Server-only.** Never route metafields through Standard Actions or the optimistic cart store.
|
|
119
|
+
- **No core intents.** Do not add `metafields-set`/`metafield-delete` to the cart action model; use an app-owned route.
|
|
120
|
+
- **Mutation returns `userErrors` only.** Never refetch and return the cart in the same request.
|
|
121
|
+
- **Re-sync with `refresh()`** after success. Treat a refresh failure as a soft error (`errors.network`), not a failed save.
|
|
122
|
+
- **Inject `ownerId` server-side** from the cart cookie (`getCartId`). Never trust a client-supplied `ownerId`.
|
|
123
|
+
- **Require an existing cart.** Reject writes when no cart cookie is present; never call `cartCreate` in this route — it races on the cookie.
|
|
124
|
+
- **Validate the request body** at the route boundary before calling the Storefront API.
|
|
125
|
+
- **Delete is single-key**, matching `cartMetafieldDelete`.
|
|
126
|
+
|
|
127
|
+
## Putting it together
|
|
128
|
+
|
|
129
|
+
Three pieces in your app, each owning one concern:
|
|
130
|
+
|
|
131
|
+
1. **Cart handlers** — add the `CartFragment` (see *Reading metafields*) so the values load into cart state.
|
|
132
|
+
2. **Metafields route** — an app-owned `POST` handler (see *Writing metafields*) that runs `cartMetafieldsSet` / `cartMetafieldDelete` and returns only `userErrors`.
|
|
133
|
+
3. **Your component** — read with `useCart`, write via the route, then call `refresh()` on success (see *Re-syncing the store*).
|
|
@@ -41,7 +41,7 @@ On mutation:
|
|
|
41
41
|
|
|
42
42
|
When overlapping mutations make response snapshots ambiguous, the store sets `state.revalidating` to `true`. It starts one authoritative refresh after those mutations settle and clears the flag when the refresh completes or fails. Until then, server-derived values such as costs remain at their last trustworthy value. A refresh failure preserves the locally reconciled cart and appears in `errors.network`.
|
|
43
43
|
|
|
44
|
-
App-owned cart mutations outside Standard Actions do not emit the events the store normally observes. After such a mutation succeeds, call `CartStore.refresh()` directly or use the framework binding's `useCartActions().refresh()`. The refresh waits for active optimistic work, reconciles custom fragment fields from the configured cart endpoint, and loads the cart when none exists yet (e.g. one just created server-side). Do not call it after ordinary Hydrogen cart forms; their Standard Actions events already synchronize the store.
|
|
44
|
+
App-owned cart mutations outside Standard Actions do not emit the events the store normally observes. After such a mutation succeeds, call `CartStore.refresh()` directly or use the framework binding's `useCartActions().refresh()`. The refresh waits for active optimistic work, reconciles custom fragment fields from the configured cart endpoint, and loads the cart when none exists yet (e.g. one just created server-side). Do not call it after ordinary Hydrogen cart forms; their Standard Actions events already synchronize the store. For cart metafields specifically, see the `hydrogen-cart-metafields` skill.
|
|
45
45
|
|
|
46
46
|
The store supersedes keyed mutations for the same line, discount batch, note, or complete attribute list. Relative additions remain independent so every submitted quantity reaches the server; their projections are reconciled together without disabling controls.
|
|
47
47
|
|
|
@@ -141,6 +141,7 @@ Errors survive unrelated cart work and clear when a new mutation begins for the
|
|
|
141
141
|
|
|
142
142
|
### Form structure
|
|
143
143
|
|
|
144
|
+
- **Optionally render visible line-item attributes.** Each cart line may carry `attributes` — an array of `{ key, value }` pairs set during add-to-cart. If your storefront uses line-item attributes (e.g. engraving text, gift messages, custom options), render them below the variant subtitle as secondary text. Filter out attributes whose key starts with `_` (these are internal/private, set by apps or Shopify systems). The same variant with different attributes creates separate cart lines — the optimistic layer uses attributes in line identity matching.
|
|
144
145
|
- **Each line item is its own form.** This gives each line its own identity input and its own submit buttons. A single form containing multiple lines creates ambiguity about which line an action targets.
|
|
145
146
|
- **Each line item form must preserve the progressive-enhancement shape.** The rendered structure will vary by framework and design system, but every line item quantity form needs the same Hydrogen contract: `register("set")`, `register("lineId", { value: line.id })`, and a real editable quantity input using `register("quantity", { value: line.quantity, interactive: true })`. Increase, decrease, and remove buttons are additional submit controls, not replacements for the set intent or the quantity input.
|
|
146
147
|
- **The `set` control is a hidden submit button, not a hidden input.** `register("set")` already returns `{ type: "submit", hidden: true }`; render it on a `<button>`. Do not swap it for `<input type="hidden">` — that removes the submit button, so pressing Enter in the quantity input no longer submits the set action.
|
|
@@ -71,7 +71,7 @@ The Customer Account handlers own:
|
|
|
71
71
|
- `GET /account/refresh`
|
|
72
72
|
- `POST /account/logout`
|
|
73
73
|
|
|
74
|
-
Do not reimplement login, authorize, refresh, or logout unless the app needs custom behavior that preserves the same safeguards:
|
|
74
|
+
Do not reimplement login, authorize, refresh, or logout unless the app needs custom behavior that preserves the same safeguards: `return_to` limited to the same origin, same-origin logout POST checks, `cache-control: no-store`, committed session cookies on the returned response, and request-context response headers.
|
|
75
75
|
|
|
76
76
|
## Server Rendering
|
|
77
77
|
|
|
@@ -81,7 +81,7 @@ Server-rendered account UI must keep session reads and token refresh separate:
|
|
|
81
81
|
- Use `customerSession.getAccessToken()` before Customer Account GraphQL calls. It returns only a currently usable access token.
|
|
82
82
|
- If `isLoggedIn()` is true but `getAccessToken()` returns `undefined`, redirect once to `/account/refresh?return_to=...` from a dynamic server route, then retry the account page after the refresh route commits cookies.
|
|
83
83
|
- If `isLoggedIn()` is false, show login UI or redirect to `/account/login` instead of sending the user to `/account/refresh`.
|
|
84
|
-
- Include a one-shot refresh guard in `return_to`; if the refreshed page still has no usable access token, fall back to login or an account error state.
|
|
84
|
+
- Include a one-shot refresh guard in the `return_to` path, such as `/account?refreshed=1`; if the refreshed page still has no usable access token, fall back to login or an account error state.
|
|
85
85
|
- Server Components and layouts should only receive `ReadonlyCustomerSessionManager`, so they cannot call `getOrRefreshAccessToken()`.
|
|
86
86
|
|
|
87
87
|
Wrap header/account-link UI in the framework's streaming primitive when possible so the shell can render before session state resolves.
|
|
@@ -104,4 +104,4 @@ The same `@shopify/hydrogen/ts-plugin` and `hydrogen gql check` setup from the `
|
|
|
104
104
|
|
|
105
105
|
## Local OAuth
|
|
106
106
|
|
|
107
|
-
Customer Account OAuth needs a public HTTPS
|
|
107
|
+
Customer Account OAuth needs a public HTTPS origin. For local development, follow the `hydrogen-local-https` skill; its Vite plugin provisions a trusted certificate and pushes the callback, JavaScript origin, and logout URLs through Shopify CLI outside CI.
|
|
@@ -30,7 +30,7 @@ import { localHttps } from "@shopify/hydrogen/vite";
|
|
|
30
30
|
import { defineConfig } from "vite";
|
|
31
31
|
|
|
32
32
|
const httpsOptions = {
|
|
33
|
-
enabled: process.env.npm_lifecycle_event === "dev:https"
|
|
33
|
+
enabled: process.env.npm_lifecycle_event === "dev:https",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export default defineConfig({
|
|
@@ -57,7 +57,7 @@ Astro needs its own host and port in addition to the Vite plugin:
|
|
|
57
57
|
import { LOCAL_HTTPS_DEFAULTS, localHttps } from "@shopify/hydrogen/vite";
|
|
58
58
|
import { defineConfig } from "astro/config";
|
|
59
59
|
|
|
60
|
-
const enabled = process.env.npm_lifecycle_event === "dev:https"
|
|
60
|
+
const enabled = process.env.npm_lifecycle_event === "dev:https";
|
|
61
61
|
const httpsOptions = { enabled };
|
|
62
62
|
|
|
63
63
|
export default defineConfig({
|
|
@@ -79,7 +79,7 @@ import type { NuxtConfig } from "nuxt/schema";
|
|
|
79
79
|
type VitePlugin = NonNullable<NonNullable<NuxtConfig["vite"]>["plugins"]>[number];
|
|
80
80
|
|
|
81
81
|
const httpsOptions = {
|
|
82
|
-
enabled: process.env.npm_lifecycle_event === "dev:https"
|
|
82
|
+
enabled: process.env.npm_lifecycle_event === "dev:https",
|
|
83
83
|
};
|
|
84
84
|
const httpsPlugin = localHttps(httpsOptions);
|
|
85
85
|
|
|
@@ -100,23 +100,30 @@ import { defineConfig } from "@solidjs/start/config";
|
|
|
100
100
|
import { localHttps } from "@shopify/hydrogen/vite";
|
|
101
101
|
|
|
102
102
|
const httpsOptions = {
|
|
103
|
-
enabled: process.env.npm_lifecycle_event === "dev:https"
|
|
103
|
+
enabled: process.env.npm_lifecycle_event === "dev:https",
|
|
104
104
|
};
|
|
105
105
|
const httpsPlugin = localHttps(httpsOptions);
|
|
106
106
|
const devServer = httpsPlugin.api.getDevServerConfig();
|
|
107
107
|
|
|
108
|
+
// Vinxi reads its bind target from HOST/PORT when the listener starts, after
|
|
109
|
+
// this config has loaded. Setting them here works on every OS; a `HOST=...`
|
|
110
|
+
// prefix in the package script fails on Windows shells.
|
|
111
|
+
if (devServer) {
|
|
112
|
+
process.env.HOST = devServer.host;
|
|
113
|
+
process.env.PORT = String(devServer.port);
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
export default defineConfig({
|
|
109
117
|
server: { https: devServer?.https },
|
|
110
118
|
vite: { plugins: [httpsPlugin] },
|
|
111
119
|
});
|
|
112
120
|
```
|
|
113
121
|
|
|
114
|
-
Vinxi also needs its bind target and port on startup:
|
|
115
|
-
|
|
116
122
|
```json
|
|
117
123
|
{
|
|
118
124
|
"scripts": {
|
|
119
|
-
"dev
|
|
125
|
+
"dev": "vinxi dev",
|
|
126
|
+
"dev:https": "vinxi dev"
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
```
|
|
@@ -131,6 +138,14 @@ next dev --experimental-https --hostname local.tryhydrogen.dev --port 5173
|
|
|
131
138
|
|
|
132
139
|
## Shopify Admin
|
|
133
140
|
|
|
141
|
+
Outside CI, the `localHttps` Vite plugin uses the installed Shopify CLI to update Customer Account API settings when the server starts. Shopify CLI must include `@shopify/cli-hydrogen` 13.0.4 or later. If the project is not linked to a Hydrogen storefront, Shopify CLI starts the interactive linking flow before it pushes the callback, JavaScript origin, and logout URLs.
|
|
142
|
+
|
|
143
|
+
The plugin skips Shopify CLI in CI. It also falls back without stopping the development server when Shopify CLI is missing or outdated, linking is cancelled, or the settings cannot be pushed. In these cases, configure the values printed in the terminal manually.
|
|
144
|
+
|
|
145
|
+
Next.js does not use the Vite plugin, so configure its values manually too.
|
|
146
|
+
|
|
147
|
+
### Manual Configuration
|
|
148
|
+
|
|
134
149
|
In the Hydrogen or Headless sales channel, open the storefront's **Customer Account API settings** and configure:
|
|
135
150
|
|
|
136
151
|
```text
|
|
@@ -4,7 +4,7 @@ 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 permalinks, AJAX cart proxy routes, /admin redirects, Storefront URL redirects,
|
|
7
|
+
checkout redirects, cart and UCP buy permalinks, AJAX cart proxy routes, /admin redirects, Storefront URL redirects,
|
|
8
8
|
requestContext response-header propagation, or framework middleware,
|
|
9
9
|
not-found, and catch-all integration.
|
|
10
10
|
---
|
|
@@ -27,7 +27,7 @@ Request
|
|
|
27
27
|
-> framework 404 page
|
|
28
28
|
```
|
|
29
29
|
|
|
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}`, 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()`.
|
|
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
31
|
|
|
32
32
|
## Variant Id Redirects
|
|
33
33
|
|
|
@@ -44,7 +44,7 @@ const shopifyRoute = handleShopifyRoutes({
|
|
|
44
44
|
|
|
45
45
|
Pass `routeTemplates` to `handleShopifyRoutes` so product `?variant=` links can be recognized before framework routing. `pathPrefix` is inferred from `requestContext.i18n.pathPrefix`, so localized product URLs stay in the localized tree.
|
|
46
46
|
|
|
47
|
-
`handleShopifyRedirects` is a post-routing 404 check for `/admin`, configured standard route redirects, Storefront URL redirects, and same-origin query
|
|
47
|
+
`handleShopifyRedirects` is a post-routing 404 check for `/admin`, configured standard route redirects, Storefront URL redirects, and same-origin `return_to`/`redirect` query params. Do not run it on every request.
|
|
48
48
|
|
|
49
49
|
## Standard Route Redirects
|
|
50
50
|
|
|
@@ -73,7 +73,7 @@ const redirect = await handleShopifyRedirects({
|
|
|
73
73
|
- `handleShopifyRoutes` and `handleShopifyRedirects` apply request-context response headers before returning matched Shopify responses. Return those responses directly without calling `requestContext.applyResponseHeaders()` again.
|
|
74
74
|
- Link and submit to Customer Account routes (`/account/login`, `/account/authorize`, `/account/refresh`, `/account/logout`) with plain HTML `<a>`/`<form>`, never the framework's client-side navigation component (`<Form>`/`<Link>` in React Router, `next/link` in Next.js, `NuxtLink` in Nuxt). The login and logout handlers return raw HTTP redirects to external Shopify URLs, which client-nav cannot process.
|
|
75
75
|
- Apps authoring Customer Account API documents use the same packed Hydrogen TypeScript plugin as Storefront API documents. Add `@shopify/hydrogen/ts-plugin` to `tsconfig.json` `compilerOptions.plugins` and chain `hydrogen gql check` into a package script. Applies to every framework.
|
|
76
|
-
- For custom or framework-routed responses, commit session headers once at the final response boundary, append those headers, then call `requestContext.applyResponseHeaders(response.headers)` so SFAPI cookies
|
|
76
|
+
- For custom or framework-routed responses, commit session headers once at the final response boundary, append those headers, then call `requestContext.applyResponseHeaders(response.headers)` so eligible SFAPI cookies and personalized-response cache safety are applied.
|
|
77
77
|
- Wire this in production runtime code, not dev-only hooks. Only GraphiQL is dev-only.
|
|
78
78
|
|
|
79
79
|
## Imports
|
|
@@ -103,5 +103,5 @@ Run the app in dev and production modes, then check:
|
|
|
103
103
|
5. `GET /admin` returns a redirect to the shop admin URL.
|
|
104
104
|
6. An unknown path returns the framework 404 when no Shopify redirect exists.
|
|
105
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,
|
|
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
107
|
9. Authenticated Customer Account responses do not preserve public or CDN cache-control headers.
|
|
@@ -109,15 +109,9 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
109
109
|
copyHeader(event.node.res.getHeader("content-type"), (value) => {
|
|
110
110
|
headers.set("content-type", value);
|
|
111
111
|
});
|
|
112
|
-
copyHeader(event.node.res.getHeader("server-timing"), (value) => {
|
|
113
|
-
headers.append("server-timing", value);
|
|
114
|
-
});
|
|
115
112
|
|
|
116
113
|
requestContext.applyResponseHeaders(headers);
|
|
117
114
|
|
|
118
|
-
const serverTiming = headers.get("server-timing");
|
|
119
|
-
if (serverTiming) event.node.res.setHeader("server-timing", serverTiming);
|
|
120
|
-
|
|
121
115
|
const setCookies = headers.getSetCookie();
|
|
122
116
|
if (setCookies.length > 0) {
|
|
123
117
|
event.node.res.setHeader("set-cookie", [
|