@shopify/cli-hydrogen 8.0.4 → 8.1.1

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 (162) hide show
  1. package/dist/{lib/setups/i18n/templates → assets/hydrogen/i18n}/domains.ts +1 -1
  2. package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +3 -0
  3. package/dist/{lib/setups/i18n/templates → assets/hydrogen/i18n}/subdomains.ts +1 -1
  4. package/dist/{lib/setups/i18n/templates → assets/hydrogen/i18n}/subfolders.ts +1 -1
  5. package/dist/{generator-templates → assets/hydrogen}/starter/.eslintrc.cjs +1 -0
  6. package/dist/{generator-templates → assets/hydrogen}/starter/CHANGELOG.md +31 -0
  7. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +76 -0
  8. package/dist/{generator-templates → assets/hydrogen}/starter/app/components/Cart.tsx +38 -14
  9. package/dist/{generator-templates → assets/hydrogen}/starter/app/components/Footer.tsx +30 -13
  10. package/dist/{generator-templates → assets/hydrogen}/starter/app/components/Header.tsx +52 -12
  11. package/dist/{generator-templates/starter/app/components/Layout.tsx → assets/hydrogen/starter/app/components/PageLayout.tsx} +38 -28
  12. package/dist/{generator-templates → assets/hydrogen}/starter/app/components/Search.tsx +5 -3
  13. package/dist/{generator-templates → assets/hydrogen}/starter/app/entry.server.tsx +8 -2
  14. package/dist/{generator-templates → assets/hydrogen}/starter/app/lib/fragments.ts +70 -0
  15. package/dist/assets/hydrogen/starter/app/root.tsx +204 -0
  16. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/_index.tsx +62 -25
  17. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.addresses.tsx +0 -1
  18. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.orders.$id.tsx +2 -2
  19. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.profile.tsx +0 -1
  20. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/api.predictive-search.tsx +6 -1
  21. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +34 -8
  22. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/blogs.$blogHandle._index.tsx +36 -10
  23. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/blogs._index.tsx +35 -12
  24. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/cart.tsx +7 -7
  25. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/collections.$handle.tsx +49 -7
  26. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/collections._index.tsx +32 -6
  27. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/collections.all.tsx +31 -6
  28. package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +84 -0
  29. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/products.$handle.tsx +82 -17
  30. package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/search.tsx +4 -1
  31. package/dist/{generator-templates → assets/hydrogen}/starter/app/styles/app.css +22 -4
  32. package/dist/{generator-templates → assets/hydrogen}/starter/env.d.ts +1 -0
  33. package/dist/{generator-templates → assets/hydrogen}/starter/package.json +10 -10
  34. package/dist/{generator-templates → assets/hydrogen}/starter/storefrontapi.generated.d.ts +3 -1
  35. package/dist/{generator-templates → assets/hydrogen}/starter/vite.config.ts +15 -0
  36. package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/routes/index.jsx +0 -10
  37. package/dist/{generator-templates/assets → assets/hydrogen}/vite/vite.config.js +15 -0
  38. package/dist/commands/hydrogen/build.js +119 -36
  39. package/dist/commands/hydrogen/debug/cpu.js +74 -64
  40. package/dist/commands/hydrogen/deploy.js +4 -4
  41. package/dist/commands/hydrogen/dev.js +133 -51
  42. package/dist/{init.d.ts → commands/hydrogen/init.d.ts} +5 -5
  43. package/dist/commands/hydrogen/init.js +17 -8
  44. package/dist/commands/hydrogen/preview.js +101 -13
  45. package/dist/commands/hydrogen/setup/vite.js +2 -2
  46. package/dist/commands/hydrogen/setup.js +1 -1
  47. package/dist/commands/hydrogen/upgrade.js +53 -50
  48. package/dist/index.d.ts +403 -0
  49. package/dist/index.js +61 -0
  50. package/dist/lib/build.js +48 -37
  51. package/dist/lib/bundle/analyzer.js +2 -4
  52. package/dist/lib/check-version.js +38 -18
  53. package/dist/lib/classic-compiler/build.js +11 -4
  54. package/dist/lib/classic-compiler/debug-cpu.js +52 -0
  55. package/dist/lib/classic-compiler/dev.js +12 -5
  56. package/dist/lib/codegen.js +34 -9
  57. package/dist/lib/cpu-profiler.js +29 -12
  58. package/dist/lib/defer.js +13 -7
  59. package/dist/lib/deps-optimizer.js +146 -0
  60. package/dist/lib/flags.js +2 -7
  61. package/dist/lib/format-code.js +1 -2
  62. package/dist/lib/import-utils.js +22 -0
  63. package/dist/lib/live-reload.js +15 -5
  64. package/dist/lib/log.js +24 -2
  65. package/dist/lib/mini-oxygen/index.js +6 -2
  66. package/dist/lib/mini-oxygen/node.js +18 -4
  67. package/dist/lib/mini-oxygen/workerd.js +18 -5
  68. package/dist/lib/onboarding/local.js +1 -1
  69. package/dist/lib/onboarding/setup-template.mocks.js +7 -11
  70. package/dist/lib/remix-config.js +11 -9
  71. package/dist/lib/remix-version-check.js +8 -13
  72. package/dist/lib/resource-cleanup.js +13 -0
  73. package/dist/lib/setups/css/assets.js +3 -3
  74. package/dist/lib/setups/css/css-modules.js +2 -2
  75. package/dist/lib/setups/css/postcss.js +2 -2
  76. package/dist/lib/setups/css/tailwind.js +2 -2
  77. package/dist/lib/setups/css/vanilla-extract.js +2 -2
  78. package/dist/lib/setups/i18n/index.js +3 -5
  79. package/dist/lib/setups/routes/generate.js +17 -16
  80. package/dist/lib/template-diff.js +120 -42
  81. package/dist/lib/template-downloader.js +6 -11
  82. package/dist/lib/transpile/morph/typedefs.js +17 -0
  83. package/dist/lib/virtual-routes.js +2 -2
  84. package/dist/lib/vite-config.js +2 -1
  85. package/oclif.manifest.json +89 -1
  86. package/package.json +13 -9
  87. package/dist/generator-templates/starter/app/components/Aside.tsx +0 -47
  88. package/dist/generator-templates/starter/app/lib/root-data.ts +0 -11
  89. package/dist/generator-templates/starter/app/root.tsx +0 -227
  90. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +0 -56
  91. package/dist/lib/setups/i18n/mock-i18n-types.js +0 -1
  92. package/dist/lib/setups/i18n/templates/domains.js +0 -14
  93. package/dist/lib/setups/i18n/templates/subdomains.js +0 -14
  94. package/dist/lib/setups/i18n/templates/subfolders.js +0 -13
  95. package/dist/lib/setups/routes/templates/locale-check.js +0 -9
  96. package/dist/virtual-routes/components/IconDiscord.jsx +0 -21
  97. /package/dist/{lib/bundle/bundle-analyzer.html → assets/hydrogen/bundle/analyzer.html} +0 -0
  98. /package/dist/{generator-templates/assets → assets/hydrogen}/css-modules/package.json +0 -0
  99. /package/dist/{generator-templates/assets → assets/hydrogen}/postcss/package.json +0 -0
  100. /package/dist/{generator-templates/assets → assets/hydrogen}/postcss/postcss.config.js +0 -0
  101. /package/dist/{lib/setups/routes/templates → assets/hydrogen/routes}/locale-check.ts +0 -0
  102. /package/dist/{generator-templates → assets/hydrogen}/starter/.eslintignore +0 -0
  103. /package/dist/{generator-templates → assets/hydrogen}/starter/.graphqlrc.yml +0 -0
  104. /package/dist/{generator-templates → assets/hydrogen}/starter/README.md +0 -0
  105. /package/dist/{generator-templates → assets/hydrogen}/starter/app/assets/favicon.svg +0 -0
  106. /package/dist/{generator-templates → assets/hydrogen}/starter/app/entry.client.tsx +0 -0
  107. /package/dist/{generator-templates → assets/hydrogen}/starter/app/graphql/customer-account/CustomerAddressMutations.ts +0 -0
  108. /package/dist/{generator-templates → assets/hydrogen}/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +0 -0
  109. /package/dist/{generator-templates → assets/hydrogen}/starter/app/graphql/customer-account/CustomerOrderQuery.ts +0 -0
  110. /package/dist/{generator-templates → assets/hydrogen}/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +0 -0
  111. /package/dist/{generator-templates → assets/hydrogen}/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +0 -0
  112. /package/dist/{generator-templates → assets/hydrogen}/starter/app/lib/search.ts +0 -0
  113. /package/dist/{generator-templates → assets/hydrogen}/starter/app/lib/session.ts +0 -0
  114. /package/dist/{generator-templates → assets/hydrogen}/starter/app/lib/variants.ts +0 -0
  115. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/$.tsx +0 -0
  116. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/[robots.txt].tsx +0 -0
  117. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/[sitemap.xml].tsx +0 -0
  118. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.$.tsx +0 -0
  119. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account._index.tsx +0 -0
  120. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.orders._index.tsx +0 -0
  121. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account.tsx +0 -0
  122. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account_.authorize.tsx +0 -0
  123. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account_.login.tsx +0 -0
  124. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/account_.logout.tsx +0 -0
  125. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/cart.$lines.tsx +0 -0
  126. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/discount.$code.tsx +0 -0
  127. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/policies.$handle.tsx +0 -0
  128. /package/dist/{generator-templates → assets/hydrogen}/starter/app/routes/policies._index.tsx +0 -0
  129. /package/dist/{generator-templates → assets/hydrogen}/starter/app/styles/reset.css +0 -0
  130. /package/dist/{generator-templates → assets/hydrogen}/starter/customer-accountapi.generated.d.ts +0 -0
  131. /package/dist/{generator-templates → assets/hydrogen}/starter/public/.gitkeep +0 -0
  132. /package/dist/{generator-templates → assets/hydrogen}/starter/server.ts +0 -0
  133. /package/dist/{generator-templates → assets/hydrogen}/starter/tsconfig.json +0 -0
  134. /package/dist/{generator-templates/assets → assets/hydrogen}/tailwind/package.json +0 -0
  135. /package/dist/{generator-templates/assets → assets/hydrogen}/tailwind/postcss.config.js +0 -0
  136. /package/dist/{generator-templates/assets → assets/hydrogen}/tailwind/tailwind.config.js +0 -0
  137. /package/dist/{generator-templates/assets → assets/hydrogen}/tailwind/tailwind.css +0 -0
  138. /package/dist/{generator-templates/assets → assets/hydrogen}/vanilla-extract/package.json +0 -0
  139. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/debug-network.css +0 -0
  140. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/favicon-dark.svg +0 -0
  141. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/favicon.svg +0 -0
  142. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/inter-variable-font.woff2 +0 -0
  143. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/jetbrainsmono-variable-font.woff2 +0 -0
  144. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/assets/styles.css +0 -0
  145. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/FlameChartWrapper.jsx +0 -0
  146. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/HydrogenLogoBaseBW.jsx +0 -0
  147. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/HydrogenLogoBaseColor.jsx +0 -0
  148. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconBanner.jsx +0 -0
  149. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconClose.jsx +0 -0
  150. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconDiscard.jsx +0 -0
  151. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconError.jsx +0 -0
  152. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconGithub.jsx +0 -0
  153. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/IconTwitter.jsx +0 -0
  154. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/Layout.jsx +0 -0
  155. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/RequestDetails.jsx +0 -0
  156. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/RequestTable.jsx +0 -0
  157. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/components/RequestWaterfall.jsx +0 -0
  158. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/lib/useDebugNetworkServer.jsx +0 -0
  159. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/routes/graphiql.jsx +0 -0
  160. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/routes/subrequest-profiler.jsx +0 -0
  161. /package/dist/{virtual-routes → assets/hydrogen/virtual-routes}/virtual-root.jsx +0 -0
  162. /package/dist/{generator-templates/assets → assets/hydrogen}/vite/package.json +0 -0
@@ -102,3 +102,73 @@ export const CART_QUERY_FRAGMENT = `#graphql
102
102
  }
103
103
  }
104
104
  ` as const;
105
+
106
+ const MENU_FRAGMENT = `#graphql
107
+ fragment MenuItem on MenuItem {
108
+ id
109
+ resourceId
110
+ tags
111
+ title
112
+ type
113
+ url
114
+ }
115
+ fragment ChildMenuItem on MenuItem {
116
+ ...MenuItem
117
+ }
118
+ fragment ParentMenuItem on MenuItem {
119
+ ...MenuItem
120
+ items {
121
+ ...ChildMenuItem
122
+ }
123
+ }
124
+ fragment Menu on Menu {
125
+ id
126
+ items {
127
+ ...ParentMenuItem
128
+ }
129
+ }
130
+ ` as const;
131
+
132
+ export const HEADER_QUERY = `#graphql
133
+ fragment Shop on Shop {
134
+ id
135
+ name
136
+ description
137
+ primaryDomain {
138
+ url
139
+ }
140
+ brand {
141
+ logo {
142
+ image {
143
+ url
144
+ }
145
+ }
146
+ }
147
+ }
148
+ query Header(
149
+ $country: CountryCode
150
+ $headerMenuHandle: String!
151
+ $language: LanguageCode
152
+ ) @inContext(language: $language, country: $country) {
153
+ shop {
154
+ ...Shop
155
+ }
156
+ menu(handle: $headerMenuHandle) {
157
+ ...Menu
158
+ }
159
+ }
160
+ ${MENU_FRAGMENT}
161
+ ` as const;
162
+
163
+ export const FOOTER_QUERY = `#graphql
164
+ query Footer(
165
+ $country: CountryCode
166
+ $footerMenuHandle: String!
167
+ $language: LanguageCode
168
+ ) @inContext(language: $language, country: $country) {
169
+ menu(handle: $footerMenuHandle) {
170
+ ...Menu
171
+ }
172
+ }
173
+ ${MENU_FRAGMENT}
174
+ ` as const;
@@ -0,0 +1,204 @@
1
+ import {useNonce, getShopAnalytics, Analytics} from '@shopify/hydrogen';
2
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
3
+ import {
4
+ Links,
5
+ Meta,
6
+ Outlet,
7
+ Scripts,
8
+ useRouteError,
9
+ useRouteLoaderData,
10
+ ScrollRestoration,
11
+ isRouteErrorResponse,
12
+ type ShouldRevalidateFunction,
13
+ } from '@remix-run/react';
14
+ import favicon from '~/assets/favicon.svg';
15
+ import resetStyles from '~/styles/reset.css?url';
16
+ import appStyles from '~/styles/app.css?url';
17
+ import {PageLayout} from '~/components/PageLayout';
18
+ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
19
+
20
+ export type RootLoader = typeof loader;
21
+
22
+ /**
23
+ * This is important to avoid re-fetching root queries on sub-navigations
24
+ */
25
+ export const shouldRevalidate: ShouldRevalidateFunction = ({
26
+ formMethod,
27
+ currentUrl,
28
+ nextUrl,
29
+ }) => {
30
+ // revalidate when a mutation is performed e.g add to cart, login...
31
+ if (formMethod && formMethod !== 'GET') {
32
+ return true;
33
+ }
34
+
35
+ // revalidate when manually revalidating via useRevalidator
36
+ if (currentUrl.toString() === nextUrl.toString()) {
37
+ return true;
38
+ }
39
+
40
+ return false;
41
+ };
42
+
43
+ export function links() {
44
+ return [
45
+ {rel: 'stylesheet', href: resetStyles},
46
+ {rel: 'stylesheet', href: appStyles},
47
+ {
48
+ rel: 'preconnect',
49
+ href: 'https://cdn.shopify.com',
50
+ },
51
+ {
52
+ rel: 'preconnect',
53
+ href: 'https://shop.app',
54
+ },
55
+ {rel: 'icon', type: 'image/svg+xml', href: favicon},
56
+ ];
57
+ }
58
+
59
+ export async function loader(args: LoaderFunctionArgs) {
60
+ // Start fetching non-critical data without blocking time to first byte
61
+ const deferredData = loadDeferredData(args);
62
+
63
+ // Await the critical data required to render initial state of the page
64
+ const criticalData = await loadCriticalData(args);
65
+
66
+ const {storefront, env} = args.context;
67
+
68
+ return defer(
69
+ {
70
+ ...deferredData,
71
+ ...criticalData,
72
+ publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
73
+ shop: getShopAnalytics({
74
+ storefront,
75
+ publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
76
+ }),
77
+ consent: {
78
+ checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
79
+ storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
80
+ },
81
+ },
82
+ {
83
+ headers: {
84
+ 'Set-Cookie': await args.context.session.commit(),
85
+ },
86
+ },
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Load data necessary for rendering content above the fold. This is the critical data
92
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
93
+ */
94
+ async function loadCriticalData({context}: LoaderFunctionArgs) {
95
+ const {storefront} = context;
96
+
97
+ const [header] = await Promise.all([
98
+ storefront.query(HEADER_QUERY, {
99
+ cache: storefront.CacheLong(),
100
+ variables: {
101
+ headerMenuHandle: 'main-menu', // Adjust to your header menu handle
102
+ },
103
+ }),
104
+ // Add other queries here, so that they are loaded in parallel
105
+ ]);
106
+
107
+ return {
108
+ header,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Load data for rendering content below the fold. This data is deferred and will be
114
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
115
+ * Make sure to not throw any errors here, as it will cause the page to 500.
116
+ */
117
+ function loadDeferredData({context}: LoaderFunctionArgs) {
118
+ const {storefront, customerAccount, cart} = context;
119
+
120
+ // defer the footer query (below the fold)
121
+ const footer = storefront
122
+ .query(FOOTER_QUERY, {
123
+ cache: storefront.CacheLong(),
124
+ variables: {
125
+ footerMenuHandle: 'footer', // Adjust to your footer menu handle
126
+ },
127
+ })
128
+ .catch((error) => {
129
+ // Log query errors, but don't throw them so the page can still render
130
+ console.error(error);
131
+ return null;
132
+ });
133
+ return {
134
+ cart: cart.get(),
135
+ isLoggedIn: customerAccount.isLoggedIn(),
136
+ footer,
137
+ };
138
+ }
139
+
140
+ function Layout({children}: {children?: React.ReactNode}) {
141
+ const nonce = useNonce();
142
+ const data = useRouteLoaderData<RootLoader>('root');
143
+
144
+ return (
145
+ <html lang="en">
146
+ <head>
147
+ <meta charSet="utf-8" />
148
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
149
+ <Meta />
150
+ <Links />
151
+ </head>
152
+ <body>
153
+ {data ? (
154
+ <Analytics.Provider
155
+ cart={data.cart}
156
+ shop={data.shop}
157
+ consent={data.consent}
158
+ >
159
+ <PageLayout {...data}>{children}</PageLayout>
160
+ </Analytics.Provider>
161
+ ) : (
162
+ children
163
+ )}
164
+ <ScrollRestoration nonce={nonce} />
165
+ <Scripts nonce={nonce} />
166
+ </body>
167
+ </html>
168
+ );
169
+ }
170
+
171
+ export default function App() {
172
+ return (
173
+ <Layout>
174
+ <Outlet />
175
+ </Layout>
176
+ );
177
+ }
178
+
179
+ export function ErrorBoundary() {
180
+ const error = useRouteError();
181
+ let errorMessage = 'Unknown error';
182
+ let errorStatus = 500;
183
+
184
+ if (isRouteErrorResponse(error)) {
185
+ errorMessage = error?.data?.message ?? error.data;
186
+ errorStatus = error.status;
187
+ } else if (error instanceof Error) {
188
+ errorMessage = error.message;
189
+ }
190
+
191
+ return (
192
+ <Layout>
193
+ <div className="route-error">
194
+ <h1>Oops</h1>
195
+ <h2>{errorStatus}</h2>
196
+ {errorMessage && (
197
+ <fieldset>
198
+ <pre>{errorMessage}</pre>
199
+ </fieldset>
200
+ )}
201
+ </div>
202
+ </Layout>
203
+ );
204
+ }
@@ -11,13 +11,48 @@ export const meta: MetaFunction = () => {
11
11
  return [{title: 'Hydrogen | Home'}];
12
12
  };
13
13
 
14
- export async function loader({context}: LoaderFunctionArgs) {
15
- const {storefront} = context;
16
- const {collections} = await storefront.query(FEATURED_COLLECTION_QUERY);
17
- const featuredCollection = collections.nodes[0];
18
- const recommendedProducts = storefront.query(RECOMMENDED_PRODUCTS_QUERY);
14
+ export async function loader(args: LoaderFunctionArgs) {
15
+ // Start fetching non-critical data without blocking time to first byte
16
+ const deferredData = loadDeferredData(args);
19
17
 
20
- return defer({featuredCollection, recommendedProducts});
18
+ // Await the critical data required to render initial state of the page
19
+ const criticalData = await loadCriticalData(args);
20
+
21
+ return defer({...deferredData, ...criticalData});
22
+ }
23
+
24
+ /**
25
+ * Load data necessary for rendering content above the fold. This is the critical data
26
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
27
+ */
28
+ async function loadCriticalData({context}: LoaderFunctionArgs) {
29
+ const [{collections}] = await Promise.all([
30
+ context.storefront.query(FEATURED_COLLECTION_QUERY),
31
+ // Add other queries here, so that they are loaded in parallel
32
+ ]);
33
+
34
+ return {
35
+ featuredCollection: collections.nodes[0],
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Load data for rendering content below the fold. This data is deferred and will be
41
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
42
+ * Make sure to not throw any errors here, as it will cause the page to 500.
43
+ */
44
+ function loadDeferredData({context}: LoaderFunctionArgs) {
45
+ const recommendedProducts = context.storefront
46
+ .query(RECOMMENDED_PRODUCTS_QUERY)
47
+ .catch((error) => {
48
+ // Log query errors, but don't throw them so the page can still render
49
+ console.error(error);
50
+ return null;
51
+ });
52
+
53
+ return {
54
+ recommendedProducts,
55
+ };
21
56
  }
22
57
 
23
58
  export default function Homepage() {
@@ -55,32 +90,34 @@ function FeaturedCollection({
55
90
  function RecommendedProducts({
56
91
  products,
57
92
  }: {
58
- products: Promise<RecommendedProductsQuery>;
93
+ products: Promise<RecommendedProductsQuery | null>;
59
94
  }) {
60
95
  return (
61
96
  <div className="recommended-products">
62
97
  <h2>Recommended Products</h2>
63
98
  <Suspense fallback={<div>Loading...</div>}>
64
99
  <Await resolve={products}>
65
- {({products}) => (
100
+ {(response) => (
66
101
  <div className="recommended-products-grid">
67
- {products.nodes.map((product) => (
68
- <Link
69
- key={product.id}
70
- className="recommended-product"
71
- to={`/products/${product.handle}`}
72
- >
73
- <Image
74
- data={product.images.nodes[0]}
75
- aspectRatio="1/1"
76
- sizes="(min-width: 45em) 20vw, 50vw"
77
- />
78
- <h4>{product.title}</h4>
79
- <small>
80
- <Money data={product.priceRange.minVariantPrice} />
81
- </small>
82
- </Link>
83
- ))}
102
+ {response
103
+ ? response.products.nodes.map((product) => (
104
+ <Link
105
+ key={product.id}
106
+ className="recommended-product"
107
+ to={`/products/${product.handle}`}
108
+ >
109
+ <Image
110
+ data={product.images.nodes[0]}
111
+ aspectRatio="1/1"
112
+ sizes="(min-width: 45em) 20vw, 50vw"
113
+ />
114
+ <h4>{product.title}</h4>
115
+ <small>
116
+ <Money data={product.priceRange.minVariantPrice} />
117
+ </small>
118
+ </Link>
119
+ ))
120
+ : null}
84
121
  </div>
85
122
  )}
86
123
  </Await>
@@ -5,7 +5,6 @@ import type {
5
5
  } from 'customer-accountapi.generated';
6
6
  import {
7
7
  json,
8
- redirect,
9
8
  type ActionFunctionArgs,
10
9
  type LoaderFunctionArgs,
11
10
  } from '@shopify/remix-oxygen';
@@ -1,5 +1,5 @@
1
1
  import {json, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
- import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Money, Image, flattenConnection} from '@shopify/hydrogen';
4
4
  import type {OrderLineItemFullFragment} from 'customer-accountapi.generated';
5
5
  import {CUSTOMER_ORDER_QUERY} from '~/graphql/customer-account/CustomerOrderQuery';
@@ -8,7 +8,7 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
8
8
  return [{title: `Order ${data?.order?.name}`}];
9
9
  };
10
10
 
11
- export async function loader({params, context, request}: LoaderFunctionArgs) {
11
+ export async function loader({params, context}: LoaderFunctionArgs) {
12
12
  if (!params.id) {
13
13
  return redirect('/account/orders');
14
14
  }
@@ -3,7 +3,6 @@ import type {CustomerUpdateInput} from '@shopify/hydrogen/customer-account-api-t
3
3
  import {CUSTOMER_UPDATE_MUTATION} from '~/graphql/customer-account/CustomerUpdateMutation';
4
4
  import {
5
5
  json,
6
- redirect,
7
6
  type ActionFunctionArgs,
8
7
  type LoaderFunctionArgs,
9
8
  } from '@shopify/remix-oxygen';
@@ -30,6 +30,8 @@ const DEFAULT_SEARCH_TYPES: PredictiveSearchTypes[] = [
30
30
  'QUERY',
31
31
  ];
32
32
 
33
+ export type PredictiveSearchAPILoader = typeof loader;
34
+
33
35
  /**
34
36
  * Fetches the search results from the predictive search API
35
37
  * requested by the SearchForm component
@@ -207,7 +209,7 @@ export function normalizePredictiveSearchResults(
207
209
  id: article.id,
208
210
  image: article.image,
209
211
  title: article.title,
210
- url: `${localePrefix}/blog/${article.handle}${trackingParams}`,
212
+ url: `${localePrefix}/blogs/${article.blog.handle}/${article.handle}/${trackingParams}`,
211
213
  };
212
214
  },
213
215
  ),
@@ -223,6 +225,9 @@ const PREDICTIVE_SEARCH_QUERY = `#graphql
223
225
  id
224
226
  title
225
227
  handle
228
+ blog {
229
+ handle
230
+ }
226
231
  image {
227
232
  url
228
233
  altText
@@ -1,4 +1,4 @@
1
- import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Image} from '@shopify/hydrogen';
4
4
 
@@ -6,16 +6,33 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
6
6
  return [{title: `Hydrogen | ${data?.article.title ?? ''} article`}];
7
7
  };
8
8
 
9
- export async function loader({params, context}: LoaderFunctionArgs) {
9
+ export async function loader(args: LoaderFunctionArgs) {
10
+ // Start fetching non-critical data without blocking time to first byte
11
+ const deferredData = loadDeferredData(args);
12
+
13
+ // Await the critical data required to render initial state of the page
14
+ const criticalData = await loadCriticalData(args);
15
+
16
+ return defer({...deferredData, ...criticalData});
17
+ }
18
+
19
+ /**
20
+ * Load data necessary for rendering content above the fold. This is the critical data
21
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
22
+ */
23
+ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
10
24
  const {blogHandle, articleHandle} = params;
11
25
 
12
26
  if (!articleHandle || !blogHandle) {
13
27
  throw new Response('Not found', {status: 404});
14
28
  }
15
29
 
16
- const {blog} = await context.storefront.query(ARTICLE_QUERY, {
17
- variables: {blogHandle, articleHandle},
18
- });
30
+ const [{blog}] = await Promise.all([
31
+ context.storefront.query(ARTICLE_QUERY, {
32
+ variables: {blogHandle, articleHandle},
33
+ }),
34
+ // Add other queries here, so that they are loaded in parallel
35
+ ]);
19
36
 
20
37
  if (!blog?.articleByHandle) {
21
38
  throw new Response(null, {status: 404});
@@ -23,7 +40,16 @@ export async function loader({params, context}: LoaderFunctionArgs) {
23
40
 
24
41
  const article = blog.articleByHandle;
25
42
 
26
- return json({article});
43
+ return {article};
44
+ }
45
+
46
+ /**
47
+ * Load data for rendering content below the fold. This data is deferred and will be
48
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
49
+ * Make sure to not throw any errors here, as it will cause the page to 500.
50
+ */
51
+ function loadDeferredData({context}: LoaderFunctionArgs) {
52
+ return {};
27
53
  }
28
54
 
29
55
  export default function Article() {
@@ -40,9 +66,9 @@ export default function Article() {
40
66
  <div className="article">
41
67
  <h1>
42
68
  {title}
43
- <span>
69
+ <div>
44
70
  {publishedDate} &middot; {author?.name}
45
- </span>
71
+ </div>
46
72
  </h1>
47
73
 
48
74
  {image && <Image data={image} sizes="90vw" loading="eager" />}
@@ -1,4 +1,4 @@
1
- import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Image, Pagination, getPaginationVariables} from '@shopify/hydrogen';
4
4
  import type {ArticleItemFragment} from 'storefrontapi.generated';
@@ -7,10 +7,24 @@ export const meta: MetaFunction<typeof loader> = ({data}) => {
7
7
  return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
8
8
  };
9
9
 
10
- export async function loader({
10
+ export async function loader(args: LoaderFunctionArgs) {
11
+ // Start fetching non-critical data without blocking time to first byte
12
+ const deferredData = loadDeferredData(args);
13
+
14
+ // Await the critical data required to render initial state of the page
15
+ const criticalData = await loadCriticalData(args);
16
+
17
+ return defer({...deferredData, ...criticalData});
18
+ }
19
+
20
+ /**
21
+ * Load data necessary for rendering content above the fold. This is the critical data
22
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
23
+ */
24
+ async function loadCriticalData({
25
+ context,
11
26
  request,
12
27
  params,
13
- context: {storefront},
14
28
  }: LoaderFunctionArgs) {
15
29
  const paginationVariables = getPaginationVariables(request, {
16
30
  pageBy: 4,
@@ -20,18 +34,30 @@ export async function loader({
20
34
  throw new Response(`blog not found`, {status: 404});
21
35
  }
22
36
 
23
- const {blog} = await storefront.query(BLOGS_QUERY, {
24
- variables: {
25
- blogHandle: params.blogHandle,
26
- ...paginationVariables,
27
- },
28
- });
37
+ const [{blog}] = await Promise.all([
38
+ context.storefront.query(BLOGS_QUERY, {
39
+ variables: {
40
+ blogHandle: params.blogHandle,
41
+ ...paginationVariables,
42
+ },
43
+ }),
44
+ // Add other queries here, so that they are loaded in parallel
45
+ ]);
29
46
 
30
47
  if (!blog?.articles) {
31
48
  throw new Response('Not found', {status: 404});
32
49
  }
33
50
 
34
- return json({blog});
51
+ return {blog};
52
+ }
53
+
54
+ /**
55
+ * Load data for rendering content below the fold. This data is deferred and will be
56
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
57
+ * Make sure to not throw any errors here, as it will cause the page to 500.
58
+ */
59
+ function loadDeferredData({context}: LoaderFunctionArgs) {
60
+ return {};
35
61
  }
36
62
 
37
63
  export default function Blog() {
@@ -1,4 +1,4 @@
1
- import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Pagination, getPaginationVariables} from '@shopify/hydrogen';
4
4
 
@@ -6,22 +6,45 @@ export const meta: MetaFunction = () => {
6
6
  return [{title: `Hydrogen | Blogs`}];
7
7
  };
8
8
 
9
- export const loader = async ({
10
- request,
11
- context: {storefront},
12
- }: LoaderFunctionArgs) => {
9
+ export async function loader(args: LoaderFunctionArgs) {
10
+ // Start fetching non-critical data without blocking time to first byte
11
+ const deferredData = loadDeferredData(args);
12
+
13
+ // Await the critical data required to render initial state of the page
14
+ const criticalData = await loadCriticalData(args);
15
+
16
+ return defer({...deferredData, ...criticalData});
17
+ }
18
+
19
+ /**
20
+ * Load data necessary for rendering content above the fold. This is the critical data
21
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
22
+ */
23
+ async function loadCriticalData({context, request}: LoaderFunctionArgs) {
13
24
  const paginationVariables = getPaginationVariables(request, {
14
25
  pageBy: 10,
15
26
  });
16
27
 
17
- const {blogs} = await storefront.query(BLOGS_QUERY, {
18
- variables: {
19
- ...paginationVariables,
20
- },
21
- });
28
+ const [{blogs}] = await Promise.all([
29
+ context.storefront.query(BLOGS_QUERY, {
30
+ variables: {
31
+ ...paginationVariables,
32
+ },
33
+ }),
34
+ // Add other queries here, so that they are loaded in parallel
35
+ ]);
22
36
 
23
- return json({blogs});
24
- };
37
+ return {blogs};
38
+ }
39
+
40
+ /**
41
+ * Load data for rendering content below the fold. This data is deferred and will be
42
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
43
+ * Make sure to not throw any errors here, as it will cause the page to 500.
44
+ */
45
+ function loadDeferredData({context}: LoaderFunctionArgs) {
46
+ return {};
47
+ }
25
48
 
26
49
  export default function Blogs() {
27
50
  const {blogs} = useLoaderData<typeof loader>();