@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,270 @@
1
+ import {
2
+ isRouteErrorResponse,
3
+ useCatch,
4
+ useMatches,
5
+ useRouteError,
6
+ } from '@remix-run/react';
7
+ import {
8
+ defer,
9
+ type LoaderArgs,
10
+ type ErrorBoundaryComponent,
11
+ } from '@shopify/remix-oxygen';
12
+ import {
13
+ Links,
14
+ Meta,
15
+ Outlet,
16
+ Scripts,
17
+ ScrollRestoration,
18
+ useLoaderData,
19
+ } from '@remix-run/react';
20
+ import type {CustomerAccessToken} from '@shopify/hydrogen-react/storefront-api-types';
21
+ import type {HydrogenSession} from '../server';
22
+ import favicon from '../public/favicon.svg';
23
+ import resetStyles from './styles/reset.css';
24
+ import appStyles from './styles/app.css';
25
+ import {Layout} from '~/components/Layout';
26
+
27
+ export function links() {
28
+ return [
29
+ {rel: 'stylesheet', href: resetStyles},
30
+ {rel: 'stylesheet', href: appStyles},
31
+ {
32
+ rel: 'preconnect',
33
+ href: 'https://cdn.shopify.com',
34
+ },
35
+ {
36
+ rel: 'preconnect',
37
+ href: 'https://shop.app',
38
+ },
39
+ {rel: 'icon', type: 'image/svg+xml', href: favicon},
40
+ ];
41
+ }
42
+
43
+ export async function loader({context}: LoaderArgs) {
44
+ const {storefront, session, cart} = context;
45
+ const customerAccessToken = await session.get('customerAccessToken');
46
+ const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;
47
+
48
+ // validate the customer access token is valid
49
+ const {isLoggedIn, headers} = await validateCustomerAccessToken(
50
+ customerAccessToken,
51
+ session,
52
+ );
53
+
54
+ // defer the cart query by not awaiting it
55
+ const cartPromise = cart.get();
56
+
57
+ // defer the footer query (below the fold)
58
+ const footerPromise = storefront.query(FOOTER_QUERY, {
59
+ cache: storefront.CacheLong(),
60
+ variables: {
61
+ footerMenuHandle: 'footer', // Adjust to your footer menu handle
62
+ },
63
+ });
64
+
65
+ // await the header query (above the fold)
66
+ const headerPromise = storefront.query(HEADER_QUERY, {
67
+ cache: storefront.CacheLong(),
68
+ variables: {
69
+ headerMenuHandle: 'main-menu', // Adjust to your header menu handle
70
+ },
71
+ });
72
+
73
+ return defer(
74
+ {
75
+ cart: cartPromise,
76
+ footer: footerPromise,
77
+ header: await headerPromise,
78
+ isLoggedIn,
79
+ publicStoreDomain,
80
+ },
81
+ {headers},
82
+ );
83
+ }
84
+
85
+ export default function App() {
86
+ const data = useLoaderData<typeof loader>();
87
+
88
+ return (
89
+ <html lang="en">
90
+ <head>
91
+ <meta charSet="utf-8" />
92
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
93
+ <Meta />
94
+ <Links />
95
+ </head>
96
+ <body>
97
+ <Layout {...data}>
98
+ <Outlet />
99
+ </Layout>
100
+ <ScrollRestoration />
101
+ <Scripts />
102
+ </body>
103
+ </html>
104
+ );
105
+ }
106
+
107
+ export function ErrorBoundary() {
108
+ const error = useRouteError();
109
+ const [root] = useMatches();
110
+ let errorMessage = 'Unknown error';
111
+ let errorStatus = 500;
112
+
113
+ if (isRouteErrorResponse(error)) {
114
+ errorMessage = error?.data?.message ?? error.data;
115
+ errorStatus = error.status;
116
+ } else if (error instanceof Error) {
117
+ errorMessage = error.message;
118
+ }
119
+
120
+ return (
121
+ <html lang="en">
122
+ <head>
123
+ <meta charSet="utf-8" />
124
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
125
+ <Meta />
126
+ <Links />
127
+ </head>
128
+ <body>
129
+ <Layout {...root.data}>
130
+ <div className="route-error">
131
+ <h1>Oops</h1>
132
+ <h2>{errorStatus}</h2>
133
+ {errorMessage && (
134
+ <fieldset>
135
+ <pre>{errorMessage}</pre>
136
+ </fieldset>
137
+ )}
138
+ </div>
139
+ </Layout>
140
+ <ScrollRestoration />
141
+ <Scripts />
142
+ </body>
143
+ </html>
144
+ );
145
+ }
146
+
147
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
148
+ // eslint-disable-next-line no-console
149
+ console.error(error);
150
+
151
+ return <div>There was an error.</div>;
152
+ };
153
+
154
+ export function CatchBoundary() {
155
+ const caught = useCatch();
156
+ // eslint-disable-next-line no-console
157
+ console.error(caught);
158
+
159
+ return (
160
+ <div>
161
+ There was an error. Status: {caught.status}. Message:{' '}
162
+ {caught.data?.message}
163
+ </div>
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Validates the customer access token and returns a boolean and headers
169
+ * @see https://shopify.dev/docs/api/storefront/latest/objects/CustomerAccessToken
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * //
174
+ * const {isLoggedIn, headers} = await validateCustomerAccessToken(
175
+ * customerAccessToken,
176
+ * session,
177
+ * );
178
+ * ```
179
+ * */
180
+ async function validateCustomerAccessToken(
181
+ customerAccessToken: CustomerAccessToken,
182
+ session: HydrogenSession,
183
+ ) {
184
+ let isLoggedIn = false;
185
+ const headers = new Headers();
186
+ if (!customerAccessToken?.accessToken || !customerAccessToken?.expiresAt) {
187
+ return {isLoggedIn, headers};
188
+ }
189
+ const expiresAt = new Date(customerAccessToken.expiresAt);
190
+ const dateNow = new Date();
191
+ const customerAccessTokenExpired = expiresAt < dateNow;
192
+ if (customerAccessTokenExpired) {
193
+ session.unset('customerAccessToken');
194
+ headers.append('Set-Cookie', await session.commit());
195
+ } else {
196
+ isLoggedIn = true;
197
+ }
198
+
199
+ return {isLoggedIn, headers};
200
+ }
201
+
202
+ const MENU_FRAGMENT = `#graphql
203
+ fragment MenuItem on MenuItem {
204
+ id
205
+ resourceId
206
+ tags
207
+ title
208
+ type
209
+ url
210
+ }
211
+ fragment ChildMenuItem on MenuItem {
212
+ ...MenuItem
213
+ }
214
+ fragment ParentMenuItem on MenuItem {
215
+ ...MenuItem
216
+ items {
217
+ ...ChildMenuItem
218
+ }
219
+ }
220
+ fragment Menu on Menu {
221
+ id
222
+ items {
223
+ ...ParentMenuItem
224
+ }
225
+ }
226
+ ` as const;
227
+
228
+ const HEADER_QUERY = `#graphql
229
+ fragment Shop on Shop {
230
+ id
231
+ name
232
+ description
233
+ primaryDomain {
234
+ url
235
+ }
236
+ brand {
237
+ logo {
238
+ image {
239
+ url
240
+ }
241
+ }
242
+ }
243
+ }
244
+ query Header(
245
+ $country: CountryCode
246
+ $headerMenuHandle: String!
247
+ $language: LanguageCode
248
+ ) @inContext(language: $language, country: $country) {
249
+ shop {
250
+ ...Shop
251
+ }
252
+ menu(handle: $headerMenuHandle) {
253
+ ...Menu
254
+ }
255
+ }
256
+ ${MENU_FRAGMENT}
257
+ ` as const;
258
+
259
+ const FOOTER_QUERY = `#graphql
260
+ query Footer(
261
+ $country: CountryCode
262
+ $footerMenuHandle: String!
263
+ $language: LanguageCode
264
+ ) @inContext(language: $language, country: $country) {
265
+ menu(handle: $footerMenuHandle) {
266
+ ...Menu
267
+ }
268
+ }
269
+ ${MENU_FRAGMENT}
270
+ ` as const;
@@ -0,0 +1,7 @@
1
+ import type {LoaderArgs} from '@shopify/remix-oxygen';
2
+
3
+ export async function loader({request}: LoaderArgs) {
4
+ throw new Response(`${new URL(request.url).pathname} not found`, {
5
+ status: 404,
6
+ });
7
+ }
@@ -1,60 +1,50 @@
1
- import {
2
- type LoaderArgs,
3
- type ErrorBoundaryComponent,
4
- } from '@shopify/remix-oxygen';
5
- import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
6
- import type {Shop} from '@shopify/hydrogen/storefront-api-types';
1
+ import {type LoaderArgs} from '@shopify/remix-oxygen';
2
+ import {useRouteError, isRouteErrorResponse} from '@remix-run/react';
7
3
  import {parseGid} from '@shopify/hydrogen';
8
4
 
9
- export const loader = async ({request, context}: LoaderArgs) => {
5
+ export async function loader({request, context}: LoaderArgs) {
10
6
  const url = new URL(request.url);
11
7
 
12
- const {shop} = await context.storefront.query<{shop: Pick<Shop, 'id'>}>(
13
- SHOP_QUERY,
14
- );
15
-
16
- return new Response(
17
- robotsTxtData({url: url.origin, shopId: parseGid(shop.id).id}),
18
- {
19
- status: 200,
20
- headers: {
21
- // eslint-disable-next-line @typescript-eslint/naming-convention
22
- 'Content-Type': 'text/plain',
23
- // eslint-disable-next-line @typescript-eslint/naming-convention
24
- 'Cache-Control': `max-age=${60 * 60 * 24}`,
25
- },
26
- },
27
- );
28
- };
29
-
30
- export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
31
- console.error(error);
8
+ const {shop} = await context.storefront.query(ROBOTS_QUERY);
32
9
 
33
- return <div>There was an error.</div>;
34
- };
10
+ const shopId = parseGid(shop.id).id;
11
+ const body = robotsTxtData({url: url.origin, shopId});
35
12
 
36
- export function CatchBoundary() {
37
- const caught = useCatch();
38
- console.error(caught);
13
+ return new Response(body, {
14
+ status: 200,
15
+ headers: {
16
+ 'Content-Type': 'text/plain',
39
17
 
40
- return (
41
- <div>
42
- There was an error. Status: {caught.status}. Message:{' '}
43
- {caught.data?.message}
44
- </div>
45
- );
18
+ 'Cache-Control': `max-age=${60 * 60 * 24}`,
19
+ },
20
+ });
46
21
  }
47
22
 
48
23
  export function ErrorBoundary() {
49
24
  const error = useRouteError();
50
25
 
51
26
  if (isRouteErrorResponse(error)) {
52
- console.error(error.status, error.statusText, error.data);
53
- return <div>Route Error</div>;
54
- } else {
55
- console.error((error as Error).message);
56
- return <div>Thrown Error</div>;
27
+ return (
28
+ <div>
29
+ <h1>Oops</h1>
30
+ <p>Status: {error.status}</p>
31
+ <p>{error.data.message}</p>
32
+ </div>
33
+ );
57
34
  }
35
+
36
+ let errorMessage = 'Unknown error';
37
+ if (error instanceof Error) {
38
+ errorMessage = error.message;
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <h1>Uh oh ...</h1>
44
+ <p>Something went wrong.</p>
45
+ <pre>{errorMessage}</pre>
46
+ </div>
47
+ );
58
48
  }
59
49
 
60
50
  function robotsTxtData({url, shopId}: {shopId?: string; url?: string}) {
@@ -69,13 +59,9 @@ User-agent: adsbot-google
69
59
  Disallow: /checkouts/
70
60
  Disallow: /checkout
71
61
  Disallow: /carts
72
- Disallow: /orders${
73
- shopId
74
- ? `
75
- Disallow: /${shopId}/checkouts
76
- Disallow: /${shopId}/orders`
77
- : ''
78
- }
62
+ Disallow: /orders
63
+ ${shopId ? `Disallow: /${shopId}/checkouts` : ''}
64
+ ${shopId ? `Disallow: /${shopId}/orders` : ''}
79
65
  Disallow: /*?*oseid=*
80
66
  Disallow: /*preview_theme_id*
81
67
  Disallow: /*preview_script_id*
@@ -100,9 +86,8 @@ Crawl-delay: 1
100
86
  }
101
87
 
102
88
  /**
103
- *
104
- * This function generates disallow rules that generally follow what Shopify's Online Store has as defaults for their robots.txt
105
- *
89
+ * This function generates disallow rules that generally follow what Shopify's
90
+ * Online Store has as defaults for their robots.txt
106
91
  */
107
92
  function generalDisallowRules({
108
93
  shopId,
@@ -115,13 +100,9 @@ function generalDisallowRules({
115
100
  Disallow: /cart
116
101
  Disallow: /orders
117
102
  Disallow: /checkouts/
118
- Disallow: /checkout${
119
- shopId
120
- ? `
121
- Disallow: /${shopId}/checkouts
122
- Disallow: /${shopId}/orders`
123
- : ''
124
- }
103
+ Disallow: /checkout
104
+ ${shopId ? `Disallow: /${shopId}/checkouts` : ''}
105
+ ${shopId ? `Disallow: /${shopId}/orders` : ''}
125
106
  Disallow: /carts
126
107
  Disallow: /account
127
108
  Disallow: /collections/*sort_by*
@@ -150,18 +131,15 @@ Disallow: /search
150
131
  Allow: /search/
151
132
  Disallow: /search/?*
152
133
  Disallow: /apple-app-site-association
153
- Disallow: /.well-known/shopify/monorail${
154
- sitemapUrl
155
- ? `
156
- Sitemap: ${sitemapUrl}`
157
- : ''
158
- }`;
134
+ Disallow: /.well-known/shopify/monorail
135
+ ${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''}`;
159
136
  }
160
137
 
161
- const SHOP_QUERY = `#graphql
162
- query robots_shop_query {
138
+ const ROBOTS_QUERY = `#graphql
139
+ query StoreRobots($country: CountryCode, $language: LanguageCode)
140
+ @inContext(country: $country, language: $language) {
163
141
  shop {
164
142
  id
165
143
  }
166
144
  }
167
- `;
145
+ ` as const;
@@ -0,0 +1,174 @@
1
+ import {flattenConnection} from '@shopify/hydrogen';
2
+ import type {LoaderArgs} from '@shopify/remix-oxygen';
3
+ import type {SitemapQuery} from 'storefrontapi.generated';
4
+
5
+ /**
6
+ * the google limit is 50K, however, the storefront API
7
+ * allows querying only 250 resources per pagination page
8
+ */
9
+ const MAX_URLS = 250;
10
+
11
+ type Entry = {
12
+ url: string;
13
+ lastMod?: string;
14
+ changeFreq?: string;
15
+ image?: {
16
+ url: string;
17
+ title?: string;
18
+ caption?: string;
19
+ };
20
+ };
21
+
22
+ export async function loader({request, context: {storefront}}: LoaderArgs) {
23
+ const data = await storefront.query(SITEMAP_QUERY, {
24
+ variables: {
25
+ urlLimits: MAX_URLS,
26
+ language: storefront.i18n.language,
27
+ },
28
+ });
29
+
30
+ if (!data) {
31
+ throw new Response('No data found', {status: 404});
32
+ }
33
+
34
+ const sitemap = generateSitemap({data, baseUrl: new URL(request.url).origin});
35
+
36
+ return new Response(sitemap, {
37
+ headers: {
38
+ 'Content-Type': 'application/xml',
39
+
40
+ 'Cache-Control': `max-age=${60 * 60 * 24}`,
41
+ },
42
+ });
43
+ }
44
+
45
+ function xmlEncode(string: string) {
46
+ return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
47
+ }
48
+
49
+ function generateSitemap({
50
+ data,
51
+ baseUrl,
52
+ }: {
53
+ data: SitemapQuery;
54
+ baseUrl: string;
55
+ }) {
56
+ const products = flattenConnection(data.products)
57
+ .filter((product) => product.onlineStoreUrl)
58
+ .map((product) => {
59
+ const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;
60
+
61
+ const productEntry: Entry = {
62
+ url,
63
+ lastMod: product.updatedAt,
64
+ changeFreq: 'daily',
65
+ };
66
+
67
+ if (product.featuredImage?.url) {
68
+ productEntry.image = {
69
+ url: xmlEncode(product.featuredImage.url),
70
+ };
71
+
72
+ if (product.title) {
73
+ productEntry.image.title = xmlEncode(product.title);
74
+ }
75
+
76
+ if (product.featuredImage.altText) {
77
+ productEntry.image.caption = xmlEncode(product.featuredImage.altText);
78
+ }
79
+ }
80
+
81
+ return productEntry;
82
+ });
83
+
84
+ const collections = flattenConnection(data.collections)
85
+ .filter((collection) => collection.onlineStoreUrl)
86
+ .map((collection) => {
87
+ const url = `${baseUrl}/collections/${collection.handle}`;
88
+
89
+ return {
90
+ url,
91
+ lastMod: collection.updatedAt,
92
+ changeFreq: 'daily',
93
+ };
94
+ });
95
+
96
+ const pages = flattenConnection(data.pages)
97
+ .filter((page) => page.onlineStoreUrl)
98
+ .map((page) => {
99
+ const url = `${baseUrl}/pages/${page.handle}`;
100
+
101
+ return {
102
+ url,
103
+ lastMod: page.updatedAt,
104
+ changeFreq: 'weekly',
105
+ };
106
+ });
107
+
108
+ const urls = [...products, ...collections, ...pages];
109
+
110
+ return `
111
+ <urlset
112
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
113
+ xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
114
+ >
115
+ ${urls.map(renderUrlTag).join('')}
116
+ </urlset>`;
117
+ }
118
+
119
+ function renderUrlTag({url, lastMod, changeFreq, image}: Entry) {
120
+ const imageTag = image
121
+ ? `<image:image>
122
+ <image:loc>${image.url}</image:loc>
123
+ <image:title>${image.title ?? ''}</image:title>
124
+ <image:caption>${image.caption ?? ''}</image:caption>
125
+ </image:image>`.trim()
126
+ : '';
127
+
128
+ return `
129
+ <url>
130
+ <loc>${url}</loc>
131
+ <lastmod>${lastMod}</lastmod>
132
+ <changefreq>${changeFreq}</changefreq>
133
+ ${imageTag}
134
+ </url>
135
+ `.trim();
136
+ }
137
+
138
+ const SITEMAP_QUERY = `#graphql
139
+ query Sitemap($urlLimits: Int, $language: LanguageCode)
140
+ @inContext(language: $language) {
141
+ products(
142
+ first: $urlLimits
143
+ query: "published_status:'online_store:visible'"
144
+ ) {
145
+ nodes {
146
+ updatedAt
147
+ handle
148
+ onlineStoreUrl
149
+ title
150
+ featuredImage {
151
+ url
152
+ altText
153
+ }
154
+ }
155
+ }
156
+ collections(
157
+ first: $urlLimits
158
+ query: "published_status:'online_store:visible'"
159
+ ) {
160
+ nodes {
161
+ updatedAt
162
+ handle
163
+ onlineStoreUrl
164
+ }
165
+ }
166
+ pages(first: $urlLimits, query: "published_status:'published'") {
167
+ nodes {
168
+ updatedAt
169
+ handle
170
+ onlineStoreUrl
171
+ }
172
+ }
173
+ }
174
+ ` as const;