@shopify/cli-hydrogen 5.0.2 → 5.1.0

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 (243) hide show
  1. package/dist/commands/hydrogen/build.js +16 -2
  2. package/dist/commands/hydrogen/codegen-unstable.js +13 -24
  3. package/dist/commands/hydrogen/dev.js +45 -39
  4. package/dist/commands/hydrogen/env/list.js +25 -24
  5. package/dist/commands/hydrogen/env/list.test.js +46 -43
  6. package/dist/commands/hydrogen/env/pull.js +53 -25
  7. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  8. package/dist/commands/hydrogen/generate/route.js +31 -132
  9. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  10. package/dist/commands/hydrogen/init.js +46 -127
  11. package/dist/commands/hydrogen/init.test.js +352 -100
  12. package/dist/commands/hydrogen/link.js +70 -69
  13. package/dist/commands/hydrogen/link.test.js +72 -107
  14. package/dist/commands/hydrogen/list.js +22 -12
  15. package/dist/commands/hydrogen/list.test.js +51 -48
  16. package/dist/commands/hydrogen/login.js +31 -0
  17. package/dist/commands/hydrogen/logout.js +21 -0
  18. package/dist/commands/hydrogen/setup/css.js +79 -0
  19. package/dist/commands/hydrogen/setup/markets.js +53 -0
  20. package/dist/commands/hydrogen/setup.js +133 -0
  21. package/dist/commands/hydrogen/shortcut.js +2 -45
  22. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  23. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  24. package/dist/generator-templates/assets/postcss/package.json +10 -0
  25. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  26. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  27. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  28. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  29. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  30. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  31. package/dist/generator-templates/starter/.eslintignore +5 -0
  32. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  33. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  34. package/dist/generator-templates/starter/README.md +40 -0
  35. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  36. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  37. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  38. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  39. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  40. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  41. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  42. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  43. package/dist/generator-templates/starter/app/root.tsx +270 -0
  44. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  45. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  46. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  47. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  48. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  49. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  50. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  51. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  52. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  53. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  54. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  55. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  56. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  57. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  58. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  59. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  60. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  61. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  62. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  63. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  64. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  65. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  66. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  67. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  68. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  69. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  70. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  71. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  72. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  73. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  74. package/dist/generator-templates/starter/app/utils.ts +46 -0
  75. package/dist/generator-templates/starter/package.json +43 -0
  76. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  77. package/dist/generator-templates/starter/remix.config.js +26 -0
  78. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  79. package/dist/generator-templates/starter/server.ts +253 -0
  80. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  81. package/dist/generator-templates/starter/tsconfig.json +22 -0
  82. package/dist/lib/auth.js +123 -0
  83. package/dist/lib/auth.test.js +157 -0
  84. package/dist/lib/build.js +51 -0
  85. package/dist/lib/check-version.js +3 -3
  86. package/dist/lib/check-version.test.js +24 -0
  87. package/dist/lib/codegen.js +26 -17
  88. package/dist/lib/environment-variables.js +68 -0
  89. package/dist/lib/environment-variables.test.js +147 -0
  90. package/dist/lib/file.js +41 -0
  91. package/dist/lib/file.test.js +69 -0
  92. package/dist/lib/flags.js +39 -2
  93. package/dist/lib/format-code.js +26 -0
  94. package/dist/lib/gid.js +12 -0
  95. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  96. package/dist/lib/graphql/admin/client.js +27 -0
  97. package/dist/lib/graphql/admin/client.test.js +51 -0
  98. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  99. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  100. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  101. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  102. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  103. package/dist/lib/graphql/admin/list-environments.js +2 -2
  104. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  105. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  106. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  107. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  108. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  109. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  110. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  111. package/dist/lib/log.js +185 -9
  112. package/dist/lib/log.test.js +92 -0
  113. package/dist/lib/mini-oxygen.js +19 -9
  114. package/dist/lib/missing-routes.js +0 -2
  115. package/dist/lib/onboarding/common.js +456 -0
  116. package/dist/lib/onboarding/index.js +2 -0
  117. package/dist/lib/onboarding/local.js +229 -0
  118. package/dist/lib/onboarding/remote.js +89 -0
  119. package/dist/lib/remix-version-interop.js +5 -5
  120. package/dist/lib/remix-version-interop.test.js +11 -1
  121. package/dist/lib/render-errors.js +13 -11
  122. package/dist/lib/setups/css/assets.js +89 -0
  123. package/dist/lib/setups/css/css-modules.js +22 -0
  124. package/dist/lib/setups/css/index.js +44 -0
  125. package/dist/lib/setups/css/postcss.js +34 -0
  126. package/dist/lib/setups/css/replacers.js +137 -0
  127. package/dist/lib/setups/css/tailwind.js +54 -0
  128. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  129. package/dist/lib/setups/i18n/domains.test.js +25 -0
  130. package/dist/lib/setups/i18n/index.js +46 -0
  131. package/dist/lib/setups/i18n/replacers.js +227 -0
  132. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  133. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  134. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  135. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  136. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  137. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  138. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  139. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  140. package/dist/lib/setups/routes/generate.js +244 -0
  141. package/dist/lib/setups/routes/generate.test.js +313 -0
  142. package/dist/lib/shell.js +52 -5
  143. package/dist/lib/shell.test.js +42 -16
  144. package/dist/lib/shopify-config.js +23 -18
  145. package/dist/lib/shopify-config.test.js +63 -73
  146. package/dist/lib/template-downloader.js +9 -7
  147. package/dist/lib/transpile-ts.js +9 -29
  148. package/dist/virtual-routes/routes/index.jsx +40 -19
  149. package/oclif.manifest.json +710 -1
  150. package/package.json +16 -16
  151. package/dist/commands/hydrogen/build.d.ts +0 -23
  152. package/dist/commands/hydrogen/check.d.ts +0 -15
  153. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  154. package/dist/commands/hydrogen/dev.d.ts +0 -21
  155. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  156. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  157. package/dist/commands/hydrogen/g.d.ts +0 -10
  158. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  159. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  160. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  161. package/dist/commands/hydrogen/init.d.ts +0 -24
  162. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  163. package/dist/commands/hydrogen/link.d.ts +0 -23
  164. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/list.d.ts +0 -21
  166. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  167. package/dist/commands/hydrogen/preview.d.ts +0 -17
  168. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  169. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  171. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  172. package/dist/create-app.d.ts +0 -1
  173. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  174. package/dist/generator-templates/routes/account/login.tsx +0 -103
  175. package/dist/generator-templates/routes/account/register.tsx +0 -103
  176. package/dist/generator-templates/routes/cart.tsx +0 -81
  177. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  178. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  179. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  180. package/dist/generator-templates/routes/index.tsx +0 -40
  181. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  182. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  183. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  184. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  185. package/dist/hooks/init.d.ts +0 -5
  186. package/dist/lib/admin-session.d.ts +0 -6
  187. package/dist/lib/admin-session.js +0 -16
  188. package/dist/lib/admin-session.test.d.ts +0 -1
  189. package/dist/lib/admin-session.test.js +0 -27
  190. package/dist/lib/admin-urls.d.ts +0 -8
  191. package/dist/lib/check-lockfile.d.ts +0 -3
  192. package/dist/lib/check-lockfile.test.d.ts +0 -1
  193. package/dist/lib/check-version.d.ts +0 -16
  194. package/dist/lib/check-version.test.d.ts +0 -1
  195. package/dist/lib/codegen.d.ts +0 -26
  196. package/dist/lib/combined-environment-variables.d.ts +0 -8
  197. package/dist/lib/combined-environment-variables.js +0 -57
  198. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  199. package/dist/lib/combined-environment-variables.test.js +0 -111
  200. package/dist/lib/config.d.ts +0 -20
  201. package/dist/lib/flags.d.ts +0 -27
  202. package/dist/lib/flags.test.d.ts +0 -1
  203. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  204. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  205. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  206. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  207. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  208. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  209. package/dist/lib/graphql.d.ts +0 -21
  210. package/dist/lib/graphql.js +0 -18
  211. package/dist/lib/graphql.test.d.ts +0 -1
  212. package/dist/lib/log.d.ts +0 -6
  213. package/dist/lib/mini-oxygen.d.ts +0 -22
  214. package/dist/lib/missing-routes.d.ts +0 -8
  215. package/dist/lib/missing-routes.test.d.ts +0 -1
  216. package/dist/lib/missing-storefronts.d.ts +0 -5
  217. package/dist/lib/missing-storefronts.js +0 -18
  218. package/dist/lib/process.d.ts +0 -6
  219. package/dist/lib/pull-environment-variables.d.ts +0 -20
  220. package/dist/lib/pull-environment-variables.js +0 -57
  221. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  222. package/dist/lib/pull-environment-variables.test.js +0 -174
  223. package/dist/lib/remix-version-interop.d.ts +0 -11
  224. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  225. package/dist/lib/render-errors.d.ts +0 -16
  226. package/dist/lib/shell.d.ts +0 -11
  227. package/dist/lib/shell.test.d.ts +0 -1
  228. package/dist/lib/shop.d.ts +0 -7
  229. package/dist/lib/shop.js +0 -32
  230. package/dist/lib/shop.test.d.ts +0 -1
  231. package/dist/lib/shop.test.js +0 -78
  232. package/dist/lib/shopify-config.d.ts +0 -35
  233. package/dist/lib/shopify-config.test.d.ts +0 -1
  234. package/dist/lib/string.d.ts +0 -3
  235. package/dist/lib/string.test.d.ts +0 -1
  236. package/dist/lib/template-downloader.d.ts +0 -6
  237. package/dist/lib/transpile-ts.d.ts +0 -16
  238. package/dist/lib/user-errors.d.ts +0 -9
  239. package/dist/lib/user-errors.js +0 -11
  240. package/dist/lib/virtual-routes.d.ts +0 -7
  241. package/dist/lib/virtual-routes.test.d.ts +0 -1
  242. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  243. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -0,0 +1,342 @@
1
+ import {json, type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import type {
3
+ NormalizedPredictiveSearch,
4
+ NormalizedPredictiveSearchResults,
5
+ } from '~/components/Search';
6
+ import {NO_PREDICTIVE_SEARCH_RESULTS} from '~/components/Search';
7
+
8
+ import type {
9
+ PredictiveArticleFragment,
10
+ PredictiveCollectionFragment,
11
+ PredictivePageFragment,
12
+ PredictiveProductFragment,
13
+ PredictiveQueryFragment,
14
+ PredictiveSearchQuery,
15
+ } from 'storefrontapi.generated';
16
+
17
+ type PredictiveSearchResultItem =
18
+ | PredictiveArticleFragment
19
+ | PredictiveCollectionFragment
20
+ | PredictivePageFragment
21
+ | PredictiveProductFragment;
22
+
23
+ type PredictiveSearchTypes =
24
+ | 'ARTICLE'
25
+ | 'COLLECTION'
26
+ | 'PAGE'
27
+ | 'PRODUCT'
28
+ | 'QUERY';
29
+
30
+ const DEFAULT_SEARCH_TYPES: PredictiveSearchTypes[] = [
31
+ 'ARTICLE',
32
+ 'COLLECTION',
33
+ 'PAGE',
34
+ 'PRODUCT',
35
+ 'QUERY',
36
+ ];
37
+
38
+ /**
39
+ * Fetches the search results from the predictive search API
40
+ * requested by the SearchForm component
41
+ */
42
+ export async function action({request, params, context}: LoaderArgs) {
43
+ if (request.method !== 'POST') {
44
+ throw new Error('Invalid request method');
45
+ }
46
+
47
+ const search = await fetchPredictiveSearchResults({
48
+ params,
49
+ request,
50
+ context,
51
+ });
52
+
53
+ return json(search);
54
+ }
55
+
56
+ async function fetchPredictiveSearchResults({
57
+ params,
58
+ request,
59
+ context,
60
+ }: Pick<LoaderArgs, 'params' | 'context' | 'request'>) {
61
+ const url = new URL(request.url);
62
+ const searchParams = new URLSearchParams(url.search);
63
+ let body;
64
+ try {
65
+ body = await request.formData();
66
+ } catch (error) {}
67
+ const searchTerm = String(body?.get('q') || searchParams.get('q') || '');
68
+ const limit = Number(body?.get('limit') || searchParams.get('limit') || 10);
69
+ const rawTypes = String(
70
+ body?.get('type') || searchParams.get('type') || 'ANY',
71
+ );
72
+ const searchTypes =
73
+ rawTypes === 'ANY'
74
+ ? DEFAULT_SEARCH_TYPES
75
+ : rawTypes
76
+ .split(',')
77
+ .map((t) => t.toUpperCase() as PredictiveSearchTypes)
78
+ .filter((t) => DEFAULT_SEARCH_TYPES.includes(t));
79
+
80
+ if (!searchTerm) {
81
+ return {
82
+ searchResults: {results: null, totalResults: 0},
83
+ searchTerm,
84
+ searchTypes,
85
+ };
86
+ }
87
+
88
+ const data = await context.storefront.query(PREDICTIVE_SEARCH_QUERY, {
89
+ variables: {
90
+ limit,
91
+ limitScope: 'EACH',
92
+ searchTerm,
93
+ types: searchTypes,
94
+ },
95
+ });
96
+
97
+ if (!data) {
98
+ throw new Error('No data returned from Shopify API');
99
+ }
100
+
101
+ const searchResults = normalizePredictiveSearchResults(
102
+ data.predictiveSearch,
103
+ params.locale,
104
+ );
105
+
106
+ return {searchResults, searchTerm, searchTypes};
107
+ }
108
+
109
+ /**
110
+ * Normalize results and apply tracking qurery parameters to each result url
111
+ * @param predictiveSearch
112
+ * @param locale
113
+ */
114
+ export function normalizePredictiveSearchResults(
115
+ predictiveSearch: PredictiveSearchQuery['predictiveSearch'],
116
+ locale: LoaderArgs['params']['locale'],
117
+ ): NormalizedPredictiveSearch {
118
+ let totalResults = 0;
119
+ if (!predictiveSearch) {
120
+ return {
121
+ results: NO_PREDICTIVE_SEARCH_RESULTS,
122
+ totalResults,
123
+ };
124
+ }
125
+
126
+ function applyTrackingParams(
127
+ resource: PredictiveSearchResultItem | PredictiveQueryFragment,
128
+ params?: string,
129
+ ) {
130
+ if (params) {
131
+ return resource.trackingParameters
132
+ ? `?${params}&${resource.trackingParameters}`
133
+ : `?${params}`;
134
+ } else {
135
+ return resource.trackingParameters
136
+ ? `?${resource.trackingParameters}`
137
+ : '';
138
+ }
139
+ }
140
+
141
+ const localePrefix = locale ? `/${locale}` : '';
142
+ const results: NormalizedPredictiveSearchResults = [];
143
+
144
+ if (predictiveSearch.queries.length) {
145
+ results.push({
146
+ type: 'queries',
147
+ items: predictiveSearch.queries.map((query: PredictiveQueryFragment) => {
148
+ const trackingParams = applyTrackingParams(
149
+ query,
150
+ `q=${encodeURIComponent(query.text)}`,
151
+ );
152
+
153
+ totalResults++;
154
+ return {
155
+ __typename: query.__typename,
156
+ handle: '',
157
+ id: query.text,
158
+ image: undefined,
159
+ title: query.text,
160
+ styledTitle: query.styledText,
161
+ url: `${localePrefix}/search${trackingParams}`,
162
+ };
163
+ }),
164
+ });
165
+ }
166
+
167
+ if (predictiveSearch.products.length) {
168
+ results.push({
169
+ type: 'products',
170
+ items: predictiveSearch.products.map(
171
+ (product: PredictiveProductFragment) => {
172
+ totalResults++;
173
+ const trackingParams = applyTrackingParams(product);
174
+ return {
175
+ __typename: product.__typename,
176
+ handle: product.handle,
177
+ id: product.id,
178
+ image: product.variants?.nodes?.[0]?.image,
179
+ title: product.title,
180
+ url: `${localePrefix}/products/${product.handle}${trackingParams}`,
181
+ price: product.variants.nodes[0].price,
182
+ };
183
+ },
184
+ ),
185
+ });
186
+ }
187
+
188
+ if (predictiveSearch.collections.length) {
189
+ results.push({
190
+ type: 'collections',
191
+ items: predictiveSearch.collections.map(
192
+ (collection: PredictiveCollectionFragment) => {
193
+ totalResults++;
194
+ const trackingParams = applyTrackingParams(collection);
195
+ return {
196
+ __typename: collection.__typename,
197
+ handle: collection.handle,
198
+ id: collection.id,
199
+ image: collection.image,
200
+ title: collection.title,
201
+ url: `${localePrefix}/collections/${collection.handle}${trackingParams}`,
202
+ };
203
+ },
204
+ ),
205
+ });
206
+ }
207
+
208
+ if (predictiveSearch.pages.length) {
209
+ results.push({
210
+ type: 'pages',
211
+ items: predictiveSearch.pages.map((page: PredictivePageFragment) => {
212
+ totalResults++;
213
+ const trackingParams = applyTrackingParams(page);
214
+ return {
215
+ __typename: page.__typename,
216
+ handle: page.handle,
217
+ id: page.id,
218
+ image: undefined,
219
+ title: page.title,
220
+ url: `${localePrefix}/pages/${page.handle}${trackingParams}`,
221
+ };
222
+ }),
223
+ });
224
+ }
225
+
226
+ if (predictiveSearch.articles.length) {
227
+ results.push({
228
+ type: 'articles',
229
+ items: predictiveSearch.articles.map(
230
+ (article: PredictiveArticleFragment) => {
231
+ totalResults++;
232
+ const trackingParams = applyTrackingParams(article);
233
+ return {
234
+ __typename: article.__typename,
235
+ handle: article.handle,
236
+ id: article.id,
237
+ image: article.image,
238
+ title: article.title,
239
+ url: `${localePrefix}/blog/${article.handle}${trackingParams}`,
240
+ };
241
+ },
242
+ ),
243
+ });
244
+ }
245
+
246
+ return {results, totalResults};
247
+ }
248
+
249
+ const PREDICTIVE_SEARCH_QUERY = `#graphql
250
+ fragment PredictiveArticle on Article {
251
+ __typename
252
+ id
253
+ title
254
+ handle
255
+ image {
256
+ url
257
+ altText
258
+ width
259
+ height
260
+ }
261
+ trackingParameters
262
+ }
263
+ fragment PredictiveCollection on Collection {
264
+ __typename
265
+ id
266
+ title
267
+ handle
268
+ image {
269
+ url
270
+ altText
271
+ width
272
+ height
273
+ }
274
+ trackingParameters
275
+ }
276
+ fragment PredictivePage on Page {
277
+ __typename
278
+ id
279
+ title
280
+ handle
281
+ trackingParameters
282
+ }
283
+ fragment PredictiveProduct on Product {
284
+ __typename
285
+ id
286
+ title
287
+ handle
288
+ trackingParameters
289
+ variants(first: 1) {
290
+ nodes {
291
+ id
292
+ image {
293
+ url
294
+ altText
295
+ width
296
+ height
297
+ }
298
+ price {
299
+ amount
300
+ currencyCode
301
+ }
302
+ }
303
+ }
304
+ }
305
+ fragment PredictiveQuery on SearchQuerySuggestion {
306
+ __typename
307
+ text
308
+ styledText
309
+ trackingParameters
310
+ }
311
+ query predictiveSearch(
312
+ $country: CountryCode
313
+ $language: LanguageCode
314
+ $limit: Int!
315
+ $limitScope: PredictiveSearchLimitScope!
316
+ $searchTerm: String!
317
+ $types: [PredictiveSearchType!]
318
+ ) @inContext(country: $country, language: $language) {
319
+ predictiveSearch(
320
+ limit: $limit,
321
+ limitScope: $limitScope,
322
+ query: $searchTerm,
323
+ types: $types,
324
+ ) {
325
+ articles {
326
+ ...PredictiveArticle
327
+ }
328
+ collections {
329
+ ...PredictiveCollection
330
+ }
331
+ pages {
332
+ ...PredictivePage
333
+ }
334
+ products {
335
+ ...PredictiveProduct
336
+ }
337
+ queries {
338
+ ...PredictiveQuery
339
+ }
340
+ }
341
+ }
342
+ ` as const;
@@ -0,0 +1,88 @@
1
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
+ import {json, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import {useLoaderData} from '@remix-run/react';
4
+ import {Image} from '@shopify/hydrogen';
5
+
6
+ export const meta: V2_MetaFunction = ({data}) => {
7
+ return [{title: `Hydrogen | ${data.article.title} article`}];
8
+ };
9
+
10
+ export async function loader({params, context}: LoaderArgs) {
11
+ const {blogHandle, articleHandle} = params;
12
+
13
+ if (!articleHandle || !blogHandle) {
14
+ throw new Response('Not found', {status: 404});
15
+ }
16
+
17
+ const {blog} = await context.storefront.query(ARTICLE_QUERY, {
18
+ variables: {blogHandle, articleHandle},
19
+ });
20
+
21
+ if (!blog?.articleByHandle) {
22
+ throw new Response(null, {status: 404});
23
+ }
24
+
25
+ const article = blog.articleByHandle;
26
+
27
+ return json({article});
28
+ }
29
+
30
+ export default function Article() {
31
+ const {article} = useLoaderData<typeof loader>();
32
+ const {title, image, contentHtml, author} = article;
33
+
34
+ const publishedDate = new Intl.DateTimeFormat('en-US', {
35
+ year: 'numeric',
36
+ month: 'long',
37
+ day: 'numeric',
38
+ }).format(new Date(article.publishedAt));
39
+
40
+ return (
41
+ <div className="article">
42
+ <h1>
43
+ {title}
44
+ <span>
45
+ {publishedDate} &middot; {author?.name}
46
+ </span>
47
+ </h1>
48
+
49
+ {image && <Image data={image} sizes="90vw" loading="eager" />}
50
+ <div
51
+ dangerouslySetInnerHTML={{__html: contentHtml}}
52
+ className="article"
53
+ />
54
+ </div>
55
+ );
56
+ }
57
+
58
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog#field-blog-articlebyhandle
59
+ const ARTICLE_QUERY = `#graphql
60
+ query Article(
61
+ $articleHandle: String!
62
+ $blogHandle: String!
63
+ $country: CountryCode
64
+ $language: LanguageCode
65
+ ) @inContext(language: $language, country: $country) {
66
+ blog(handle: $blogHandle) {
67
+ articleByHandle(handle: $articleHandle) {
68
+ title
69
+ contentHtml
70
+ publishedAt
71
+ author: authorV2 {
72
+ name
73
+ }
74
+ image {
75
+ id
76
+ altText
77
+ url
78
+ width
79
+ height
80
+ }
81
+ seo {
82
+ description
83
+ title
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ` as const;
@@ -0,0 +1,162 @@
1
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
+ import {json, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import {Link, useLoaderData} from '@remix-run/react';
4
+ import {Image, Pagination, getPaginationVariables} from '@shopify/hydrogen';
5
+ import type {ArticleItemFragment} from 'storefrontapi.generated';
6
+
7
+ export const meta: V2_MetaFunction = ({data}) => {
8
+ return [{title: `Hydrogen | ${data.blog.title} blog`}];
9
+ };
10
+
11
+ export const loader = async ({
12
+ request,
13
+ params,
14
+ context: {storefront},
15
+ }: LoaderArgs) => {
16
+ const paginationVariables = getPaginationVariables(request, {
17
+ pageBy: 4,
18
+ });
19
+
20
+ if (!params.blogHandle) {
21
+ throw new Response(`blog not found`, {status: 404});
22
+ }
23
+
24
+ const {blog} = await storefront.query(BLOGS_QUERY, {
25
+ variables: {
26
+ blogHandle: params.blogHandle,
27
+ ...paginationVariables,
28
+ },
29
+ });
30
+
31
+ if (!blog?.articles) {
32
+ throw new Response('Not found', {status: 404});
33
+ }
34
+
35
+ return json({blog});
36
+ };
37
+
38
+ export default function Blog() {
39
+ const {blog} = useLoaderData<typeof loader>();
40
+ const {articles} = blog;
41
+
42
+ return (
43
+ <div className="blog">
44
+ <h1>{blog.title}</h1>
45
+ <div className="blog-grid">
46
+ <Pagination connection={articles}>
47
+ {({nodes, isLoading, PreviousLink, NextLink}) => {
48
+ return (
49
+ <>
50
+ <PreviousLink>
51
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
52
+ </PreviousLink>
53
+ {nodes.map((article, index) => {
54
+ return (
55
+ <ArticleItem
56
+ article={article}
57
+ key={article.id}
58
+ loading={index < 2 ? 'eager' : 'lazy'}
59
+ />
60
+ );
61
+ })}
62
+ <NextLink>
63
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
64
+ </NextLink>
65
+ </>
66
+ );
67
+ }}
68
+ </Pagination>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ function ArticleItem({
75
+ article,
76
+ loading,
77
+ }: {
78
+ article: ArticleItemFragment;
79
+ loading?: HTMLImageElement['loading'];
80
+ }) {
81
+ const publishedAt = new Intl.DateTimeFormat('en-US', {
82
+ year: 'numeric',
83
+ month: 'long',
84
+ day: 'numeric',
85
+ }).format(new Date(article.publishedAt!));
86
+ return (
87
+ <div className="blog-article" key={article.id}>
88
+ <Link to={`/blogs/${article.blog.handle}/${article.handle}`}>
89
+ {article.image && (
90
+ <div className="blog-article-image">
91
+ <Image
92
+ alt={article.image.altText || article.title}
93
+ aspectRatio="3/2"
94
+ data={article.image}
95
+ loading={loading}
96
+ sizes="(min-width: 768px) 50vw, 100vw"
97
+ />
98
+ </div>
99
+ )}
100
+ <h3>{article.title}</h3>
101
+ <small>{publishedAt}</small>
102
+ </Link>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
108
+ const BLOGS_QUERY = `#graphql
109
+ query Blog(
110
+ $language: LanguageCode
111
+ $blogHandle: String!
112
+ $first: Int
113
+ $last: Int
114
+ $startCursor: String
115
+ $endCursor: String
116
+ ) @inContext(language: $language) {
117
+ blog(handle: $blogHandle) {
118
+ title
119
+ seo {
120
+ title
121
+ description
122
+ }
123
+ articles(
124
+ first: $first,
125
+ last: $last,
126
+ before: $startCursor,
127
+ after: $endCursor
128
+ ) {
129
+ nodes {
130
+ ...ArticleItem
131
+ }
132
+ pageInfo {
133
+ hasPreviousPage
134
+ hasNextPage
135
+ hasNextPage
136
+ endCursor
137
+ }
138
+
139
+ }
140
+ }
141
+ }
142
+ fragment ArticleItem on Article {
143
+ author: authorV2 {
144
+ name
145
+ }
146
+ contentHtml
147
+ handle
148
+ id
149
+ image {
150
+ id
151
+ altText
152
+ url
153
+ width
154
+ height
155
+ }
156
+ publishedAt
157
+ title
158
+ blog {
159
+ handle
160
+ }
161
+ }
162
+ ` as const;
@@ -0,0 +1,94 @@
1
+ import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
+ import {json, type LoaderArgs} from '@shopify/remix-oxygen';
3
+ import {Link, useLoaderData} from '@remix-run/react';
4
+ import {Pagination, getPaginationVariables} from '@shopify/hydrogen';
5
+
6
+ export const meta: V2_MetaFunction = () => {
7
+ return [{title: `Hydrogen | Logs`}];
8
+ };
9
+
10
+ export const loader = async ({request, context: {storefront}}: LoaderArgs) => {
11
+ const paginationVariables = getPaginationVariables(request, {
12
+ pageBy: 10,
13
+ });
14
+
15
+ const {blogs} = await storefront.query(BLOGS_QUERY, {
16
+ variables: {
17
+ ...paginationVariables,
18
+ },
19
+ });
20
+
21
+ return json({blogs});
22
+ };
23
+
24
+ export default function Blogs() {
25
+ const {blogs} = useLoaderData<typeof loader>();
26
+
27
+ return (
28
+ <div className="blogs">
29
+ <h1>Blogs</h1>
30
+ <div className="blogs-grid">
31
+ <Pagination connection={blogs}>
32
+ {({nodes, isLoading, PreviousLink, NextLink}) => {
33
+ return (
34
+ <>
35
+ <PreviousLink>
36
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
37
+ </PreviousLink>
38
+ {nodes.map((blog) => {
39
+ return (
40
+ <Link
41
+ className="blog"
42
+ key={blog.handle}
43
+ prefetch="intent"
44
+ to={`/blogs/${blog.handle}`}
45
+ >
46
+ <h2>{blog.title}</h2>
47
+ </Link>
48
+ );
49
+ })}
50
+ <NextLink>
51
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
52
+ </NextLink>
53
+ </>
54
+ );
55
+ }}
56
+ </Pagination>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/blog
63
+ const BLOGS_QUERY = `#graphql
64
+ query Blogs(
65
+ $country: CountryCode
66
+ $endCursor: String
67
+ $first: Int
68
+ $language: LanguageCode
69
+ $last: Int
70
+ $startCursor: String
71
+ ) @inContext(country: $country, language: $language) {
72
+ blogs(
73
+ first: $first,
74
+ last: $last,
75
+ before: $startCursor,
76
+ after: $endCursor
77
+ ) {
78
+ pageInfo {
79
+ hasNextPage
80
+ hasPreviousPage
81
+ startCursor
82
+ endCursor
83
+ }
84
+ nodes {
85
+ title
86
+ handle
87
+ seo {
88
+ title
89
+ description
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ` as const;