diff --git a/.changeset/fresh-bottles-glow.md b/.changeset/fresh-bottles-glow.md deleted file mode 100644 index cb3581dc29..0000000000 --- a/.changeset/fresh-bottles-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'skeleton': patch ---- - -Remove initial redirect from product display page diff --git a/.changeset/lemon-beans-drum.md b/.changeset/lemon-beans-drum.md deleted file mode 100644 index b463651c73..0000000000 --- a/.changeset/lemon-beans-drum.md +++ /dev/null @@ -1,794 +0,0 @@ ---- -'skeleton': patch ---- - -Optional updates for the product route and product form to handle combined listing and 2000 variant limit. - -1. Update your SFAPI product query to bring in the new query fields: - -```diff -const PRODUCT_FRAGMENT = `#graphql - fragment Product on Product { - id - title - vendor - handle - descriptionHtml - description -+ encodedVariantExistence -+ encodedVariantAvailability - options { - name - optionValues { - name -+ firstSelectableVariant { -+ ...ProductVariant -+ } -+ swatch { -+ color -+ image { -+ previewImage { -+ url -+ } -+ } -+ } - } - } -- selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { -+ selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { -+ ...ProductVariant -+ } -+ adjacentVariants (selectedOptions: $selectedOptions) { -+ ...ProductVariant -+ } -- variants(first: 1) { -- nodes { -- ...ProductVariant -- } -- } - seo { - description - title - } - } - ${PRODUCT_VARIANT_FRAGMENT} -` as const; -``` - -2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable. - -```diff -function loadDeferredData({context, params}: LoaderFunctionArgs) { -+ // Put any API calls that is not critical to be available on first page render -+ // For example: product reviews, product recommendations, social feeds. -- // In order to show which variants are available in the UI, we need to query -- // all of them. But there might be a *lot*, so instead separate the variants -- // into it's own separate query that is deferred. So there's a brief moment -- // where variant options might show as available when they're not, but after -- // this deferred query resolves, the UI will update. -- const variants = context.storefront -- .query(VARIANTS_QUERY, { -- variables: {handle: params.handle!}, -- }) -- .catch((error) => { -- // Log query errors, but don't throw them so the page can still render -- console.error(error); -- return null; -- }); - -+ return {} -- return { -- variants, -- }; -} -``` - -3. Remove the redirect logic in the `loadCriticalData` function and completely remove `redirectToFirstVariant` function - -```diff -async function loadCriticalData({ - context, - params, - request, -}: LoaderFunctionArgs) { - const {handle} = params; - const {storefront} = context; - if (!handle) { - throw new Error('Expected product handle to be defined'); - } - const [{product}] = await Promise.all([ - storefront.query(PRODUCT_QUERY, { - variables: {handle, selectedOptions: getSelectedProductOptions(request)}, - }), - // Add other queries here, so that they are loaded in parallel - ]); - - if (!product?.id) { - throw new Response(null, {status: 404}); - } - -- const firstVariant = product.variants.nodes[0]; -- const firstVariantIsDefault = Boolean( -- firstVariant.selectedOptions.find( -- (option: SelectedOption) => -- option.name === 'Title' && option.value === 'Default Title', -- ), -- ); - -- if (firstVariantIsDefault) { -- product.selectedVariant = firstVariant; -- } else { -- // if no selected variant was returned from the selected options, -- // we redirect to the first variant's url with it's selected options applied -- if (!product.selectedVariant) { -- throw redirectToFirstVariant({product, request}); -- } -- } - - return { - product, - }; -} - -... - -- function redirectToFirstVariant({ -- product, -- request, -- }: { -- product: ProductFragment; -- request: Request; -- }) { -- ... -- } -``` - -4. Update the `Product` component to use the new data fields. - -```diff -import { - getSelectedProductOptions, - Analytics, - useOptimisticVariant, -+ getAdjacentAndFirstAvailableVariants, -} from '@shopify/hydrogen'; - -export default function Product() { -+ const {product} = useLoaderData(); -- const {product, variants} = useLoaderData(); - -+ // Optimistically selects a variant with given available variant information -+ const selectedVariant = useOptimisticVariant( -+ product.selectedOrFirstAvailableVariant, -+ getAdjacentAndFirstAvailableVariants(product), -+ ); -- const selectedVariant = useOptimisticVariant( -- product.selectedVariant, -- variants, -- ); -``` - -5. Handle missing search query param in url from selecting a first variant - -```diff -import { - getSelectedProductOptions, - Analytics, - useOptimisticVariant, - getAdjacentAndFirstAvailableVariants, -+ useSelectedOptionInUrlParam, -} from '@shopify/hydrogen'; - -export default function Product() { - const {product} = useLoaderData(); - - // Optimistically selects a variant with given available variant information - const selectedVariant = useOptimisticVariant( - product.selectedOrFirstAvailableVariant, - getAdjacentAndFirstAvailableVariants(product), - ); - -+ // Sets the search param to the selected variant without navigation -+ // only when no search params are set in the url -+ useSelectedOptionInUrlParam(selectedVariant.selectedOptions); -``` - -6. Get the product options array using `getProductOptions` - -```diff -import { - getSelectedProductOptions, - Analytics, - useOptimisticVariant, -+ getProductOptions, - getAdjacentAndFirstAvailableVariants, - useSelectedOptionInUrlParam, -} from '@shopify/hydrogen'; - -export default function Product() { - const {product} = useLoaderData(); - - // Optimistically selects a variant with given available variant information - const selectedVariant = useOptimisticVariant( - product.selectedOrFirstAvailableVariant, - getAdjacentAndFirstAvailableVariants(product), - ); - - // Sets the search param to the selected variant without navigation - // only when no search params are set in the url - useSelectedOptionInUrlParam(selectedVariant.selectedOptions); - -+ // Get the product options array -+ const productOptions = getProductOptions({ -+ ...product, -+ selectedOrFirstAvailableVariant: selectedVariant, -+ }); -``` - -7. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for. - -```diff -export default function Product() { - - ... - - return ( - ... -+ -- -- } -- > -- -- {(data) => ( -- -- )} -- -- -``` - -8. Update the `ProductForm` component. - -```tsx -import {Link, useNavigate} from '@remix-run/react'; -import {type MappedProductOptions} from '@shopify/hydrogen'; -import type { - Maybe, - ProductOptionValueSwatch, -} from '@shopify/hydrogen/storefront-api-types'; -import {AddToCartButton} from './AddToCartButton'; -import {useAside} from './Aside'; -import type {ProductFragment} from 'storefrontapi.generated'; - -export function ProductForm({ - productOptions, - selectedVariant, -}: { - productOptions: MappedProductOptions[]; - selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; -}) { - const navigate = useNavigate(); - const {open} = useAside(); - return ( -
- {productOptions.map((option) => ( -
-
{option.name}
-
- {option.optionValues.map((value) => { - const { - name, - handle, - variantUriQuery, - selected, - available, - exists, - isDifferentProduct, - swatch, - } = value; - - if (isDifferentProduct) { - // SEO - // When the variant is a combined listing child product - // that leads to a different url, we need to render it - // as an anchor tag - return ( - - - - ); - } else { - // SEO - // When the variant is an update to the search param, - // render it as a button with javascript navigating to - // the variant so that SEO bots do not index these as - // duplicated links - return ( - - ); - } - })} -
-
-
- ))} - { - open('cart'); - }} - lines={ - selectedVariant - ? [ - { - merchandiseId: selectedVariant.id, - quantity: 1, - selectedVariant, - }, - ] - : [] - } - > - {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} - -
- ); -} - -function ProductOptionSwatch({ - swatch, - name, -}: { - swatch?: Maybe | undefined; - name: string; -}) { - const image = swatch?.image?.previewImage?.url; - const color = swatch?.color; - - if (!image && !color) return name; - - return ( -
- {!!image && {name}} -
- ); -} -``` - -9. Update `app.css` - -```diff -+ /* -+ * -------------------------------------------------- -+ * Non anchor links -+ * -------------------------------------------------- -+ */ -+ .link:hover { -+ text-decoration: underline; -+ cursor: pointer; -+ } - -... - -- .product-options-item { -+ .product-options-item, -+ .product-options-item:disabled { -+ padding: 0.25rem 0.5rem; -+ background-color: transparent; -+ font-size: 1rem; -+ font-family: inherit; -+ } - -+ .product-option-label-swatch { -+ width: 1.25rem; -+ height: 1.25rem; -+ margin: 0.25rem 0; -+ } - -+ .product-option-label-swatch img { -+ width: 100%; -+ } -``` - -10. Update `lib/variants.ts` - -Make `useVariantUrl` and `getVariantUrl` flexible to supplying a selected option param - -```diff -export function useVariantUrl( - handle: string, -- selectedOptions: SelectedOption[], -+ selectedOptions?: SelectedOption[], -) { - const {pathname} = useLocation(); - - return useMemo(() => { - return getVariantUrl({ - handle, - pathname, - searchParams: new URLSearchParams(), - selectedOptions, - }); - }, [handle, selectedOptions, pathname]); -} -export function getVariantUrl({ - handle, - pathname, - searchParams, - selectedOptions, -}: { - handle: string; - pathname: string; - searchParams: URLSearchParams; -- selectedOptions: SelectedOption[]; -+ selectedOptions?: SelectedOption[], -}) { - const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); - const isLocalePathname = match && match.length > 0; - const path = isLocalePathname - ? `${match![0]}products/${handle}` - : `/products/${handle}`; - -- selectedOptions.forEach((option) => { -+ selectedOptions?.forEach((option) => { - searchParams.set(option.name, option.value); - }); -``` - -11. Update `routes/collections.$handle.tsx` - -We no longer need to query for the variants since product route can efficiently -obtain the first available variants. Update the code to reflect that: - -```diff -const PRODUCT_ITEM_FRAGMENT = `#graphql - fragment MoneyProductItem on MoneyV2 { - amount - currencyCode - } - fragment ProductItem on Product { - id - handle - title - featuredImage { - id - altText - url - width - height - } - priceRange { - minVariantPrice { - ...MoneyProductItem - } - maxVariantPrice { - ...MoneyProductItem - } - } -- variants(first: 1) { -- nodes { -- selectedOptions { -- name -- value -- } -- } -- } - } -` as const; -``` - -and remove the variant reference -```diff -function ProductItem({ - product, - loading, -}: { - product: ProductItemFragment; - loading?: 'eager' | 'lazy'; -}) { -- const variant = product.variants.nodes[0]; -- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); -+ const variantUrl = useVariantUrl(product.handle); - return ( -``` - -12. Update `routes/collections.all.tsx` - -Same reasoning as `collections.$handle.tsx` - -```diff -const PRODUCT_ITEM_FRAGMENT = `#graphql - fragment MoneyProductItem on MoneyV2 { - amount - currencyCode - } - fragment ProductItem on Product { - id - handle - title - featuredImage { - id - altText - url - width - height - } - priceRange { - minVariantPrice { - ...MoneyProductItem - } - maxVariantPrice { - ...MoneyProductItem - } - } -- variants(first: 1) { -- nodes { -- selectedOptions { -- name -- value -- } -- } -- } - } -` as const; -``` - -and remove the variant reference -```diff -function ProductItem({ - product, - loading, -}: { - product: ProductItemFragment; - loading?: 'eager' | 'lazy'; -}) { -- const variant = product.variants.nodes[0]; -- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); -+ const variantUrl = useVariantUrl(product.handle); - return ( -``` - -13. Update `routes/search.tsx` - -Instead of using the first variant, use `selectedOrFirstAvailableVariant` - -```diff -const SEARCH_PRODUCT_FRAGMENT = `#graphql - fragment SearchProduct on Product { - __typename - handle - id - publishedAt - title - trackingParameters - vendor -- variants(first: 1) { -- nodes { -+ selectedOrFirstAvailableVariant( -+ selectedOptions: [] -+ ignoreUnknownOptions: true -+ caseInsensitiveMatch: true -+ ) { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } - compareAtPrice { - amount - currencyCode - } - selectedOptions { - name - value - } - product { - handle - title - } - } -- } - } -` as const; -``` - -```diff -const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql - fragment PredictiveProduct on Product { - __typename - id - title - handle - trackingParameters -- variants(first: 1) { -- nodes { -+ selectedOrFirstAvailableVariant( -+ selectedOptions: [] -+ ignoreUnknownOptions: true -+ caseInsensitiveMatch: true -+ ) { - id - image { - url - altText - width - height - } - price { - amount - currencyCode - } - } -- } - } -``` - -14. Update `components/SearchResults.tsx` - -```diff -function SearchResultsProducts({ - term, - products, -}: PartialSearchResult<'products'>) { - if (!products?.nodes.length) { - return null; - } - - return ( -
-

Products

- - {({nodes, isLoading, NextLink, PreviousLink}) => { - const ItemsMarkup = nodes.map((product) => { - const productUrl = urlWithTrackingParams({ - baseUrl: `/products/${product.handle}`, - trackingParams: product.trackingParameters, - term, - }); - -+ const price = product?.selectedOrFirstAvailableVariant?.price; -+ const image = product?.selectedOrFirstAvailableVariant?.image; - - return ( -
- -- {product.variants.nodes[0].image && ( -+ {image && ( - {product.title} - )} -
-

{product.title}

- -- -+ {price && -+ -+ } - -
- -
- ); - }); -``` - -15. Update `components/SearchResultsPredictive.tsx` - -```diff -function SearchResultsPredictiveProducts({ - term, - products, - closeSearch, -}: PartialPredictiveSearchResult<'products'>) { - if (!products.length) return null; - - return ( -
-
Products
-
    - {products.map((product) => { - const productUrl = urlWithTrackingParams({ - baseUrl: `/products/${product.handle}`, - trackingParams: product.trackingParameters, - term: term.current, - }); - -+ const price = product?.selectedOrFirstAvailableVariant?.price; -- const image = product?.variants?.nodes?.[0].image; -+ const image = product?.selectedOrFirstAvailableVariant?.image; - return ( -
  • - - {image && ( - {image.altText - )} -
    -

    {product.title}

    - -- {product?.variants?.nodes?.[0].price && ( -+ {price && ( -- -+ - )} - -
    - -
  • - ); - })} -
-
- ); -} -``` diff --git a/.changeset/metal-wasps-collect.md b/.changeset/metal-wasps-collect.md deleted file mode 100644 index 9d7e8e3138..0000000000 --- a/.changeset/metal-wasps-collect.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@shopify/hydrogen': patch ---- - -Added namespace support to prevent conflicts when using multiple Pagination components: -- New optional `namespace` prop for the `` component -- New optional `namespace` option for `getPaginationVariables()` utility -- When specified, pagination URL parameters are prefixed with the namespace (e.g., `products_cursor` instead of `cursor`) -- Maintains backwards compatibility when no namespace is provided diff --git a/.changeset/nervous-olives-nail.md b/.changeset/nervous-olives-nail.md deleted file mode 100644 index 9554a04e86..0000000000 --- a/.changeset/nervous-olives-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'skeleton': patch ---- - -Update `Aside` to have an accessible close button label diff --git a/.changeset/nervous-poets-fly.md b/.changeset/nervous-poets-fly.md deleted file mode 100644 index 036d21e16f..0000000000 --- a/.changeset/nervous-poets-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'skeleton': patch ---- - -Fix cart route so that it works with no-js diff --git a/.changeset/poor-crabs-invite.md b/.changeset/poor-crabs-invite.md deleted file mode 100644 index 928c2dd908..0000000000 --- a/.changeset/poor-crabs-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/cli-hydrogen': patch ---- - -Attach Hydrogen version metadata to deployments diff --git a/.changeset/thick-rockets-pay.md b/.changeset/thick-rockets-pay.md deleted file mode 100644 index 864f2af7df..0000000000 --- a/.changeset/thick-rockets-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'skeleton': patch ---- - -Bump Shopify cli version diff --git a/.changeset/three-cows-shave.md b/.changeset/three-cows-shave.md deleted file mode 100644 index c742a88a4b..0000000000 --- a/.changeset/three-cows-shave.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@shopify/hydrogen-react': patch -'@shopify/hydrogen': patch ---- - -Introduce `getProductOptions`, `getAdjacentAndFirstAvailableVariants`, `useSelectedOptionInUrlParam`, and `mapSelectedProductOptionToObject` to support combined listing products and products with 2000 variants limit. diff --git a/.changeset/tricky-mails-peel.md b/.changeset/tricky-mails-peel.md deleted file mode 100644 index 28cc7a5037..0000000000 --- a/.changeset/tricky-mails-peel.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@shopify/hydrogen': patch ---- - -Add params to override the login and authorize paths: - -```ts -const hydrogenContext = createHydrogenContext({ - // ... - customerAccount: { - loginPath = '/account/login', - authorizePath = '/account/authorize', - defaultRedirectPath = '/account', - }, -}); -``` diff --git a/.changeset/twelve-carrots-switch.md b/.changeset/twelve-carrots-switch.md deleted file mode 100644 index fe1616fe44..0000000000 --- a/.changeset/twelve-carrots-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/hydrogen': patch ---- - -Add `selectedVariant` prop to the `VariantSelector` to use for the initial state if no URL parameters are set diff --git a/.changeset/wild-nails-burn.md b/.changeset/wild-nails-burn.md deleted file mode 100644 index b4b74f9ca1..0000000000 --- a/.changeset/wild-nails-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@shopify/create-hydrogen': patch ---- - -Update to skeleton template diff --git a/examples/express/package.json b/examples/express/package.json index dded4dd97f..c15e266755 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -14,7 +14,7 @@ "@remix-run/node": "^2.13.1", "@remix-run/react": "^2.13.1", "@remix-run/server-runtime": "^2.13.1", - "@shopify/hydrogen": "2024.10.0", + "@shopify/hydrogen": "2024.10.1", "compression": "^1.7.4", "cross-env": "^7.0.3", "express": "^4.19.2", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index bddbaa03b4..57bb4c6648 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @shopify/cli-hydrogen +## 9.0.3 + +### Patch Changes + +- Attach Hydrogen version metadata to deployments ([#2645](https://github.com/Shopify/hydrogen/pull/2645)) by [@benwolfram](https://github.com/benwolfram) + ## 9.0.2 ### Patch Changes diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index c482ec024c..1fa1b86cbc 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1748,5 +1748,5 @@ ] } }, - "version": "9.0.2" + "version": "9.0.3" } \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 1502db0265..03992a369e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -4,7 +4,7 @@ "access": "public", "@shopify:registry": "https://registry.npmjs.org" }, - "version": "9.0.2", + "version": "9.0.3", "license": "MIT", "type": "module", "scripts": { diff --git a/packages/create-hydrogen/CHANGELOG.md b/packages/create-hydrogen/CHANGELOG.md index b4a420c009..1f6cf3846d 100644 --- a/packages/create-hydrogen/CHANGELOG.md +++ b/packages/create-hydrogen/CHANGELOG.md @@ -1,5 +1,11 @@ # @shopify/create-hydrogen +## 5.0.12 + +### Patch Changes + +- Update to skeleton template by [@wizardlyhel](https://github.com/wizardlyhel) + ## 5.0.11 ### Patch Changes diff --git a/packages/create-hydrogen/package.json b/packages/create-hydrogen/package.json index d3705da10e..384b8f0e5c 100644 --- a/packages/create-hydrogen/package.json +++ b/packages/create-hydrogen/package.json @@ -5,7 +5,7 @@ "@shopify:registry": "https://registry.npmjs.org" }, "license": "MIT", - "version": "5.0.11", + "version": "5.0.12", "type": "module", "scripts": { "build": "tsup --clean", diff --git a/packages/hydrogen-react/CHANGELOG.md b/packages/hydrogen-react/CHANGELOG.md index 408d1208a9..cb7c66d9b4 100644 --- a/packages/hydrogen-react/CHANGELOG.md +++ b/packages/hydrogen-react/CHANGELOG.md @@ -1,5 +1,11 @@ # @shopify/hydrogen-react +## 2024.10.1 + +### Patch Changes + +- Introduce `getProductOptions`, `getAdjacentAndFirstAvailableVariants`, `useSelectedOptionInUrlParam`, and `mapSelectedProductOptionToObject` to support combined listing products and products with 2000 variants limit. ([#2659](https://github.com/Shopify/hydrogen/pull/2659)) by [@wizardlyhel](https://github.com/wizardlyhel) + ## 2024.10.0 ### Patch Changes diff --git a/packages/hydrogen-react/package.json b/packages/hydrogen-react/package.json index 86684f1777..6aff83aac7 100644 --- a/packages/hydrogen-react/package.json +++ b/packages/hydrogen-react/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/hydrogen-react", - "version": "2024.10.0", + "version": "2024.10.1", "description": "React components, hooks, and utilities for creating custom Shopify storefronts", "homepage": "https://github.com/Shopify/hydrogen/tree/main/packages/hydrogen-react", "license": "MIT", diff --git a/packages/hydrogen/CHANGELOG.md b/packages/hydrogen/CHANGELOG.md index dc46aca5f4..ecda4c634c 100644 --- a/packages/hydrogen/CHANGELOG.md +++ b/packages/hydrogen/CHANGELOG.md @@ -1,5 +1,36 @@ # @shopify/hydrogen +## 2024.10.1 + +### Patch Changes + +- Added namespace support to prevent conflicts when using multiple Pagination components: ([#2649](https://github.com/Shopify/hydrogen/pull/2649)) by [@scottdixon](https://github.com/scottdixon) + + - New optional `namespace` prop for the `` component + - New optional `namespace` option for `getPaginationVariables()` utility + - When specified, pagination URL parameters are prefixed with the namespace (e.g., `products_cursor` instead of `cursor`) + - Maintains backwards compatibility when no namespace is provided + +- Introduce `getProductOptions`, `getAdjacentAndFirstAvailableVariants`, `useSelectedOptionInUrlParam`, and `mapSelectedProductOptionToObject` to support combined listing products and products with 2000 variants limit. ([#2659](https://github.com/Shopify/hydrogen/pull/2659)) by [@wizardlyhel](https://github.com/wizardlyhel) + +- Add params to override the login and authorize paths: ([#2648](https://github.com/Shopify/hydrogen/pull/2648)) by [@blittle](https://github.com/blittle) + + ```ts + const hydrogenContext = createHydrogenContext({ + // ... + customerAccount: { + loginPath = '/account/login', + authorizePath = '/account/authorize', + defaultRedirectPath = '/account', + }, + }); + ``` + +- Add `selectedVariant` prop to the `VariantSelector` to use for the initial state if no URL parameters are set ([#2643](https://github.com/Shopify/hydrogen/pull/2643)) by [@scottdixon](https://github.com/scottdixon) + +- Updated dependencies [[`a57d5267`](https://github.com/Shopify/hydrogen/commit/a57d5267daa2f22fe1a426fb9f62c242957f95b6)]: + - @shopify/hydrogen-react@2024.10.1 + ## 2024.10.0 ### Patch Changes diff --git a/packages/hydrogen/package.json b/packages/hydrogen/package.json index a57642b8e7..e16a7a5236 100644 --- a/packages/hydrogen/package.json +++ b/packages/hydrogen/package.json @@ -5,7 +5,7 @@ "@shopify:registry": "https://registry.npmjs.org" }, "type": "module", - "version": "2024.10.0", + "version": "2024.10.1", "license": "MIT", "main": "dist/index.cjs", "module": "dist/production/index.js", @@ -63,7 +63,7 @@ "dist" ], "dependencies": { - "@shopify/hydrogen-react": "2024.10.0", + "@shopify/hydrogen-react": "2024.10.1", "content-security-policy-builder": "^2.2.0", "source-map-support": "^0.5.21", "type-fest": "^4.26.1", diff --git a/packages/hydrogen/src/version.ts b/packages/hydrogen/src/version.ts index 43216710af..d5e917e68c 100644 --- a/packages/hydrogen/src/version.ts +++ b/packages/hydrogen/src/version.ts @@ -1 +1 @@ -export const LIB_VERSION = '2024.10.0'; +export const LIB_VERSION = '2024.10.1'; diff --git a/templates/skeleton/CHANGELOG.md b/templates/skeleton/CHANGELOG.md index bda4d763d9..080749ddd0 100644 --- a/templates/skeleton/CHANGELOG.md +++ b/templates/skeleton/CHANGELOG.md @@ -1,5 +1,813 @@ # skeleton +## 2024.10.2 + +### Patch Changes + +- Remove initial redirect from product display page ([#2643](https://github.com/Shopify/hydrogen/pull/2643)) by [@scottdixon](https://github.com/scottdixon) + +- Optional updates for the product route and product form to handle combined listing and 2000 variant limit. ([#2659](https://github.com/Shopify/hydrogen/pull/2659)) by [@wizardlyhel](https://github.com/wizardlyhel) + + 1. Update your SFAPI product query to bring in the new query fields: + + ```diff + const PRODUCT_FRAGMENT = `#graphql + fragment Product on Product { + id + title + vendor + handle + descriptionHtml + description + + encodedVariantExistence + + encodedVariantAvailability + options { + name + optionValues { + name + + firstSelectableVariant { + + ...ProductVariant + + } + + swatch { + + color + + image { + + previewImage { + + url + + } + + } + + } + } + } + - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + + ...ProductVariant + + } + + adjacentVariants (selectedOptions: $selectedOptions) { + + ...ProductVariant + + } + - variants(first: 1) { + - nodes { + - ...ProductVariant + - } + - } + seo { + description + title + } + } + ${PRODUCT_VARIANT_FRAGMENT} + ` as const; + ``` + + 2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable. + + ```diff + function loadDeferredData({context, params}: LoaderFunctionArgs) { + + // Put any API calls that is not critical to be available on first page render + + // For example: product reviews, product recommendations, social feeds. + - // In order to show which variants are available in the UI, we need to query + - // all of them. But there might be a *lot*, so instead separate the variants + - // into it's own separate query that is deferred. So there's a brief moment + - // where variant options might show as available when they're not, but after + - // this deferred query resolves, the UI will update. + - const variants = context.storefront + - .query(VARIANTS_QUERY, { + - variables: {handle: params.handle!}, + - }) + - .catch((error) => { + - // Log query errors, but don't throw them so the page can still render + - console.error(error); + - return null; + - }); + + + return {} + - return { + - variants, + - }; + } + ``` + + 3. Remove the redirect logic in the `loadCriticalData` function and completely remove `redirectToFirstVariant` function + + ```diff + async function loadCriticalData({ + context, + params, + request, + }: LoaderFunctionArgs) { + const {handle} = params; + const {storefront} = context; + if (!handle) { + throw new Error('Expected product handle to be defined'); + } + const [{product}] = await Promise.all([ + storefront.query(PRODUCT_QUERY, { + variables: {handle, selectedOptions: getSelectedProductOptions(request)}, + }), + // Add other queries here, so that they are loaded in parallel + ]); + + if (!product?.id) { + throw new Response(null, {status: 404}); + } + + - const firstVariant = product.variants.nodes[0]; + - const firstVariantIsDefault = Boolean( + - firstVariant.selectedOptions.find( + - (option: SelectedOption) => + - option.name === 'Title' && option.value === 'Default Title', + - ), + - ); + + - if (firstVariantIsDefault) { + - product.selectedVariant = firstVariant; + - } else { + - // if no selected variant was returned from the selected options, + - // we redirect to the first variant's url with it's selected options applied + - if (!product.selectedVariant) { + - throw redirectToFirstVariant({product, request}); + - } + - } + + return { + product, + }; + } + + ... + + - function redirectToFirstVariant({ + - product, + - request, + - }: { + - product: ProductFragment; + - request: Request; + - }) { + - ... + - } + ``` + + 4. Update the `Product` component to use the new data fields. + + ```diff + import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, + + getAdjacentAndFirstAvailableVariants, + } from '@shopify/hydrogen'; + + export default function Product() { + + const {product} = useLoaderData(); + - const {product, variants} = useLoaderData(); + + + // Optimistically selects a variant with given available variant information + + const selectedVariant = useOptimisticVariant( + + product.selectedOrFirstAvailableVariant, + + getAdjacentAndFirstAvailableVariants(product), + + ); + - const selectedVariant = useOptimisticVariant( + - product.selectedVariant, + - variants, + - ); + ``` + + 5. Handle missing search query param in url from selecting a first variant + + ```diff + import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, + getAdjacentAndFirstAvailableVariants, + + useSelectedOptionInUrlParam, + } from '@shopify/hydrogen'; + + export default function Product() { + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); + + + // Sets the search param to the selected variant without navigation + + // only when no search params are set in the url + + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + ``` + + 6. Get the product options array using `getProductOptions` + + ```diff + import { + getSelectedProductOptions, + Analytics, + useOptimisticVariant, + + getProductOptions, + getAdjacentAndFirstAvailableVariants, + useSelectedOptionInUrlParam, + } from '@shopify/hydrogen'; + + export default function Product() { + const {product} = useLoaderData(); + + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); + + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + + + // Get the product options array + + const productOptions = getProductOptions({ + + ...product, + + selectedOrFirstAvailableVariant: selectedVariant, + + }); + ``` + + 7. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for. + + ```diff + export default function Product() { + + ... + + return ( + ... + + + - + - } + - > + - + - {(data) => ( + - + - )} + - + - + ``` + + 8. Update the `ProductForm` component. + + ```tsx + import {Link, useNavigate} from '@remix-run/react'; + import {type MappedProductOptions} from '@shopify/hydrogen'; + import type { + Maybe, + ProductOptionValueSwatch, + } from '@shopify/hydrogen/storefront-api-types'; + import {AddToCartButton} from './AddToCartButton'; + import {useAside} from './Aside'; + import type {ProductFragment} from 'storefrontapi.generated'; + + export function ProductForm({ + productOptions, + selectedVariant, + }: { + productOptions: MappedProductOptions[]; + selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; + }) { + const navigate = useNavigate(); + const {open} = useAside(); + return ( +
+ {productOptions.map((option) => ( +
+
{option.name}
+
+ {option.optionValues.map((value) => { + const { + name, + handle, + variantUriQuery, + selected, + available, + exists, + isDifferentProduct, + swatch, + } = value; + + if (isDifferentProduct) { + // SEO + // When the variant is a combined listing child product + // that leads to a different url, we need to render it + // as an anchor tag + return ( + + + + ); + } else { + // SEO + // When the variant is an update to the search param, + // render it as a button with javascript navigating to + // the variant so that SEO bots do not index these as + // duplicated links + return ( + + ); + } + })} +
+
+
+ ))} + { + open('cart'); + }} + lines={ + selectedVariant + ? [ + { + merchandiseId: selectedVariant.id, + quantity: 1, + selectedVariant, + }, + ] + : [] + } + > + {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} + +
+ ); + } + + function ProductOptionSwatch({ + swatch, + name, + }: { + swatch?: Maybe | undefined; + name: string; + }) { + const image = swatch?.image?.previewImage?.url; + const color = swatch?.color; + + if (!image && !color) return name; + + return ( +
+ {!!image && {name}} +
+ ); + } + ``` + + 9. Update `app.css` + + ```diff + + /* + + * -------------------------------------------------- + + * Non anchor links + + * -------------------------------------------------- + + */ + + .link:hover { + + text-decoration: underline; + + cursor: pointer; + + } + + ... + + - .product-options-item { + + .product-options-item, + + .product-options-item:disabled { + + padding: 0.25rem 0.5rem; + + background-color: transparent; + + font-size: 1rem; + + font-family: inherit; + + } + + + .product-option-label-swatch { + + width: 1.25rem; + + height: 1.25rem; + + margin: 0.25rem 0; + + } + + + .product-option-label-swatch img { + + width: 100%; + + } + ``` + + 10. Update `lib/variants.ts` + + Make `useVariantUrl` and `getVariantUrl` flexible to supplying a selected option param + + ```diff + export function useVariantUrl( + handle: string, + - selectedOptions: SelectedOption[], + + selectedOptions?: SelectedOption[], + ) { + const {pathname} = useLocation(); + + return useMemo(() => { + return getVariantUrl({ + handle, + pathname, + searchParams: new URLSearchParams(), + selectedOptions, + }); + }, [handle, selectedOptions, pathname]); + } + export function getVariantUrl({ + handle, + pathname, + searchParams, + selectedOptions, + }: { + handle: string; + pathname: string; + searchParams: URLSearchParams; + - selectedOptions: SelectedOption[]; + + selectedOptions?: SelectedOption[], + }) { + const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); + const isLocalePathname = match && match.length > 0; + const path = isLocalePathname + ? `${match![0]}products/${handle}` + : `/products/${handle}`; + + - selectedOptions.forEach((option) => { + + selectedOptions?.forEach((option) => { + searchParams.set(option.name, option.value); + }); + ``` + + 11. Update `routes/collections.$handle.tsx` + + We no longer need to query for the variants since product route can efficiently + obtain the first available variants. Update the code to reflect that: + + ```diff + const PRODUCT_ITEM_FRAGMENT = `#graphql + fragment MoneyProductItem on MoneyV2 { + amount + currencyCode + } + fragment ProductItem on Product { + id + handle + title + featuredImage { + id + altText + url + width + height + } + priceRange { + minVariantPrice { + ...MoneyProductItem + } + maxVariantPrice { + ...MoneyProductItem + } + } + - variants(first: 1) { + - nodes { + - selectedOptions { + - name + - value + - } + - } + - } + } + ` as const; + ``` + + and remove the variant reference + + ```diff + function ProductItem({ + product, + loading, + }: { + product: ProductItemFragment; + loading?: 'eager' | 'lazy'; + }) { + - const variant = product.variants.nodes[0]; + - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + + const variantUrl = useVariantUrl(product.handle); + return ( + ``` + + 12. Update `routes/collections.all.tsx` + + Same reasoning as `collections.$handle.tsx` + + ```diff + const PRODUCT_ITEM_FRAGMENT = `#graphql + fragment MoneyProductItem on MoneyV2 { + amount + currencyCode + } + fragment ProductItem on Product { + id + handle + title + featuredImage { + id + altText + url + width + height + } + priceRange { + minVariantPrice { + ...MoneyProductItem + } + maxVariantPrice { + ...MoneyProductItem + } + } + - variants(first: 1) { + - nodes { + - selectedOptions { + - name + - value + - } + - } + - } + } + ` as const; + ``` + + and remove the variant reference + + ```diff + function ProductItem({ + product, + loading, + }: { + product: ProductItemFragment; + loading?: 'eager' | 'lazy'; + }) { + - const variant = product.variants.nodes[0]; + - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions); + + const variantUrl = useVariantUrl(product.handle); + return ( + ``` + + 13. Update `routes/search.tsx` + + Instead of using the first variant, use `selectedOrFirstAvailableVariant` + + ```diff + const SEARCH_PRODUCT_FRAGMENT = `#graphql + fragment SearchProduct on Product { + __typename + handle + id + publishedAt + title + trackingParameters + vendor + - variants(first: 1) { + - nodes { + + selectedOrFirstAvailableVariant( + + selectedOptions: [] + + ignoreUnknownOptions: true + + caseInsensitiveMatch: true + + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + compareAtPrice { + amount + currencyCode + } + selectedOptions { + name + value + } + product { + handle + title + } + } + - } + } + ` as const; + ``` + + ```diff + const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql + fragment PredictiveProduct on Product { + __typename + id + title + handle + trackingParameters + - variants(first: 1) { + - nodes { + + selectedOrFirstAvailableVariant( + + selectedOptions: [] + + ignoreUnknownOptions: true + + caseInsensitiveMatch: true + + ) { + id + image { + url + altText + width + height + } + price { + amount + currencyCode + } + } + - } + } + ``` + + 14. Update `components/SearchResults.tsx` + + ```diff + function SearchResultsProducts({ + term, + products, + }: PartialSearchResult<'products'>) { + if (!products?.nodes.length) { + return null; + } + + return ( +
+

Products

+ + {({nodes, isLoading, NextLink, PreviousLink}) => { + const ItemsMarkup = nodes.map((product) => { + const productUrl = urlWithTrackingParams({ + baseUrl: `/products/${product.handle}`, + trackingParams: product.trackingParameters, + term, + }); + + + const price = product?.selectedOrFirstAvailableVariant?.price; + + const image = product?.selectedOrFirstAvailableVariant?.image; + + return ( +
+ + - {product.variants.nodes[0].image && ( + + {image && ( + {product.title} + )} +
+

{product.title}

+ + - + + {price && + + + + } + +
+ +
+ ); + }); + ``` + + 15. Update `components/SearchResultsPredictive.tsx` + + ```diff + function SearchResultsPredictiveProducts({ + term, + products, + closeSearch, + }: PartialPredictiveSearchResult<'products'>) { + if (!products.length) return null; + + return ( +
+
Products
+
    + {products.map((product) => { + const productUrl = urlWithTrackingParams({ + baseUrl: `/products/${product.handle}`, + trackingParams: product.trackingParameters, + term: term.current, + }); + + + const price = product?.selectedOrFirstAvailableVariant?.price; + - const image = product?.variants?.nodes?.[0].image; + + const image = product?.selectedOrFirstAvailableVariant?.image; + return ( +
  • + + {image && ( + {image.altText + )} +
    +

    {product.title}

    + + - {product?.variants?.nodes?.[0].price && ( + + {price && ( + - + + + )} + +
    + +
  • + ); + })} +
+
+ ); + } + ``` + +- Update `Aside` to have an accessible close button label ([#2639](https://github.com/Shopify/hydrogen/pull/2639)) by [@lb-](https://github.com/lb-) + +- Fix cart route so that it works with no-js ([#2665](https://github.com/Shopify/hydrogen/pull/2665)) by [@wizardlyhel](https://github.com/wizardlyhel) + +- Bump Shopify cli version ([#2667](https://github.com/Shopify/hydrogen/pull/2667)) by [@wizardlyhel](https://github.com/wizardlyhel) + +- Updated dependencies [[`8f64915e`](https://github.com/Shopify/hydrogen/commit/8f64915e934130299307417627a12caf756cd8da), [`a57d5267`](https://github.com/Shopify/hydrogen/commit/a57d5267daa2f22fe1a426fb9f62c242957f95b6), [`91d60fd2`](https://github.com/Shopify/hydrogen/commit/91d60fd2174b7c34f9f6b781cd5f0a70371fd899), [`23a80f3e`](https://github.com/Shopify/hydrogen/commit/23a80f3e7bf9f9908130fc9345397fc694420364)]: + - @shopify/hydrogen@2024.10.1 + ## 2024.10.1 ### Patch Changes diff --git a/templates/skeleton/package.json b/templates/skeleton/package.json index a262580115..1d974b0e98 100644 --- a/templates/skeleton/package.json +++ b/templates/skeleton/package.json @@ -2,7 +2,7 @@ "name": "skeleton", "private": true, "sideEffects": false, - "version": "2024.10.1", + "version": "2024.10.2", "type": "module", "scripts": { "build": "shopify hydrogen build --codegen", @@ -16,7 +16,7 @@ "dependencies": { "@remix-run/react": "^2.13.1", "@remix-run/server-runtime": "^2.13.1", - "@shopify/hydrogen": "2024.10.0", + "@shopify/hydrogen": "2024.10.1", "@shopify/remix-oxygen": "^2.0.9", "graphql": "^16.6.0", "graphql-tag": "^2.12.6",