@shopify/create-hydrogen 5.0.11 → 5.0.13

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