@shopify/cli 3.65.1 → 3.65.3

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 (122) hide show
  1. package/dist/assets/hydrogen/i18n/domains.ts +4 -11
  2. package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +4 -2
  3. package/dist/assets/hydrogen/i18n/subdomains.ts +4 -11
  4. package/dist/assets/hydrogen/i18n/subfolders.ts +4 -11
  5. package/dist/assets/hydrogen/starter/CHANGELOG.md +165 -0
  6. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +5 -2
  7. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +2 -2
  8. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +65 -19
  9. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +42 -0
  10. package/dist/assets/hydrogen/starter/app/components/SearchForm.tsx +68 -0
  11. package/dist/assets/hydrogen/starter/app/components/SearchFormPredictive.tsx +76 -0
  12. package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +164 -0
  13. package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +322 -0
  14. package/dist/assets/hydrogen/starter/app/entry.client.tsx +10 -8
  15. package/dist/assets/hydrogen/starter/app/entry.server.tsx +1 -1
  16. package/dist/assets/hydrogen/starter/app/lib/context.ts +43 -0
  17. package/dist/assets/hydrogen/starter/app/lib/fragments.ts +53 -0
  18. package/dist/assets/hydrogen/starter/app/lib/search.ts +74 -24
  19. package/dist/assets/hydrogen/starter/app/root.tsx +4 -7
  20. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -3
  21. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +5 -19
  22. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +11 -24
  23. package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +14 -27
  24. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +12 -30
  25. package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +13 -27
  26. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +9 -31
  27. package/dist/assets/hydrogen/starter/app/routes/search.tsx +312 -73
  28. package/dist/assets/hydrogen/starter/app/styles/reset.css +12 -2
  29. package/dist/assets/hydrogen/starter/env.d.ts +11 -30
  30. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  31. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +391 -0
  32. package/dist/assets/hydrogen/starter/guides/search/search.jpg +0 -0
  33. package/dist/assets/hydrogen/starter/guides/search/search.md +333 -0
  34. package/dist/assets/hydrogen/starter/package.json +4 -4
  35. package/dist/assets/hydrogen/starter/server.ts +18 -74
  36. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +242 -172
  37. package/dist/assets/hydrogen/virtual-routes/components/{PageLayout.jsx → Layout.jsx} +2 -2
  38. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +7 -6
  39. package/dist/{chunk-H42RFZDD.js → chunk-3ABSSTBQ.js} +4 -4
  40. package/dist/{chunk-M7WMYV4S.js → chunk-3D4VZQOH.js} +2 -2
  41. package/dist/{chunk-J7BYFGNJ.js → chunk-3GSKXZGY.js} +2 -2
  42. package/dist/{chunk-TDWX3KIR.js → chunk-3LDWVYMD.js} +2 -2
  43. package/dist/{chunk-N2BXKOJG.js → chunk-646BIVHE.js} +4 -4
  44. package/dist/{chunk-CZ3SHYYH.js → chunk-7WAEFADN.js} +4 -4
  45. package/dist/{chunk-EKT2GUGH.js → chunk-7WGBIPDW.js} +2 -2
  46. package/dist/{chunk-M6KGRVDD.js → chunk-AX77SAMU.js} +3 -3
  47. package/dist/{chunk-4HAEQQTQ.js → chunk-BQBBVYYU.js} +4 -4
  48. package/dist/{chunk-5YD4FDOS.js → chunk-BZLNTDGG.js} +3 -3
  49. package/dist/{chunk-VWALMO2Z.js → chunk-CSCEGIBZ.js} +3 -3
  50. package/dist/{chunk-F2Y7KYHZ.js → chunk-EIUQV76I.js} +5 -5
  51. package/dist/{chunk-MODBIZ4R.js → chunk-GN74L7IW.js} +2 -2
  52. package/dist/{chunk-5EAVIJTQ.js → chunk-HYCRESCR.js} +2 -2
  53. package/dist/{chunk-GDARYUPU.js → chunk-K7KD247K.js} +188 -243
  54. package/dist/{chunk-PZM45AUI.js → chunk-KIUXMPTX.js} +3 -3
  55. package/dist/{chunk-PYMSCBPA.js → chunk-LAJ4OEME.js} +2 -2
  56. package/dist/{chunk-YVHV3H5H.js → chunk-MIQBXNSN.js} +4 -4
  57. package/dist/{chunk-BLKDGMHM.js → chunk-MV6A3QHA.js} +4 -4
  58. package/dist/{chunk-CFFAWVDL.js → chunk-N3YORLAS.js} +2 -2
  59. package/dist/{chunk-EU5ZOEUT.js → chunk-NBTEOGQW.js} +2 -2
  60. package/dist/{chunk-ZXJU6UP4.js → chunk-O3JOUAA5.js} +4 -4
  61. package/dist/{chunk-EZ5DG73H.js → chunk-PEAIOYXD.js} +4 -4
  62. package/dist/{chunk-YDS7NZBQ.js → chunk-R5GT4GBL.js} +4 -4
  63. package/dist/{chunk-6M65VRAT.js → chunk-S7FJTFYR.js} +5 -5
  64. package/dist/{chunk-DX2RXOQ5.js → chunk-S7RH664J.js} +3 -3
  65. package/dist/{chunk-WMECC32P.js → chunk-SKF2SKWO.js} +3 -3
  66. package/dist/{chunk-27HGZPUX.js → chunk-SMKCVFDT.js} +3 -3
  67. package/dist/{chunk-EID6L4PR.js → chunk-T4Y7NDNJ.js} +2 -2
  68. package/dist/{chunk-PY33KMCK.js → chunk-TWWJNMTO.js} +2 -2
  69. package/dist/{chunk-YXPGPWR2.js → chunk-U2PN6QZ2.js} +5 -5
  70. package/dist/{chunk-3REVOIEW.js → chunk-UBCH575K.js} +5 -5
  71. package/dist/{chunk-A4NQWDPT.js → chunk-XLURAR5E.js} +3 -3
  72. package/dist/{chunk-ZZKUI3DP.js → chunk-YPG7LXPN.js} +3 -3
  73. package/dist/cli/commands/auth/logout.js +10 -10
  74. package/dist/cli/commands/auth/logout.test.js +11 -11
  75. package/dist/cli/commands/debug/command-flags.js +9 -9
  76. package/dist/cli/commands/demo/catalog.js +10 -10
  77. package/dist/cli/commands/demo/generate-file.js +10 -10
  78. package/dist/cli/commands/demo/index.js +10 -10
  79. package/dist/cli/commands/demo/print-ai-prompt.js +10 -10
  80. package/dist/cli/commands/docs/generate.js +9 -9
  81. package/dist/cli/commands/docs/generate.test.js +9 -9
  82. package/dist/cli/commands/help.js +9 -9
  83. package/dist/cli/commands/kitchen-sink/async.js +10 -10
  84. package/dist/cli/commands/kitchen-sink/async.test.js +10 -10
  85. package/dist/cli/commands/kitchen-sink/index.js +12 -12
  86. package/dist/cli/commands/kitchen-sink/index.test.js +12 -12
  87. package/dist/cli/commands/kitchen-sink/prompts.js +10 -10
  88. package/dist/cli/commands/kitchen-sink/prompts.test.js +10 -10
  89. package/dist/cli/commands/kitchen-sink/static.js +10 -10
  90. package/dist/cli/commands/kitchen-sink/static.test.js +10 -10
  91. package/dist/cli/commands/search.js +10 -10
  92. package/dist/cli/commands/upgrade.js +9 -9
  93. package/dist/cli/commands/version.js +10 -10
  94. package/dist/cli/commands/version.test.js +10 -10
  95. package/dist/cli/services/commands/search.js +2 -2
  96. package/dist/cli/services/commands/search.test.js +2 -2
  97. package/dist/cli/services/commands/version.js +4 -4
  98. package/dist/cli/services/commands/version.test.js +5 -5
  99. package/dist/cli/services/demo.js +2 -2
  100. package/dist/cli/services/demo.test.js +2 -2
  101. package/dist/cli/services/kitchen-sink/async.js +2 -2
  102. package/dist/cli/services/kitchen-sink/prompts.js +2 -2
  103. package/dist/cli/services/kitchen-sink/static.js +2 -2
  104. package/dist/cli/services/upgrade.js +3 -3
  105. package/dist/cli/services/upgrade.test.js +5 -5
  106. package/dist/{custom-oclif-loader-JHNX2EGV.js → custom-oclif-loader-BT7EH2NN.js} +3 -3
  107. package/dist/{error-handler-4UJ6363X.js → error-handler-OSEY6KVA.js} +8 -8
  108. package/dist/hooks/postrun.js +6 -6
  109. package/dist/hooks/prerun.js +4 -4
  110. package/dist/index.js +1333 -1279
  111. package/dist/{local-V7RONWNU.js → local-OQXN5NM2.js} +2 -2
  112. package/dist/{morph-DN4AZJZW.js → morph-IQTWRBBT.js} +16 -12
  113. package/dist/{node-3H4OKRLA.js → node-YQVH3Y7J.js} +13 -13
  114. package/dist/{node-package-manager-XM7EXHQA.js → node-package-manager-VW2DN7R4.js} +3 -3
  115. package/dist/{system-F63VIZ5U.js → system-347PZWVP.js} +2 -2
  116. package/dist/tsconfig.tsbuildinfo +1 -1
  117. package/dist/{ui-BXWWRIFS.js → ui-S7L55PBH.js} +2 -2
  118. package/dist/{workerd-A5NCF6UA.js → workerd-OLKE7G4X.js} +12 -12
  119. package/oclif.manifest.json +39 -2
  120. package/package.json +7 -7
  121. package/dist/assets/hydrogen/starter/app/components/Search.tsx +0 -514
  122. package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +0 -318
@@ -0,0 +1,391 @@
1
+ # Hydrogen Predictive Search
2
+
3
+ Our skeleton template ships with predictive search functionality. While [regular search](../search/search.md)
4
+ provides paginated search of `pages`, `articles` and `products` inside the `/search` route,
5
+ predictive provides real-time results in a aside drawer for `pages`, `articles`, `products`, `collections` and
6
+ recommended `queries/suggestions`.
7
+
8
+ This integration uses the storefront API (SFAPI) [predictiveSearch](https://shopify.dev/docs/api/storefront/latest/queries/vpredictiveSearch) endpoint to retrieve predictive search results based on a search term.
9
+
10
+ ## Components Architecture
11
+
12
+ ![alt text](./predictiveSearch.jpg)
13
+
14
+ ## Components
15
+
16
+ | File | Description |
17
+ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
18
+ | [`app/components/SearchFormPredictive.tsx`](../../app/components/SearchFormPredictive.tsx) | A fully customizable form component configured to make form `GET` requests to the `/search` route. |
19
+ | [`app/components/SearchResultsPredictive.tsx`](../../app/components/SearchResultsPredictive.tsx) | A fully customizable search results wrapper, that provides compound components to render `articles`, `pages`, `products`, `collections` and `queries`. |
20
+
21
+ ## Instructions
22
+
23
+ ### 1. Create the search route
24
+
25
+ Create a new file at `/routes/search.tsx` (if not already created)
26
+
27
+ ### 3. Add `predictiveSearch` query and fetcher
28
+
29
+ The predictiveSearch fetcher parses the `q` and `limit` formData properties sent
30
+ by the `<SearchFormPredictive />` component and performs the predictive search
31
+ SFAPI request.
32
+
33
+ ```ts
34
+ /**
35
+ * Predictive search query and fragments
36
+ * (adjust as needed)
37
+ */
38
+ const PREDICTIVE_SEARCH_ARTICLE_FRAGMENT = `#graphql
39
+ fragment PredictiveArticle on Article {
40
+ __typename
41
+ id
42
+ title
43
+ handle
44
+ blog {
45
+ handle
46
+ }
47
+ image {
48
+ url
49
+ altText
50
+ width
51
+ height
52
+ }
53
+ trackingParameters
54
+ }
55
+ ` as const;
56
+
57
+ const PREDICTIVE_SEARCH_COLLECTION_FRAGMENT = `#graphql
58
+ fragment PredictiveCollection on Collection {
59
+ __typename
60
+ id
61
+ title
62
+ handle
63
+ image {
64
+ url
65
+ altText
66
+ width
67
+ height
68
+ }
69
+ trackingParameters
70
+ }
71
+ ` as const;
72
+
73
+ const PREDICTIVE_SEARCH_PAGE_FRAGMENT = `#graphql
74
+ fragment PredictivePage on Page {
75
+ __typename
76
+ id
77
+ title
78
+ handle
79
+ trackingParameters
80
+ }
81
+ ` as const;
82
+
83
+ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
84
+ fragment PredictiveProduct on Product {
85
+ __typename
86
+ id
87
+ title
88
+ handle
89
+ trackingParameters
90
+ variants(first: 1) {
91
+ nodes {
92
+ id
93
+ image {
94
+ url
95
+ altText
96
+ width
97
+ height
98
+ }
99
+ price {
100
+ amount
101
+ currencyCode
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ` as const;
107
+
108
+ const PREDICTIVE_SEARCH_QUERY_FRAGMENT = `#graphql
109
+ fragment PredictiveQuery on SearchQuerySuggestion {
110
+ __typename
111
+ text
112
+ styledText
113
+ trackingParameters
114
+ }
115
+ ` as const;
116
+
117
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/queries/predictiveSearch
118
+ const PREDICTIVE_SEARCH_QUERY = `#graphql
119
+ query predictiveSearch(
120
+ $country: CountryCode
121
+ $language: LanguageCode
122
+ $limit: Int!
123
+ $limitScope: PredictiveSearchLimitScope!
124
+ $term: String!
125
+ $types: [PredictiveSearchType!]
126
+ ) @inContext(country: $country, language: $language) {
127
+ predictiveSearch(
128
+ limit: $limit,
129
+ limitScope: $limitScope,
130
+ query: $term,
131
+ types: $types,
132
+ ) {
133
+ articles {
134
+ ...PredictiveArticle
135
+ }
136
+ collections {
137
+ ...PredictiveCollection
138
+ }
139
+ pages {
140
+ ...PredictivePage
141
+ }
142
+ products {
143
+ ...PredictiveProduct
144
+ }
145
+ queries {
146
+ ...PredictiveQuery
147
+ }
148
+ }
149
+ }
150
+ ${PREDICTIVE_SEARCH_ARTICLE_FRAGMENT}
151
+ ${PREDICTIVE_SEARCH_COLLECTION_FRAGMENT}
152
+ ${PREDICTIVE_SEARCH_PAGE_FRAGMENT}
153
+ ${PREDICTIVE_SEARCH_PRODUCT_FRAGMENT}
154
+ ${PREDICTIVE_SEARCH_QUERY_FRAGMENT}
155
+ ` as const;
156
+
157
+ /**
158
+ * Predictive search fetcher
159
+ */
160
+ async function predictiveSeach({
161
+ request,
162
+ context,
163
+ }: Pick<ActionFunctionArgs, 'request' | 'context'>) {
164
+ const {storefront} = context;
165
+ const formData = await request.formData();
166
+ const term = String(formData.get('q') || '');
167
+
168
+ const limit = Number(formData.get('limit') || 10);
169
+
170
+ // Predictively search articles, collections, pages, products, and queries (suggestions)
171
+ const {predictiveSearch: items, errors} = await storefront.query(
172
+ PREDICTIVE_SEARCH_QUERY,
173
+ {
174
+ variables: {
175
+ // customize search options as needed
176
+ limit,
177
+ limitScope: 'EACH',
178
+ term,
179
+ },
180
+ },
181
+ );
182
+
183
+ if (errors) {
184
+ throw new Error(
185
+ `Shopify API errors: ${errors.map(({message}) => message).join(', ')}`,
186
+ );
187
+ }
188
+
189
+ if (!items) {
190
+ throw new Error('No predictive search data returned');
191
+ }
192
+
193
+ const total = Object.values(items).reduce((acc, {length}) => acc + length, 0);
194
+
195
+ return json({term, result: {items, total}, error: null});
196
+ }
197
+ ```
198
+
199
+ ### 3. Add a `loader` export to the route
200
+
201
+ This action receives and processes `GET` requests from the `<SearchFormPredictive />`
202
+ component. These request include the search parameter `predictive` to identify them over
203
+ regular search requests.
204
+
205
+ A `q` URL parameter will be used as the search term and appended automatically by
206
+ the form if present in it's children prop.
207
+
208
+ ```ts
209
+ /**
210
+ * Handles predictive search GET requests
211
+ * requested by the SearchFormPredictive component
212
+ */
213
+ export async function loader({request, context}: LoaderFunctionArgs) {
214
+ const url = new URL(request.url);
215
+ const isPredictive = url.searchParams.has('predictive');
216
+
217
+ if (!isPredictive) {
218
+ return json({})
219
+ }
220
+
221
+ const searchPromise = predictiveSearch({request, context})
222
+
223
+ searchPromise.catch((error: Error) => {
224
+ console.error(error);
225
+ return {term: '', result: null, error: error.message};
226
+ });
227
+
228
+ return json(await searchPromise);
229
+ }
230
+ ```
231
+
232
+ ### 4. Render the predictive search form and results
233
+
234
+ Create a SearchAside or similar component to render the form and results.
235
+
236
+ ```ts
237
+ import { SearchFormPredictive } from '~/components/SearchFormPredictive';
238
+ import { SearchResultsPredictive } from '~/components/SearchResultsPredictive';
239
+
240
+ function SearchAside() {
241
+ return (
242
+ <Aside type="search" heading="SEARCH">
243
+ <div className="predictive-search">
244
+ <br />
245
+ <SearchFormPredictive>
246
+ {({ fetchResults, goToSearch, inputRef }) => (
247
+ <>
248
+ <input
249
+ name="q"
250
+ onChange={fetchResults}
251
+ onFocus={fetchResults}
252
+ placeholder="Search"
253
+ ref={inputRef}
254
+ type="search"
255
+ />
256
+ &nbsp;
257
+ <button onClick={goToSearch}>
258
+ Search
259
+ </button>
260
+ </>
261
+ )}
262
+ </SearchFormPredictive>
263
+
264
+ <SearchResultsPredictive>
265
+ {({ items, total, term, state, inputRef, closeSearch }) => {
266
+ const { articles, collections, pages, products, queries } = items;
267
+
268
+ if (state === 'loading' && term.current) {
269
+ return <div>Loading...</div>;
270
+ }
271
+
272
+ if (!total) {
273
+ return <SearchResultsPredictive.Empty term={term} />;
274
+ }
275
+
276
+ return (
277
+ <>
278
+ <SearchResultsPredictive.Queries
279
+ queries={queries}
280
+ term={term}
281
+ inputRef={inputRef}
282
+ />
283
+ <SearchResultsPredictive.Products
284
+ products={products}
285
+ closeSearch={closeSearch}
286
+ term={term}
287
+ />
288
+ <SearchResultsPredictive.Collections
289
+ collections={collections}
290
+ closeSearch={closeSearch}
291
+ term={term}
292
+ />
293
+ <SearchResultsPredictive.Pages
294
+ pages={pages}
295
+ closeSearch={closeSearch}
296
+ term={term}
297
+ />
298
+ <SearchResultsPredictive.Articles
299
+ articles={articles}
300
+ closeSearch={closeSearch}
301
+ term={term}
302
+ />
303
+ {term.current && total && (
304
+ <Link onClick={closeSearch} to={`/search?q=${term.current}`}>
305
+ <p>
306
+ View all results for <q>{term.current}</q> →
307
+ </p>
308
+ </Link>
309
+ )}
310
+ </>
311
+ );
312
+ }}
313
+ </SearchResultsPredictive>
314
+ </div>
315
+ </Aside>
316
+ );
317
+ }
318
+ ```
319
+
320
+ ## Additional Notes
321
+
322
+ ### How to use a different URL search parameter?
323
+
324
+ - Modify the `name` attribute in the forms input element. e.g
325
+
326
+ ```ts
327
+ <input name="query" />`.
328
+ ```
329
+
330
+ - Modify the fetchers term variable to parse the new name. e.g
331
+
332
+ ```ts
333
+ const term = String(searchParams.get('query') || '');
334
+ ```
335
+
336
+ ### How to customize the way the results look?
337
+
338
+ Simply go to `/app/components/SearchResultsPredictive.tsx` and look for the compound
339
+ component you would like to modify.
340
+
341
+ Here we add images to each predictive product result item
342
+
343
+ ```diff
344
+ SearchResultsPredictive.Products = function ({
345
+ products,
346
+ closeSearch,
347
+ term,
348
+ }: SearchResultsPredictiveProductsProps) {
349
+ if (!products.length) return null;
350
+
351
+ return (
352
+ <div className="predictive-search-result" key="products">
353
+ <h5>Products</h5>
354
+ <ul>
355
+ {products.map((product) => {
356
+ const productUrl = urlWithTrackingParams({
357
+ baseUrl: `/products/${product.handle}`,
358
+ trackingParams: product.trackingParameters,
359
+ term: term.current,
360
+ });
361
+ + const image = product?.variants?.nodes?.[0].image;
362
+ return (
363
+ <li className="predictive-search-result-item" key={product.id}>
364
+ <Link to={productUrl} onClick={closeSearch}>
365
+ + {image && (
366
+ + <Image
367
+ + alt={image.altText ?? ''}
368
+ + src={image.url}
369
+ + width={50}
370
+ + height={50}
371
+ + />
372
+ + )}
373
+ <div>
374
+ <p>{product.title}</p>
375
+ <small>
376
+ {product?.variants?.nodes?.[0].price && (
377
+ <Money
378
+ data={product.variants.nodes[0].price}
379
+ />
380
+ )}
381
+ </small>
382
+ </div>
383
+ </Link>
384
+ </li>
385
+ );
386
+ })}
387
+ </ul>
388
+ </div>
389
+ )
390
+ };
391
+ ```
@@ -0,0 +1,333 @@
1
+ # Hydrogen Search
2
+
3
+ Our skeleton template ships with a `/search` route and a set of components to easily
4
+ implement a traditional search flow.
5
+
6
+ This integration uses the storefront API (SFAPI) [search](https://shopify.dev/docs/api/storefront/latest/queries/search)
7
+ endpoint to retrieve search results based on a search term.
8
+
9
+ ## Components Architecture
10
+
11
+ ![alt text](./search.jpg)
12
+
13
+ ## Components
14
+
15
+ | File | Description |
16
+ | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
17
+ | [`app/components/SearchForm.tsx`](app/components/SearchForm.tsx) | A fully customizable form component configured to make (server-side) form `GET` requests to the `/search` route. |
18
+ | [`app/components/SearchResults.tsx`](app/components/SearchResults.tsx) | A fully customizable search results wrapper, that provides compound components to render `articles`, `pages` and `products` |
19
+
20
+ ## Instructions
21
+
22
+ ### 1. Create the search route
23
+
24
+ Create a new file at `/routes/search.tsx`
25
+
26
+ ### 3. Add `search` query and fetcher
27
+
28
+ The search fetcher parses the `q` parameter and performs the search SFAPI request.
29
+
30
+ ```ts
31
+ /**
32
+ * Regular search query and fragments
33
+ * (adjust as needed)
34
+ */
35
+ const SEARCH_PRODUCT_FRAGMENT = `#graphql
36
+ fragment SearchProduct on Product {
37
+ __typename
38
+ handle
39
+ id
40
+ publishedAt
41
+ title
42
+ trackingParameters
43
+ vendor
44
+ variants(first: 1) {
45
+ nodes {
46
+ id
47
+ image {
48
+ url
49
+ altText
50
+ width
51
+ height
52
+ }
53
+ price {
54
+ amount
55
+ currencyCode
56
+ }
57
+ compareAtPrice {
58
+ amount
59
+ currencyCode
60
+ }
61
+ selectedOptions {
62
+ name
63
+ value
64
+ }
65
+ product {
66
+ handle
67
+ title
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ` as const;
73
+
74
+ const SEARCH_PAGE_FRAGMENT = `#graphql
75
+ fragment SearchPage on Page {
76
+ __typename
77
+ handle
78
+ id
79
+ title
80
+ trackingParameters
81
+ }
82
+ ` as const;
83
+
84
+ const SEARCH_ARTICLE_FRAGMENT = `#graphql
85
+ fragment SearchArticle on Article {
86
+ __typename
87
+ handle
88
+ id
89
+ title
90
+ trackingParameters
91
+ }
92
+ ` as const;
93
+
94
+ const PAGE_INFO_FRAGMENT = `#graphql
95
+ fragment PageInfoFragment on PageInfo {
96
+ hasNextPage
97
+ hasPreviousPage
98
+ startCursor
99
+ endCursor
100
+ }
101
+ ` as const;
102
+
103
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/queries/search
104
+ export const SEARCH_QUERY = `#graphql
105
+ query Search(
106
+ $country: CountryCode
107
+ $endCursor: String
108
+ $first: Int
109
+ $language: LanguageCode
110
+ $last: Int
111
+ $term: String!
112
+ $startCursor: String
113
+ ) @inContext(country: $country, language: $language) {
114
+ articles: search(
115
+ query: $term,
116
+ types: [ARTICLE],
117
+ first: $first,
118
+ ) {
119
+ nodes {
120
+ ...on Article {
121
+ ...SearchArticle
122
+ }
123
+ }
124
+ }
125
+ pages: search(
126
+ query: $term,
127
+ types: [PAGE],
128
+ first: $first,
129
+ ) {
130
+ nodes {
131
+ ...on Page {
132
+ ...SearchPage
133
+ }
134
+ }
135
+ }
136
+ products: search(
137
+ after: $endCursor,
138
+ before: $startCursor,
139
+ first: $first,
140
+ last: $last,
141
+ query: $term,
142
+ sortKey: RELEVANCE,
143
+ types: [PRODUCT],
144
+ unavailableProducts: HIDE,
145
+ ) {
146
+ nodes {
147
+ ...on Product {
148
+ ...SearchProduct
149
+ }
150
+ }
151
+ pageInfo {
152
+ ...PageInfoFragment
153
+ }
154
+ }
155
+ }
156
+ ${SEARCH_PRODUCT_FRAGMENT}
157
+ ${SEARCH_PAGE_FRAGMENT}
158
+ ${SEARCH_ARTICLE_FRAGMENT}
159
+ ${PAGE_INFO_FRAGMENT}
160
+ ` as const;
161
+
162
+ /**
163
+ * Regular search fetcher
164
+ */
165
+ async function search({
166
+ request,
167
+ context,
168
+ }: Pick<LoaderFunctionArgs, 'request' | 'context'>) {
169
+ const {storefront} = context;
170
+ const url = new URL(request.url);
171
+ const searchParams = new URLSearchParams(url.search);
172
+ const variables = getPaginationVariables(request, {pageBy: 8});
173
+ const term = String(searchParams.get('q') || '');
174
+
175
+ // Search articles, pages, and products for the `q` term
176
+ const {errors, ...items} = await storefront.query(SEARCH_QUERY, {
177
+ variables: {...variables, term},
178
+ });
179
+
180
+ if (!items) {
181
+ throw new Error('No search data returned from Shopify API');
182
+ }
183
+
184
+ if (errors) {
185
+ throw new Error(errors[0].message);
186
+ }
187
+
188
+ const total = Object.values(items).reduce((acc, {nodes}) => {
189
+ return acc + nodes.length;
190
+ }, 0);
191
+
192
+ return json({term, result: {total, items}});
193
+ }
194
+ ```
195
+
196
+ ### 3. Add a `loader` export to the route
197
+
198
+ This loader receives and processes `GET` requests from the `<SearchForm />` component.
199
+
200
+ A `q` URL parameter will be used as the search term and appended automatically by
201
+ the form if present in it's children prop
202
+
203
+ ```ts
204
+ /**
205
+ * Handles regular search GET requests
206
+ * requested by the SearchForm component and /search route visits
207
+ */
208
+ export async function loader({request, context}: LoaderFunctionArgs) {
209
+ const url = new URL(request.url);
210
+ const isRegular = !url.searchParams.has('predictive');
211
+
212
+ if (!isRegular) {
213
+ return json({})
214
+ }
215
+
216
+ const searchPromise = regularSearch({request, context});
217
+
218
+ searchPromise.catch((error: Error) => {
219
+ console.error(error);
220
+ return {term: '', result: null, error: error.message};
221
+ });
222
+
223
+ return json(await searchPromise);
224
+ }
225
+ ```
226
+
227
+ ### 4. Render the search form and results
228
+
229
+ Finally, create a default export to render both the search form and the search results
230
+
231
+ ```ts
232
+ import {SearchForm} from '~/components/SearchForm';
233
+ import {SearchResults} from '~/components/SearchResults';
234
+
235
+ /**
236
+ * Renders the /search route
237
+ */
238
+ export default function SearchPage() {
239
+ const {term, result} = useLoaderData<typeof loader>();
240
+
241
+ return (
242
+ <div className="search">
243
+ <h1>Search</h1>
244
+ <SearchForm>
245
+ {({inputRef}) => (
246
+ <>
247
+ <input
248
+ defaultValue={term}
249
+ name="q"
250
+ placeholder="Search…"
251
+ ref={inputRef}
252
+ type="search"
253
+ />
254
+ &nbsp;
255
+ <button type="submit">Search</button>
256
+ </>
257
+ )}
258
+ </SearchForm>
259
+ {!term || !result?.total ? (
260
+ <SearchResults.Empty />
261
+ ) : (
262
+ <SearchResults result={result} term={term}>
263
+ {({articles, pages, products, term}) => (
264
+ <div>
265
+ <SearchResults.Products products={products} term={term} />
266
+ <SearchResults.Pages pages={pages} term={term} />
267
+ <SearchResults.Articles articles={articles} term={term} />
268
+ </div>
269
+ )}
270
+ </SearchResults>
271
+ )}
272
+ </div>
273
+ );
274
+ }
275
+ ```
276
+
277
+ ## Additional Notes
278
+
279
+ ### How to use a different URL search parameter?
280
+
281
+ - Modify the `name` attribute in the forms input element. e.g
282
+
283
+ ```ts
284
+ <input name="query" />`.
285
+ ```
286
+
287
+ - Modify the search fetcher term variable to parse the new name. e.g
288
+
289
+ ```ts
290
+ const term = String(searchParams.get('query') || '');
291
+ ```
292
+
293
+ ### How to customize the way the results look?
294
+
295
+ Simply go to `/app/components/SearchResults.txx` and look for the compound component you
296
+ want to modify.
297
+
298
+ For example, let's render articles in a horizontal flex container
299
+
300
+ ```diff
301
+ SearchResults.Pages = function({
302
+ pages,
303
+ term,
304
+ }: {
305
+ pages: SearchItems['pages'];
306
+ term: string;
307
+ }) {
308
+ if (!pages?.nodes.length) {
309
+ return null;
310
+ }
311
+ return (
312
+ <div className="search-result">
313
+ <h2>Pages</h2>
314
+ + <div className="flex">
315
+ {pages?.nodes?.map((page) => {
316
+ const pageUrl = urlWithTrackingParams({
317
+ baseUrl: `/pages/${page.handle}`,
318
+ trackingParams: page.trackingParameters,
319
+ term,
320
+ });
321
+ return (
322
+ <div className="search-results-item" key={page.id}>
323
+ <Link prefetch="intent" to={pageUrl}>
324
+ {page.title}
325
+ </Link>
326
+ </div>
327
+ );
328
+ })}
329
+ </div>
330
+ </div>
331
+ );
332
+ };
333
+ ```