@shopify/cli-hydrogen 9.0.2 → 9.0.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.
Files changed (22) hide show
  1. package/dist/assets/hydrogen/starter/CHANGELOG.md +826 -0
  2. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +1 -1
  3. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +3 -4
  4. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +114 -44
  5. package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +8 -3
  6. package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +6 -5
  7. package/dist/assets/hydrogen/starter/app/lib/variants.ts +3 -3
  8. package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +1 -1
  9. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +9 -16
  10. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +1 -10
  11. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +1 -10
  12. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +50 -119
  13. package/dist/assets/hydrogen/starter/app/routes/search.tsx +42 -38
  14. package/dist/assets/hydrogen/starter/app/styles/app.css +25 -1
  15. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +23 -20
  16. package/dist/assets/hydrogen/starter/guides/search/search.md +27 -25
  17. package/dist/assets/hydrogen/starter/package.json +3 -3
  18. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +177 -194
  19. package/dist/assets/hydrogen/tailwind/tailwind.css +1 -1
  20. package/dist/commands/hydrogen/deploy.js +14 -3
  21. package/oclif.manifest.json +1 -1
  22. package/package.json +4 -4
@@ -1,5 +1,831 @@
1
1
  # skeleton
2
2
 
3
+ ## 2024.10.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Bump cli version ([#2694](https://github.com/Shopify/hydrogen/pull/2694)) by [@wizardlyhel](https://github.com/wizardlyhel)
8
+
9
+ ## 2024.10.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Prevent scroll reset on variant change ([#2672](https://github.com/Shopify/hydrogen/pull/2672)) by [@scottdixon](https://github.com/scottdixon)
14
+
15
+ ## 2024.10.2
16
+
17
+ ### Patch Changes
18
+
19
+ - Remove initial redirect from product display page ([#2643](https://github.com/Shopify/hydrogen/pull/2643)) by [@scottdixon](https://github.com/scottdixon)
20
+
21
+ - 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)
22
+
23
+ 1. Update your SFAPI product query to bring in the new query fields:
24
+
25
+ ```diff
26
+ const PRODUCT_FRAGMENT = `#graphql
27
+ fragment Product on Product {
28
+ id
29
+ title
30
+ vendor
31
+ handle
32
+ descriptionHtml
33
+ description
34
+ + encodedVariantExistence
35
+ + encodedVariantAvailability
36
+ options {
37
+ name
38
+ optionValues {
39
+ name
40
+ + firstSelectableVariant {
41
+ + ...ProductVariant
42
+ + }
43
+ + swatch {
44
+ + color
45
+ + image {
46
+ + previewImage {
47
+ + url
48
+ + }
49
+ + }
50
+ + }
51
+ }
52
+ }
53
+ - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
54
+ + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
55
+ + ...ProductVariant
56
+ + }
57
+ + adjacentVariants (selectedOptions: $selectedOptions) {
58
+ + ...ProductVariant
59
+ + }
60
+ - variants(first: 1) {
61
+ - nodes {
62
+ - ...ProductVariant
63
+ - }
64
+ - }
65
+ seo {
66
+ description
67
+ title
68
+ }
69
+ }
70
+ ${PRODUCT_VARIANT_FRAGMENT}
71
+ ` as const;
72
+ ```
73
+
74
+ 2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable.
75
+
76
+ ```diff
77
+ function loadDeferredData({context, params}: LoaderFunctionArgs) {
78
+ + // Put any API calls that is not critical to be available on first page render
79
+ + // For example: product reviews, product recommendations, social feeds.
80
+ - // In order to show which variants are available in the UI, we need to query
81
+ - // all of them. But there might be a *lot*, so instead separate the variants
82
+ - // into it's own separate query that is deferred. So there's a brief moment
83
+ - // where variant options might show as available when they're not, but after
84
+ - // this deferred query resolves, the UI will update.
85
+ - const variants = context.storefront
86
+ - .query(VARIANTS_QUERY, {
87
+ - variables: {handle: params.handle!},
88
+ - })
89
+ - .catch((error) => {
90
+ - // Log query errors, but don't throw them so the page can still render
91
+ - console.error(error);
92
+ - return null;
93
+ - });
94
+
95
+ + return {}
96
+ - return {
97
+ - variants,
98
+ - };
99
+ }
100
+ ```
101
+
102
+ 3. Remove the redirect logic in the `loadCriticalData` function and completely remove `redirectToFirstVariant` function
103
+
104
+ ```diff
105
+ async function loadCriticalData({
106
+ context,
107
+ params,
108
+ request,
109
+ }: LoaderFunctionArgs) {
110
+ const {handle} = params;
111
+ const {storefront} = context;
112
+ if (!handle) {
113
+ throw new Error('Expected product handle to be defined');
114
+ }
115
+ const [{product}] = await Promise.all([
116
+ storefront.query(PRODUCT_QUERY, {
117
+ variables: {handle, selectedOptions: getSelectedProductOptions(request)},
118
+ }),
119
+ // Add other queries here, so that they are loaded in parallel
120
+ ]);
121
+
122
+ if (!product?.id) {
123
+ throw new Response(null, {status: 404});
124
+ }
125
+
126
+ - const firstVariant = product.variants.nodes[0];
127
+ - const firstVariantIsDefault = Boolean(
128
+ - firstVariant.selectedOptions.find(
129
+ - (option: SelectedOption) =>
130
+ - option.name === 'Title' && option.value === 'Default Title',
131
+ - ),
132
+ - );
133
+
134
+ - if (firstVariantIsDefault) {
135
+ - product.selectedVariant = firstVariant;
136
+ - } else {
137
+ - // if no selected variant was returned from the selected options,
138
+ - // we redirect to the first variant's url with it's selected options applied
139
+ - if (!product.selectedVariant) {
140
+ - throw redirectToFirstVariant({product, request});
141
+ - }
142
+ - }
143
+
144
+ return {
145
+ product,
146
+ };
147
+ }
148
+
149
+ ...
150
+
151
+ - function redirectToFirstVariant({
152
+ - product,
153
+ - request,
154
+ - }: {
155
+ - product: ProductFragment;
156
+ - request: Request;
157
+ - }) {
158
+ - ...
159
+ - }
160
+ ```
161
+
162
+ 4. Update the `Product` component to use the new data fields.
163
+
164
+ ```diff
165
+ import {
166
+ getSelectedProductOptions,
167
+ Analytics,
168
+ useOptimisticVariant,
169
+ + getAdjacentAndFirstAvailableVariants,
170
+ } from '@shopify/hydrogen';
171
+
172
+ export default function Product() {
173
+ + const {product} = useLoaderData<typeof loader>();
174
+ - const {product, variants} = useLoaderData<typeof loader>();
175
+
176
+ + // Optimistically selects a variant with given available variant information
177
+ + const selectedVariant = useOptimisticVariant(
178
+ + product.selectedOrFirstAvailableVariant,
179
+ + getAdjacentAndFirstAvailableVariants(product),
180
+ + );
181
+ - const selectedVariant = useOptimisticVariant(
182
+ - product.selectedVariant,
183
+ - variants,
184
+ - );
185
+ ```
186
+
187
+ 5. Handle missing search query param in url from selecting a first variant
188
+
189
+ ```diff
190
+ import {
191
+ getSelectedProductOptions,
192
+ Analytics,
193
+ useOptimisticVariant,
194
+ getAdjacentAndFirstAvailableVariants,
195
+ + useSelectedOptionInUrlParam,
196
+ } from '@shopify/hydrogen';
197
+
198
+ export default function Product() {
199
+ const {product} = useLoaderData<typeof loader>();
200
+
201
+ // Optimistically selects a variant with given available variant information
202
+ const selectedVariant = useOptimisticVariant(
203
+ product.selectedOrFirstAvailableVariant,
204
+ getAdjacentAndFirstAvailableVariants(product),
205
+ );
206
+
207
+ + // Sets the search param to the selected variant without navigation
208
+ + // only when no search params are set in the url
209
+ + useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
210
+ ```
211
+
212
+ 6. Get the product options array using `getProductOptions`
213
+
214
+ ```diff
215
+ import {
216
+ getSelectedProductOptions,
217
+ Analytics,
218
+ useOptimisticVariant,
219
+ + getProductOptions,
220
+ getAdjacentAndFirstAvailableVariants,
221
+ useSelectedOptionInUrlParam,
222
+ } from '@shopify/hydrogen';
223
+
224
+ export default function Product() {
225
+ const {product} = useLoaderData<typeof loader>();
226
+
227
+ // Optimistically selects a variant with given available variant information
228
+ const selectedVariant = useOptimisticVariant(
229
+ product.selectedOrFirstAvailableVariant,
230
+ getAdjacentAndFirstAvailableVariants(product),
231
+ );
232
+
233
+ // Sets the search param to the selected variant without navigation
234
+ // only when no search params are set in the url
235
+ useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
236
+
237
+ + // Get the product options array
238
+ + const productOptions = getProductOptions({
239
+ + ...product,
240
+ + selectedOrFirstAvailableVariant: selectedVariant,
241
+ + });
242
+ ```
243
+
244
+ 7. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for.
245
+
246
+ ```diff
247
+ export default function Product() {
248
+
249
+ ...
250
+
251
+ return (
252
+ ...
253
+ + <ProductForm
254
+ + productOptions={productOptions}
255
+ + selectedVariant={selectedVariant}
256
+ + />
257
+ - <Suspense
258
+ - fallback={
259
+ - <ProductForm
260
+ - product={product}
261
+ - selectedVariant={selectedVariant}
262
+ - variants={[]}
263
+ - />
264
+ - }
265
+ - >
266
+ - <Await
267
+ - errorElement="There was a problem loading product variants"
268
+ - resolve={variants}
269
+ - >
270
+ - {(data) => (
271
+ - <ProductForm
272
+ - product={product}
273
+ - selectedVariant={selectedVariant}
274
+ - variants={data?.product?.variants.nodes || []}
275
+ - />
276
+ - )}
277
+ - </Await>
278
+ - </Suspense>
279
+ ```
280
+
281
+ 8. Update the `ProductForm` component.
282
+
283
+ ```tsx
284
+ import {Link, useNavigate} from '@remix-run/react';
285
+ import {type MappedProductOptions} from '@shopify/hydrogen';
286
+ import type {
287
+ Maybe,
288
+ ProductOptionValueSwatch,
289
+ } from '@shopify/hydrogen/storefront-api-types';
290
+ import {AddToCartButton} from './AddToCartButton';
291
+ import {useAside} from './Aside';
292
+ import type {ProductFragment} from 'storefrontapi.generated';
293
+
294
+ export function ProductForm({
295
+ productOptions,
296
+ selectedVariant,
297
+ }: {
298
+ productOptions: MappedProductOptions[];
299
+ selectedVariant: ProductFragment['selectedOrFirstAvailableVariant'];
300
+ }) {
301
+ const navigate = useNavigate();
302
+ const {open} = useAside();
303
+ return (
304
+ <div className="product-form">
305
+ {productOptions.map((option) => (
306
+ <div className="product-options" key={option.name}>
307
+ <h5>{option.name}</h5>
308
+ <div className="product-options-grid">
309
+ {option.optionValues.map((value) => {
310
+ const {
311
+ name,
312
+ handle,
313
+ variantUriQuery,
314
+ selected,
315
+ available,
316
+ exists,
317
+ isDifferentProduct,
318
+ swatch,
319
+ } = value;
320
+
321
+ if (isDifferentProduct) {
322
+ // SEO
323
+ // When the variant is a combined listing child product
324
+ // that leads to a different url, we need to render it
325
+ // as an anchor tag
326
+ return (
327
+ <Link
328
+ className="product-options-item"
329
+ key={option.name + name}
330
+ prefetch="intent"
331
+ preventScrollReset
332
+ replace
333
+ to={`/products/${handle}?${variantUriQuery}`}
334
+ style={{
335
+ border: selected
336
+ ? '1px solid black'
337
+ : '1px solid transparent',
338
+ opacity: available ? 1 : 0.3,
339
+ }}
340
+ >
341
+ <ProductOptionSwatch swatch={swatch} name={name} />
342
+ </Link>
343
+ );
344
+ } else {
345
+ // SEO
346
+ // When the variant is an update to the search param,
347
+ // render it as a button with javascript navigating to
348
+ // the variant so that SEO bots do not index these as
349
+ // duplicated links
350
+ return (
351
+ <button
352
+ type="button"
353
+ className={`product-options-item${
354
+ exists && !selected ? ' link' : ''
355
+ }`}
356
+ key={option.name + name}
357
+ style={{
358
+ border: selected
359
+ ? '1px solid black'
360
+ : '1px solid transparent',
361
+ opacity: available ? 1 : 0.3,
362
+ }}
363
+ disabled={!exists}
364
+ onClick={() => {
365
+ if (!selected) {
366
+ navigate(`?${variantUriQuery}`, {
367
+ replace: true,
368
+ });
369
+ }
370
+ }}
371
+ >
372
+ <ProductOptionSwatch swatch={swatch} name={name} />
373
+ </button>
374
+ );
375
+ }
376
+ })}
377
+ </div>
378
+ <br />
379
+ </div>
380
+ ))}
381
+ <AddToCartButton
382
+ disabled={!selectedVariant || !selectedVariant.availableForSale}
383
+ onClick={() => {
384
+ open('cart');
385
+ }}
386
+ lines={
387
+ selectedVariant
388
+ ? [
389
+ {
390
+ merchandiseId: selectedVariant.id,
391
+ quantity: 1,
392
+ selectedVariant,
393
+ },
394
+ ]
395
+ : []
396
+ }
397
+ >
398
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
399
+ </AddToCartButton>
400
+ </div>
401
+ );
402
+ }
403
+
404
+ function ProductOptionSwatch({
405
+ swatch,
406
+ name,
407
+ }: {
408
+ swatch?: Maybe<ProductOptionValueSwatch> | undefined;
409
+ name: string;
410
+ }) {
411
+ const image = swatch?.image?.previewImage?.url;
412
+ const color = swatch?.color;
413
+
414
+ if (!image && !color) return name;
415
+
416
+ return (
417
+ <div
418
+ aria-label={name}
419
+ className="product-option-label-swatch"
420
+ style={{
421
+ backgroundColor: color || 'transparent',
422
+ }}
423
+ >
424
+ {!!image && <img src={image} alt={name} />}
425
+ </div>
426
+ );
427
+ }
428
+ ```
429
+
430
+ 9. Update `app.css`
431
+
432
+ ```diff
433
+ + /*
434
+ + * --------------------------------------------------
435
+ + * Non anchor links
436
+ + * --------------------------------------------------
437
+ + */
438
+ + .link:hover {
439
+ + text-decoration: underline;
440
+ + cursor: pointer;
441
+ + }
442
+
443
+ ...
444
+
445
+ - .product-options-item {
446
+ + .product-options-item,
447
+ + .product-options-item:disabled {
448
+ + padding: 0.25rem 0.5rem;
449
+ + background-color: transparent;
450
+ + font-size: 1rem;
451
+ + font-family: inherit;
452
+ + }
453
+
454
+ + .product-option-label-swatch {
455
+ + width: 1.25rem;
456
+ + height: 1.25rem;
457
+ + margin: 0.25rem 0;
458
+ + }
459
+
460
+ + .product-option-label-swatch img {
461
+ + width: 100%;
462
+ + }
463
+ ```
464
+
465
+ 10. Update `lib/variants.ts`
466
+
467
+ Make `useVariantUrl` and `getVariantUrl` flexible to supplying a selected option param
468
+
469
+ ```diff
470
+ export function useVariantUrl(
471
+ handle: string,
472
+ - selectedOptions: SelectedOption[],
473
+ + selectedOptions?: SelectedOption[],
474
+ ) {
475
+ const {pathname} = useLocation();
476
+
477
+ return useMemo(() => {
478
+ return getVariantUrl({
479
+ handle,
480
+ pathname,
481
+ searchParams: new URLSearchParams(),
482
+ selectedOptions,
483
+ });
484
+ }, [handle, selectedOptions, pathname]);
485
+ }
486
+ export function getVariantUrl({
487
+ handle,
488
+ pathname,
489
+ searchParams,
490
+ selectedOptions,
491
+ }: {
492
+ handle: string;
493
+ pathname: string;
494
+ searchParams: URLSearchParams;
495
+ - selectedOptions: SelectedOption[];
496
+ + selectedOptions?: SelectedOption[],
497
+ }) {
498
+ const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname);
499
+ const isLocalePathname = match && match.length > 0;
500
+ const path = isLocalePathname
501
+ ? `${match![0]}products/${handle}`
502
+ : `/products/${handle}`;
503
+
504
+ - selectedOptions.forEach((option) => {
505
+ + selectedOptions?.forEach((option) => {
506
+ searchParams.set(option.name, option.value);
507
+ });
508
+ ```
509
+
510
+ 11. Update `routes/collections.$handle.tsx`
511
+
512
+ We no longer need to query for the variants since product route can efficiently
513
+ obtain the first available variants. Update the code to reflect that:
514
+
515
+ ```diff
516
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
517
+ fragment MoneyProductItem on MoneyV2 {
518
+ amount
519
+ currencyCode
520
+ }
521
+ fragment ProductItem on Product {
522
+ id
523
+ handle
524
+ title
525
+ featuredImage {
526
+ id
527
+ altText
528
+ url
529
+ width
530
+ height
531
+ }
532
+ priceRange {
533
+ minVariantPrice {
534
+ ...MoneyProductItem
535
+ }
536
+ maxVariantPrice {
537
+ ...MoneyProductItem
538
+ }
539
+ }
540
+ - variants(first: 1) {
541
+ - nodes {
542
+ - selectedOptions {
543
+ - name
544
+ - value
545
+ - }
546
+ - }
547
+ - }
548
+ }
549
+ ` as const;
550
+ ```
551
+
552
+ and remove the variant reference
553
+
554
+ ```diff
555
+ function ProductItem({
556
+ product,
557
+ loading,
558
+ }: {
559
+ product: ProductItemFragment;
560
+ loading?: 'eager' | 'lazy';
561
+ }) {
562
+ - const variant = product.variants.nodes[0];
563
+ - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
564
+ + const variantUrl = useVariantUrl(product.handle);
565
+ return (
566
+ ```
567
+
568
+ 12. Update `routes/collections.all.tsx`
569
+
570
+ Same reasoning as `collections.$handle.tsx`
571
+
572
+ ```diff
573
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
574
+ fragment MoneyProductItem on MoneyV2 {
575
+ amount
576
+ currencyCode
577
+ }
578
+ fragment ProductItem on Product {
579
+ id
580
+ handle
581
+ title
582
+ featuredImage {
583
+ id
584
+ altText
585
+ url
586
+ width
587
+ height
588
+ }
589
+ priceRange {
590
+ minVariantPrice {
591
+ ...MoneyProductItem
592
+ }
593
+ maxVariantPrice {
594
+ ...MoneyProductItem
595
+ }
596
+ }
597
+ - variants(first: 1) {
598
+ - nodes {
599
+ - selectedOptions {
600
+ - name
601
+ - value
602
+ - }
603
+ - }
604
+ - }
605
+ }
606
+ ` as const;
607
+ ```
608
+
609
+ and remove the variant reference
610
+
611
+ ```diff
612
+ function ProductItem({
613
+ product,
614
+ loading,
615
+ }: {
616
+ product: ProductItemFragment;
617
+ loading?: 'eager' | 'lazy';
618
+ }) {
619
+ - const variant = product.variants.nodes[0];
620
+ - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
621
+ + const variantUrl = useVariantUrl(product.handle);
622
+ return (
623
+ ```
624
+
625
+ 13. Update `routes/search.tsx`
626
+
627
+ Instead of using the first variant, use `selectedOrFirstAvailableVariant`
628
+
629
+ ```diff
630
+ const SEARCH_PRODUCT_FRAGMENT = `#graphql
631
+ fragment SearchProduct on Product {
632
+ __typename
633
+ handle
634
+ id
635
+ publishedAt
636
+ title
637
+ trackingParameters
638
+ vendor
639
+ - variants(first: 1) {
640
+ - nodes {
641
+ + selectedOrFirstAvailableVariant(
642
+ + selectedOptions: []
643
+ + ignoreUnknownOptions: true
644
+ + caseInsensitiveMatch: true
645
+ + ) {
646
+ id
647
+ image {
648
+ url
649
+ altText
650
+ width
651
+ height
652
+ }
653
+ price {
654
+ amount
655
+ currencyCode
656
+ }
657
+ compareAtPrice {
658
+ amount
659
+ currencyCode
660
+ }
661
+ selectedOptions {
662
+ name
663
+ value
664
+ }
665
+ product {
666
+ handle
667
+ title
668
+ }
669
+ }
670
+ - }
671
+ }
672
+ ` as const;
673
+ ```
674
+
675
+ ```diff
676
+ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
677
+ fragment PredictiveProduct on Product {
678
+ __typename
679
+ id
680
+ title
681
+ handle
682
+ trackingParameters
683
+ - variants(first: 1) {
684
+ - nodes {
685
+ + selectedOrFirstAvailableVariant(
686
+ + selectedOptions: []
687
+ + ignoreUnknownOptions: true
688
+ + caseInsensitiveMatch: true
689
+ + ) {
690
+ id
691
+ image {
692
+ url
693
+ altText
694
+ width
695
+ height
696
+ }
697
+ price {
698
+ amount
699
+ currencyCode
700
+ }
701
+ }
702
+ - }
703
+ }
704
+ ```
705
+
706
+ 14. Update `components/SearchResults.tsx`
707
+
708
+ ```diff
709
+ function SearchResultsProducts({
710
+ term,
711
+ products,
712
+ }: PartialSearchResult<'products'>) {
713
+ if (!products?.nodes.length) {
714
+ return null;
715
+ }
716
+
717
+ return (
718
+ <div className="search-result">
719
+ <h2>Products</h2>
720
+ <Pagination connection={products}>
721
+ {({nodes, isLoading, NextLink, PreviousLink}) => {
722
+ const ItemsMarkup = nodes.map((product) => {
723
+ const productUrl = urlWithTrackingParams({
724
+ baseUrl: `/products/${product.handle}`,
725
+ trackingParams: product.trackingParameters,
726
+ term,
727
+ });
728
+
729
+ + const price = product?.selectedOrFirstAvailableVariant?.price;
730
+ + const image = product?.selectedOrFirstAvailableVariant?.image;
731
+
732
+ return (
733
+ <div className="search-results-item" key={product.id}>
734
+ <Link prefetch="intent" to={productUrl}>
735
+ - {product.variants.nodes[0].image && (
736
+ + {image && (
737
+ <Image
738
+ - data={product.variants.nodes[0].image}
739
+ + data={image}
740
+ alt={product.title}
741
+ width={50}
742
+ />
743
+ )}
744
+ <div>
745
+ <p>{product.title}</p>
746
+ <small>
747
+ - <Money data={product.variants.nodes[0].price} />
748
+ + {price &&
749
+ + <Money data={price} />
750
+ + }
751
+ </small>
752
+ </div>
753
+ </Link>
754
+ </div>
755
+ );
756
+ });
757
+ ```
758
+
759
+ 15. Update `components/SearchResultsPredictive.tsx`
760
+
761
+ ```diff
762
+ function SearchResultsPredictiveProducts({
763
+ term,
764
+ products,
765
+ closeSearch,
766
+ }: PartialPredictiveSearchResult<'products'>) {
767
+ if (!products.length) return null;
768
+
769
+ return (
770
+ <div className="predictive-search-result" key="products">
771
+ <h5>Products</h5>
772
+ <ul>
773
+ {products.map((product) => {
774
+ const productUrl = urlWithTrackingParams({
775
+ baseUrl: `/products/${product.handle}`,
776
+ trackingParams: product.trackingParameters,
777
+ term: term.current,
778
+ });
779
+
780
+ + const price = product?.selectedOrFirstAvailableVariant?.price;
781
+ - const image = product?.variants?.nodes?.[0].image;
782
+ + const image = product?.selectedOrFirstAvailableVariant?.image;
783
+ return (
784
+ <li className="predictive-search-result-item" key={product.id}>
785
+ <Link to={productUrl} onClick={closeSearch}>
786
+ {image && (
787
+ <Image
788
+ alt={image.altText ?? ''}
789
+ src={image.url}
790
+ width={50}
791
+ height={50}
792
+ />
793
+ )}
794
+ <div>
795
+ <p>{product.title}</p>
796
+ <small>
797
+ - {product?.variants?.nodes?.[0].price && (
798
+ + {price && (
799
+ - <Money data={product.variants.nodes[0].price} />
800
+ + <Money data={price} />
801
+ )}
802
+ </small>
803
+ </div>
804
+ </Link>
805
+ </li>
806
+ );
807
+ })}
808
+ </ul>
809
+ </div>
810
+ );
811
+ }
812
+ ```
813
+
814
+ - Update `Aside` to have an accessible close button label ([#2639](https://github.com/Shopify/hydrogen/pull/2639)) by [@lb-](https://github.com/lb-)
815
+
816
+ - Fix cart route so that it works with no-js ([#2665](https://github.com/Shopify/hydrogen/pull/2665)) by [@wizardlyhel](https://github.com/wizardlyhel)
817
+
818
+ - Bump Shopify cli version ([#2667](https://github.com/Shopify/hydrogen/pull/2667)) by [@wizardlyhel](https://github.com/wizardlyhel)
819
+
820
+ - 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)]:
821
+ - @shopify/hydrogen@2024.10.1
822
+
823
+ ## 2024.10.1
824
+
825
+ ### Patch Changes
826
+
827
+ - Bump to get new cli package version by [@wizardlyhel](https://github.com/wizardlyhel)
828
+
3
829
  ## 2024.10.0
4
830
 
5
831
  ### Patch Changes