@shopify/create-hydrogen 5.0.20 → 5.0.22
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.
- package/dist/assets/hydrogen/starter/CHANGELOG.md +142 -56
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +15 -0
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/components/ProductItem.tsx +44 -0
- package/dist/assets/hydrogen/starter/app/lib/redirect.ts +23 -0
- package/dist/assets/hydrogen/starter/app/root.tsx +45 -4
- package/dist/assets/hydrogen/starter/app/routes/_index.tsx +8 -23
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +22 -2
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +4 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +7 -41
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +11 -44
- package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +9 -1
- package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +5 -1
- package/dist/assets/hydrogen/starter/app/routes.ts +4 -2
- package/dist/assets/hydrogen/starter/app/styles/app.css +15 -3
- package/dist/assets/hydrogen/starter/package.json +5 -5
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +57 -36
- package/dist/assets/hydrogen/tailwind/package.json +4 -1
- package/dist/{chunk-BC2VIKDB.js → chunk-NIHY2BIB.js} +383 -387
- package/dist/create-app.js +269 -269
- package/dist/error-handler-O653XSNU.js +2 -0
- package/dist/is-wsl-LL6KGQIK.js +2 -0
- package/package.json +1 -1
- package/dist/assets/hydrogen/starter/app/layout.tsx +0 -46
- package/dist/error-handler-T5EOGV44.js +0 -2
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# skeleton
|
|
2
2
|
|
|
3
|
+
## 2025.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Moved the Cursor rules into more generic LLM prompt files. If you were using the Cursor rules, you will find the prompts in the `cookbook/llms` folder and they can be put into your `.cursor/rules` folder manually. LLM prompt files will be maintained moving forward, while previous Cursor rules will not be updated anymore. ([#2936](https://github.com/Shopify/hydrogen/pull/2936)) by [@ruggishop](https://github.com/ruggishop)
|
|
8
|
+
|
|
9
|
+
- Added bundles recipe ([#2915](https://github.com/Shopify/hydrogen/pull/2915)) by [@ruggishop](https://github.com/ruggishop)
|
|
10
|
+
|
|
11
|
+
- Update copy for subscriptions, combined listings, bundles recipes ([#2924](https://github.com/Shopify/hydrogen/pull/2924)) by [@ruggishop](https://github.com/ruggishop)
|
|
12
|
+
|
|
13
|
+
- Bump skeleton @shopify/cli and @shopify/mini-oxygen ([#2883](https://github.com/Shopify/hydrogen/pull/2883)) by [@juanpprieto](https://github.com/juanpprieto)
|
|
14
|
+
|
|
15
|
+
- Remove rules from the template. ([#2931](https://github.com/Shopify/hydrogen/pull/2931)) by [@ruggishop](https://github.com/ruggishop)
|
|
16
|
+
|
|
17
|
+
- Update SFAPI and CAAPI versions to 2025.04 ([#2886](https://github.com/Shopify/hydrogen/pull/2886)) by [@juanpprieto](https://github.com/juanpprieto)
|
|
18
|
+
|
|
19
|
+
- Updated recipes: subscriptions, bundles, combined-listings. New recipe: markets. ([#2930](https://github.com/Shopify/hydrogen/pull/2930)) by [@ruggishop](https://github.com/ruggishop)
|
|
20
|
+
|
|
21
|
+
- Updated the subscriptions recipe to better display the purchase options. ([#2912](https://github.com/Shopify/hydrogen/pull/2912)) by [@ruggishop](https://github.com/ruggishop)
|
|
22
|
+
|
|
23
|
+
- Bump recipes with copy adjustments ([#2935](https://github.com/Shopify/hydrogen/pull/2935)) by [@ruggishop](https://github.com/ruggishop)
|
|
24
|
+
|
|
25
|
+
- Added a Combined Listings recipe. ([#2876](https://github.com/Shopify/hydrogen/pull/2876)) by [@ruggishop](https://github.com/ruggishop)
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [[`af23e710`](https://github.com/Shopify/hydrogen/commit/af23e710dac83bb57498d9c2ef1d8bcf9df55d34), [`9d8a6644`](https://github.com/Shopify/hydrogen/commit/9d8a6644a5b67dca890c6687df390aee78fc85c3)]:
|
|
28
|
+
- @shopify/hydrogen@2025.4.0
|
|
29
|
+
|
|
30
|
+
## 2025.1.7
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- Fix an issue with our starter template where duplicate content can exist on URLs that use internationalized handles. For example, if you have a product handle in english of `the-havoc` and translate it to `das-chaos` in German, duplicate content exists at both: ([#2821](https://github.com/Shopify/hydrogen/pull/2821)) by [@blittle](https://github.com/blittle)
|
|
35
|
+
|
|
36
|
+
1. https://hydrogen.shop/de-de/products/das-chaos
|
|
37
|
+
2. https://hydrogen.shop/de-de/products/the-havoc
|
|
38
|
+
|
|
39
|
+
We've changed the starter template to make the second redirect to the first.
|
|
40
|
+
|
|
41
|
+
- Added the Cursor rule for the subscriptions recipe. ([#2874](https://github.com/Shopify/hydrogen/pull/2874)) by [@ruggishop](https://github.com/ruggishop)
|
|
42
|
+
|
|
43
|
+
- Fix faulty truthiness check for cart quantity ([#2855](https://github.com/Shopify/hydrogen/pull/2855)) by [@frontsideair](https://github.com/frontsideair)
|
|
44
|
+
|
|
45
|
+
- Refactor ProductItem into a separate component ([#2872](https://github.com/Shopify/hydrogen/pull/2872)) by [@juanpprieto](https://github.com/juanpprieto)
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [[`f80f3bc7`](https://github.com/Shopify/hydrogen/commit/f80f3bc7239b3ee6641cb468a17e15c77bb7815b), [`61ddf924`](https://github.com/Shopify/hydrogen/commit/61ddf92487524b3c04632ae2cfdaa2869a3ae02c), [`642bde4f`](https://github.com/Shopify/hydrogen/commit/642bde4f3df11511e125b013abd977618da25692)]:
|
|
48
|
+
- @shopify/hydrogen@2025.1.4
|
|
49
|
+
|
|
50
|
+
## 2025.1.6
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- Moved the `Layout` component back into `root.tsx` to avoid issues with styled errors. ([#2829](https://github.com/Shopify/hydrogen/pull/2829)) by [@ruggishop](https://github.com/ruggishop)
|
|
55
|
+
|
|
56
|
+
1. If you have a separate `app/layout.tsx` file, delete it and move its default exported component into your `root.tsx`. For example:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// /app/root.tsx
|
|
60
|
+
export function Layout({children}: {children?: React.ReactNode}) {
|
|
61
|
+
const nonce = useNonce();
|
|
62
|
+
const data = useRouteLoaderData<RootLoader>('root');
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<html lang="en">
|
|
66
|
+
...
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
3
71
|
## 2025.1.5
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
|
@@ -39,7 +107,30 @@
|
|
|
39
107
|
|
|
40
108
|
Please refer to the Remix documentation for more details on `v3_routeConfig` future flag: [https://remix.run/docs/en/main/start/future-flags#v3_routeconfig](https://remix.run/docs/en/main/start/future-flags#v3_routeconfig)
|
|
41
109
|
|
|
42
|
-
1.
|
|
110
|
+
1. Update your `vite.config.ts`.
|
|
111
|
+
|
|
112
|
+
```diff
|
|
113
|
+
export default defineConfig({
|
|
114
|
+
plugins: [
|
|
115
|
+
hydrogen(),
|
|
116
|
+
oxygen(),
|
|
117
|
+
remix({
|
|
118
|
+
- presets: [hydrogen.preset()],
|
|
119
|
+
+ presets: [hydrogen.v3preset()],
|
|
120
|
+
future: {
|
|
121
|
+
v3_fetcherPersist: true,
|
|
122
|
+
v3_relativeSplatPath: true,
|
|
123
|
+
v3_throwAbortReason: true,
|
|
124
|
+
v3_lazyRouteDiscovery: true,
|
|
125
|
+
v3_singleFetch: true,
|
|
126
|
+
+ v3_routeConfig: true,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
tsconfigPaths(),
|
|
130
|
+
],
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
1. Update your `package.json` and install the new packages. Make sure to match the Remix version along with other Remix npm packages and ensure the versions are 2.16.1 or above:
|
|
43
134
|
|
|
44
135
|
```diff
|
|
45
136
|
"devDependencies": {
|
|
@@ -48,46 +139,41 @@
|
|
|
48
139
|
+ "@remix-run/route-config": "^2.16.1",
|
|
49
140
|
```
|
|
50
141
|
|
|
51
|
-
1.
|
|
142
|
+
1. Move the `Layout` component export from `root.tsx` into its own file. Make sure to supply an `<Outlet>` so Remix knows where to inject your route content.
|
|
52
143
|
|
|
53
144
|
```ts
|
|
54
145
|
// /app/layout.tsx
|
|
146
|
+
import {Outlet} from '@remix-run/react';
|
|
147
|
+
|
|
55
148
|
export default function Layout() {
|
|
56
149
|
const nonce = useNonce();
|
|
57
150
|
const data = useRouteLoaderData<RootLoader>('root');
|
|
58
151
|
|
|
59
152
|
return (
|
|
60
153
|
<html lang="en">
|
|
61
|
-
|
|
154
|
+
...
|
|
155
|
+
<Outlet />
|
|
156
|
+
...
|
|
157
|
+
</html>
|
|
62
158
|
);
|
|
63
159
|
}
|
|
160
|
+
|
|
161
|
+
// Remember to remove the Layout export from your root.tsx
|
|
64
162
|
```
|
|
65
163
|
|
|
66
|
-
1.
|
|
164
|
+
1. Add a routes.ts file. This is your new Remix route configuration file.
|
|
67
165
|
|
|
68
166
|
```ts
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
167
|
+
import {flatRoutes} from '@remix-run/fs-routes';
|
|
168
|
+
import {layout, type RouteConfig} from '@remix-run/route-config';
|
|
169
|
+
import {hydrogenRoutes} from '@shopify/hydrogen';
|
|
72
170
|
|
|
73
171
|
export default hydrogenRoutes([
|
|
74
172
|
// Your entire app reading from routes folder using Layout from layout.tsx
|
|
75
|
-
layout(
|
|
173
|
+
layout('./layout.tsx', await flatRoutes()),
|
|
76
174
|
]) satisfies RouteConfig;
|
|
77
175
|
```
|
|
78
176
|
|
|
79
|
-
1. Update your `vite.config.ts`.
|
|
80
|
-
|
|
81
|
-
```diff
|
|
82
|
-
export default defineConfig({
|
|
83
|
-
plugins: [
|
|
84
|
-
hydrogen(),
|
|
85
|
-
oxygen(),
|
|
86
|
-
remix({
|
|
87
|
-
- presets: [hydrogen.preset()],
|
|
88
|
-
+ presets: [hydrogen.v3preset()],
|
|
89
|
-
```
|
|
90
|
-
|
|
91
177
|
- Updated dependencies [[`0425e50d`](https://github.com/Shopify/hydrogen/commit/0425e50dafe2f42326cba67076e5fcea2905e885), [`74ef1ba7`](https://github.com/Shopify/hydrogen/commit/74ef1ba7d41988350e9d2c81731c90381943d1f0)]:
|
|
92
178
|
- @shopify/remix-oxygen@2.0.12
|
|
93
179
|
- @shopify/hydrogen@2025.1.3
|
|
@@ -675,25 +761,25 @@
|
|
|
675
761
|
8. Update the `ProductForm` component.
|
|
676
762
|
|
|
677
763
|
```tsx
|
|
678
|
-
import {
|
|
679
|
-
import {
|
|
764
|
+
import {Link, useNavigate} from '@remix-run/react';
|
|
765
|
+
import {type MappedProductOptions} from '@shopify/hydrogen';
|
|
680
766
|
import type {
|
|
681
767
|
Maybe,
|
|
682
768
|
ProductOptionValueSwatch,
|
|
683
|
-
} from
|
|
684
|
-
import {
|
|
685
|
-
import {
|
|
686
|
-
import type {
|
|
769
|
+
} from '@shopify/hydrogen/storefront-api-types';
|
|
770
|
+
import {AddToCartButton} from './AddToCartButton';
|
|
771
|
+
import {useAside} from './Aside';
|
|
772
|
+
import type {ProductFragment} from 'storefrontapi.generated';
|
|
687
773
|
|
|
688
774
|
export function ProductForm({
|
|
689
775
|
productOptions,
|
|
690
776
|
selectedVariant,
|
|
691
777
|
}: {
|
|
692
778
|
productOptions: MappedProductOptions[];
|
|
693
|
-
selectedVariant: ProductFragment[
|
|
779
|
+
selectedVariant: ProductFragment['selectedOrFirstAvailableVariant'];
|
|
694
780
|
}) {
|
|
695
781
|
const navigate = useNavigate();
|
|
696
|
-
const {
|
|
782
|
+
const {open} = useAside();
|
|
697
783
|
return (
|
|
698
784
|
<div className="product-form">
|
|
699
785
|
{productOptions.map((option) => (
|
|
@@ -727,8 +813,8 @@
|
|
|
727
813
|
to={`/products/${handle}?${variantUriQuery}`}
|
|
728
814
|
style={{
|
|
729
815
|
border: selected
|
|
730
|
-
?
|
|
731
|
-
:
|
|
816
|
+
? '1px solid black'
|
|
817
|
+
: '1px solid transparent',
|
|
732
818
|
opacity: available ? 1 : 0.3,
|
|
733
819
|
}}
|
|
734
820
|
>
|
|
@@ -745,13 +831,13 @@
|
|
|
745
831
|
<button
|
|
746
832
|
type="button"
|
|
747
833
|
className={`product-options-item${
|
|
748
|
-
exists && !selected ?
|
|
834
|
+
exists && !selected ? ' link' : ''
|
|
749
835
|
}`}
|
|
750
836
|
key={option.name + name}
|
|
751
837
|
style={{
|
|
752
838
|
border: selected
|
|
753
|
-
?
|
|
754
|
-
:
|
|
839
|
+
? '1px solid black'
|
|
840
|
+
: '1px solid transparent',
|
|
755
841
|
opacity: available ? 1 : 0.3,
|
|
756
842
|
}}
|
|
757
843
|
disabled={!exists}
|
|
@@ -775,7 +861,7 @@
|
|
|
775
861
|
<AddToCartButton
|
|
776
862
|
disabled={!selectedVariant || !selectedVariant.availableForSale}
|
|
777
863
|
onClick={() => {
|
|
778
|
-
open(
|
|
864
|
+
open('cart');
|
|
779
865
|
}}
|
|
780
866
|
lines={
|
|
781
867
|
selectedVariant
|
|
@@ -789,7 +875,7 @@
|
|
|
789
875
|
: []
|
|
790
876
|
}
|
|
791
877
|
>
|
|
792
|
-
{selectedVariant?.availableForSale ?
|
|
878
|
+
{selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
|
|
793
879
|
</AddToCartButton>
|
|
794
880
|
</div>
|
|
795
881
|
);
|
|
@@ -812,7 +898,7 @@
|
|
|
812
898
|
aria-label={name}
|
|
813
899
|
className="product-option-label-swatch"
|
|
814
900
|
style={{
|
|
815
|
-
backgroundColor: color ||
|
|
901
|
+
backgroundColor: color || 'transparent',
|
|
816
902
|
}}
|
|
817
903
|
>
|
|
818
904
|
{!!image && <img src={image} alt={name} />}
|
|
@@ -1313,21 +1399,21 @@
|
|
|
1313
1399
|
New `withCache.fetch` is for caching simple fetch requests. This method caches the responses if they are OK responses, and you can pass `shouldCacheResponse`, `cacheKey`, etc. to modify behavior. `data` is the consumed body of the response (we need to consume to cache it).
|
|
1314
1400
|
|
|
1315
1401
|
```ts
|
|
1316
|
-
const withCache = createWithCache({
|
|
1402
|
+
const withCache = createWithCache({cache, waitUntil, request});
|
|
1317
1403
|
|
|
1318
|
-
const {
|
|
1319
|
-
|
|
1404
|
+
const {data, response} = await withCache.fetch<{data: T; error: string}>(
|
|
1405
|
+
'my-cms.com/api',
|
|
1320
1406
|
{
|
|
1321
|
-
method:
|
|
1322
|
-
headers: {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: {'Content-type': 'application/json'},
|
|
1323
1409
|
body,
|
|
1324
1410
|
},
|
|
1325
1411
|
{
|
|
1326
1412
|
cacheStrategy: CacheLong(),
|
|
1327
1413
|
// Cache if there are no data errors or a specific data that make this result not suited for caching
|
|
1328
1414
|
shouldCacheResponse: (result) => !result?.error,
|
|
1329
|
-
cacheKey: [
|
|
1330
|
-
displayName:
|
|
1415
|
+
cacheKey: ['my-cms', body],
|
|
1416
|
+
displayName: 'My CMS query',
|
|
1331
1417
|
},
|
|
1332
1418
|
);
|
|
1333
1419
|
```
|
|
@@ -1903,9 +1989,9 @@
|
|
|
1903
1989
|
|
|
1904
1990
|
```tsx
|
|
1905
1991
|
// app/lib/root-data.ts
|
|
1906
|
-
import {
|
|
1907
|
-
import type {
|
|
1908
|
-
import type {
|
|
1992
|
+
import {useMatches} from '@remix-run/react';
|
|
1993
|
+
import type {SerializeFrom} from '@shopify/remix-oxygen';
|
|
1994
|
+
import type {loader} from '~/root';
|
|
1909
1995
|
|
|
1910
1996
|
/**
|
|
1911
1997
|
* Access the result of the root loader from a React component.
|
|
@@ -2067,10 +2153,10 @@
|
|
|
2067
2153
|
- This is an important fix to a bug with 404 routes and path-based i18n projects where some unknown routes would not properly render a 404. This fixes all new projects, but to fix existing projects, add a `($locale).tsx` route with the following contents: ([#1732](https://github.com/Shopify/hydrogen/pull/1732)) by [@blittle](https://github.com/blittle)
|
|
2068
2154
|
|
|
2069
2155
|
```ts
|
|
2070
|
-
import {
|
|
2156
|
+
import {type LoaderFunctionArgs} from '@remix-run/server-runtime';
|
|
2071
2157
|
|
|
2072
|
-
export async function loader({
|
|
2073
|
-
const {
|
|
2158
|
+
export async function loader({params, context}: LoaderFunctionArgs) {
|
|
2159
|
+
const {language, country} = context.storefront.i18n;
|
|
2074
2160
|
|
|
2075
2161
|
if (
|
|
2076
2162
|
params.locale &&
|
|
@@ -2078,7 +2164,7 @@
|
|
|
2078
2164
|
) {
|
|
2079
2165
|
// If the locale URL param is defined, yet we still are still at the default locale
|
|
2080
2166
|
// then the the locale param must be invalid, send to the 404 page
|
|
2081
|
-
throw new Response(null, {
|
|
2167
|
+
throw new Response(null, {status: 404});
|
|
2082
2168
|
}
|
|
2083
2169
|
|
|
2084
2170
|
return null;
|
|
@@ -2134,11 +2220,11 @@
|
|
|
2134
2220
|
```yaml
|
|
2135
2221
|
projects:
|
|
2136
2222
|
default:
|
|
2137
|
-
schema:
|
|
2223
|
+
schema: 'node_modules/@shopify/hydrogen/storefront.schema.json'
|
|
2138
2224
|
documents:
|
|
2139
|
-
-
|
|
2140
|
-
-
|
|
2141
|
-
-
|
|
2225
|
+
- '!*.d.ts'
|
|
2226
|
+
- '*.{ts,tsx,js,jsx}'
|
|
2227
|
+
- 'app/**/*.{ts,tsx,js,jsx}'
|
|
2142
2228
|
```
|
|
2143
2229
|
|
|
2144
2230
|
- Improve resiliency of `HydrogenSession` ([#1583](https://github.com/Shopify/hydrogen/pull/1583)) by [@blittle](https://github.com/blittle)
|
|
@@ -2353,8 +2439,8 @@
|
|
|
2353
2439
|
```ts
|
|
2354
2440
|
// root.tsx
|
|
2355
2441
|
|
|
2356
|
-
import {
|
|
2357
|
-
import {
|
|
2442
|
+
import {useMatches} from '@remix-run/react';
|
|
2443
|
+
import {type SerializeFrom} from '@shopify/remix-oxygen';
|
|
2358
2444
|
|
|
2359
2445
|
export const useRootLoaderData = () => {
|
|
2360
2446
|
const [root] = useMatches();
|
|
@@ -123,6 +123,7 @@ function CartLineRemoveButton({
|
|
|
123
123
|
}) {
|
|
124
124
|
return (
|
|
125
125
|
<CartForm
|
|
126
|
+
fetcherKey={getUpdateKey(lineIds)}
|
|
126
127
|
route="/cart"
|
|
127
128
|
action={CartForm.ACTIONS.LinesRemove}
|
|
128
129
|
inputs={{lineIds}}
|
|
@@ -141,8 +142,11 @@ function CartLineUpdateButton({
|
|
|
141
142
|
children: React.ReactNode;
|
|
142
143
|
lines: CartLineUpdateInput[];
|
|
143
144
|
}) {
|
|
145
|
+
const lineIds = lines.map((line) => line.id);
|
|
146
|
+
|
|
144
147
|
return (
|
|
145
148
|
<CartForm
|
|
149
|
+
fetcherKey={getUpdateKey(lineIds)}
|
|
146
150
|
route="/cart"
|
|
147
151
|
action={CartForm.ACTIONS.LinesUpdate}
|
|
148
152
|
inputs={{lines}}
|
|
@@ -151,3 +155,14 @@ function CartLineUpdateButton({
|
|
|
151
155
|
</CartForm>
|
|
152
156
|
);
|
|
153
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns a unique key for the update action. This is used to make sure actions modifying the same line
|
|
161
|
+
* items are not run concurrently, but cancel each other. For example, if the user clicks "Increase quantity"
|
|
162
|
+
* and "Decrease quantity" in rapid succession, the actions will cancel each other and only the last one will run.
|
|
163
|
+
* @param lineIds - line ids affected by the update
|
|
164
|
+
* @returns
|
|
165
|
+
*/
|
|
166
|
+
function getUpdateKey(lineIds: string[]) {
|
|
167
|
+
return [CartForm.ACTIONS.LinesUpdate, ...lineIds].join('-');
|
|
168
|
+
}
|
|
@@ -26,7 +26,7 @@ export function CartMain({layout, cart: originalCart}: CartMainProps) {
|
|
|
26
26
|
cart &&
|
|
27
27
|
Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length);
|
|
28
28
|
const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
|
|
29
|
-
const cartHasItems = cart?.totalQuantity
|
|
29
|
+
const cartHasItems = cart?.totalQuantity ? cart.totalQuantity > 0 : false;
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<div className={className}>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {Link} from '@remix-run/react';
|
|
2
|
+
import {Image, Money} from '@shopify/hydrogen';
|
|
3
|
+
import type {
|
|
4
|
+
ProductItemFragment,
|
|
5
|
+
CollectionItemFragment,
|
|
6
|
+
RecommendedProductFragment,
|
|
7
|
+
} from 'storefrontapi.generated';
|
|
8
|
+
import {useVariantUrl} from '~/lib/variants';
|
|
9
|
+
|
|
10
|
+
export function ProductItem({
|
|
11
|
+
product,
|
|
12
|
+
loading,
|
|
13
|
+
}: {
|
|
14
|
+
product:
|
|
15
|
+
| CollectionItemFragment
|
|
16
|
+
| ProductItemFragment
|
|
17
|
+
| RecommendedProductFragment;
|
|
18
|
+
loading?: 'eager' | 'lazy';
|
|
19
|
+
}) {
|
|
20
|
+
const variantUrl = useVariantUrl(product.handle);
|
|
21
|
+
const image = product.featuredImage;
|
|
22
|
+
return (
|
|
23
|
+
<Link
|
|
24
|
+
className="product-item"
|
|
25
|
+
key={product.id}
|
|
26
|
+
prefetch="intent"
|
|
27
|
+
to={variantUrl}
|
|
28
|
+
>
|
|
29
|
+
{image && (
|
|
30
|
+
<Image
|
|
31
|
+
alt={image.altText || product.title}
|
|
32
|
+
aspectRatio="1/1"
|
|
33
|
+
data={image}
|
|
34
|
+
loading={loading}
|
|
35
|
+
sizes="(min-width: 45em) 400px, 100vw"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
<h4>{product.title}</h4>
|
|
39
|
+
<small>
|
|
40
|
+
<Money data={product.priceRange.minVariantPrice} />
|
|
41
|
+
</small>
|
|
42
|
+
</Link>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {redirect} from '@shopify/remix-oxygen';
|
|
2
|
+
|
|
3
|
+
export function redirectIfHandleIsLocalized(
|
|
4
|
+
request: Request,
|
|
5
|
+
...localizedResources: Array<{
|
|
6
|
+
handle: string;
|
|
7
|
+
data: {handle: string} & unknown;
|
|
8
|
+
}>
|
|
9
|
+
) {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
let shouldRedirect = false;
|
|
12
|
+
|
|
13
|
+
localizedResources.forEach(({handle, data}) => {
|
|
14
|
+
if (handle !== data.handle) {
|
|
15
|
+
url.pathname = url.pathname.replace(handle, data.handle);
|
|
16
|
+
shouldRedirect = true;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (shouldRedirect) {
|
|
21
|
+
throw redirect(url.toString());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {getShopAnalytics} from '@shopify/hydrogen';
|
|
1
|
+
import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
|
|
2
2
|
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
3
3
|
import {
|
|
4
4
|
Outlet,
|
|
5
5
|
useRouteError,
|
|
6
6
|
isRouteErrorResponse,
|
|
7
7
|
type ShouldRevalidateFunction,
|
|
8
|
+
Links,
|
|
9
|
+
Meta,
|
|
10
|
+
Scripts,
|
|
11
|
+
ScrollRestoration,
|
|
12
|
+
useRouteLoaderData,
|
|
8
13
|
} from '@remix-run/react';
|
|
9
14
|
import favicon from '~/assets/favicon.svg';
|
|
10
15
|
import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
|
|
16
|
+
import resetStyles from '~/styles/reset.css?url';
|
|
17
|
+
import appStyles from '~/styles/app.css?url';
|
|
18
|
+
import {PageLayout} from './components/PageLayout';
|
|
11
19
|
|
|
12
20
|
export type RootLoader = typeof loader;
|
|
13
21
|
|
|
@@ -25,9 +33,9 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({
|
|
|
25
33
|
// revalidate when manually revalidating via useRevalidator
|
|
26
34
|
if (currentUrl.toString() === nextUrl.toString()) return true;
|
|
27
35
|
|
|
28
|
-
// Defaulting to no revalidation for root loader data to improve performance.
|
|
29
|
-
// When using this feature, you risk your UI getting out of sync with your server.
|
|
30
|
-
// Use with caution. If you are uncomfortable with this optimization, update the
|
|
36
|
+
// Defaulting to no revalidation for root loader data to improve performance.
|
|
37
|
+
// When using this feature, you risk your UI getting out of sync with your server.
|
|
38
|
+
// Use with caution. If you are uncomfortable with this optimization, update the
|
|
31
39
|
// line below to `return defaultShouldRevalidate` instead.
|
|
32
40
|
// For more details see: https://remix.run/docs/en/main/route/should-revalidate
|
|
33
41
|
return false;
|
|
@@ -133,6 +141,39 @@ function loadDeferredData({context}: LoaderFunctionArgs) {
|
|
|
133
141
|
};
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
export function Layout({children}: {children?: React.ReactNode}) {
|
|
145
|
+
const nonce = useNonce();
|
|
146
|
+
const data = useRouteLoaderData<RootLoader>('root');
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<html lang="en">
|
|
150
|
+
<head>
|
|
151
|
+
<meta charSet="utf-8" />
|
|
152
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
153
|
+
<link rel="stylesheet" href={resetStyles}></link>
|
|
154
|
+
<link rel="stylesheet" href={appStyles}></link>
|
|
155
|
+
<Meta />
|
|
156
|
+
<Links />
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
{data ? (
|
|
160
|
+
<Analytics.Provider
|
|
161
|
+
cart={data.cart}
|
|
162
|
+
shop={data.shop}
|
|
163
|
+
consent={data.consent}
|
|
164
|
+
>
|
|
165
|
+
<PageLayout {...data}>{children}</PageLayout>
|
|
166
|
+
</Analytics.Provider>
|
|
167
|
+
) : (
|
|
168
|
+
children
|
|
169
|
+
)}
|
|
170
|
+
<ScrollRestoration nonce={nonce} />
|
|
171
|
+
<Scripts nonce={nonce} />
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
136
177
|
export default function App() {
|
|
137
178
|
return <Outlet />;
|
|
138
179
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
FeaturedCollectionFragment,
|
|
7
7
|
RecommendedProductsQuery,
|
|
8
8
|
} from 'storefrontapi.generated';
|
|
9
|
+
import {ProductItem} from '~/components/ProductItem';
|
|
9
10
|
|
|
10
11
|
export const meta: MetaFunction = () => {
|
|
11
12
|
return [{title: 'Hydrogen | Home'}];
|
|
@@ -101,21 +102,7 @@ function RecommendedProducts({
|
|
|
101
102
|
<div className="recommended-products-grid">
|
|
102
103
|
{response
|
|
103
104
|
? response.products.nodes.map((product) => (
|
|
104
|
-
<
|
|
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>
|
|
105
|
+
<ProductItem key={product.id} product={product} />
|
|
119
106
|
))
|
|
120
107
|
: null}
|
|
121
108
|
</div>
|
|
@@ -161,14 +148,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
|
|
|
161
148
|
currencyCode
|
|
162
149
|
}
|
|
163
150
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
height
|
|
171
|
-
}
|
|
151
|
+
featuredImage {
|
|
152
|
+
id
|
|
153
|
+
url
|
|
154
|
+
altText
|
|
155
|
+
width
|
|
156
|
+
height
|
|
172
157
|
}
|
|
173
158
|
}
|
|
174
159
|
query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
2
|
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
3
|
import {Image} from '@shopify/hydrogen';
|
|
4
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
4
5
|
|
|
5
6
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
6
7
|
return [{title: `Hydrogen | ${data?.article.title ?? ''} article`}];
|
|
@@ -20,7 +21,11 @@ export async function loader(args: LoaderFunctionArgs) {
|
|
|
20
21
|
* Load data necessary for rendering content above the fold. This is the critical data
|
|
21
22
|
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
22
23
|
*/
|
|
23
|
-
async function loadCriticalData({
|
|
24
|
+
async function loadCriticalData({
|
|
25
|
+
context,
|
|
26
|
+
request,
|
|
27
|
+
params,
|
|
28
|
+
}: LoaderFunctionArgs) {
|
|
24
29
|
const {blogHandle, articleHandle} = params;
|
|
25
30
|
|
|
26
31
|
if (!articleHandle || !blogHandle) {
|
|
@@ -38,6 +43,18 @@ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
|
|
|
38
43
|
throw new Response(null, {status: 404});
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
redirectIfHandleIsLocalized(
|
|
47
|
+
request,
|
|
48
|
+
{
|
|
49
|
+
handle: articleHandle,
|
|
50
|
+
data: blog.articleByHandle,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
handle: blogHandle,
|
|
54
|
+
data: blog,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
41
58
|
const article = blog.articleByHandle;
|
|
42
59
|
|
|
43
60
|
return {article};
|
|
@@ -67,7 +84,8 @@ export default function Article() {
|
|
|
67
84
|
<h1>
|
|
68
85
|
{title}
|
|
69
86
|
<div>
|
|
70
|
-
{publishedDate} ·
|
|
87
|
+
<time dateTime={article.publishedAt}>{publishedDate}</time> ·{' '}
|
|
88
|
+
<address>{author?.name}</address>
|
|
71
89
|
</div>
|
|
72
90
|
</h1>
|
|
73
91
|
|
|
@@ -89,7 +107,9 @@ const ARTICLE_QUERY = `#graphql
|
|
|
89
107
|
$language: LanguageCode
|
|
90
108
|
) @inContext(language: $language, country: $country) {
|
|
91
109
|
blog(handle: $blogHandle) {
|
|
110
|
+
handle
|
|
92
111
|
articleByHandle(handle: $articleHandle) {
|
|
112
|
+
handle
|
|
93
113
|
title
|
|
94
114
|
contentHtml
|
|
95
115
|
publishedAt
|
|
@@ -3,6 +3,7 @@ import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
|
3
3
|
import {Image, getPaginationVariables} from '@shopify/hydrogen';
|
|
4
4
|
import type {ArticleItemFragment} from 'storefrontapi.generated';
|
|
5
5
|
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
|
|
6
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
6
7
|
|
|
7
8
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
8
9
|
return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
|
|
@@ -49,6 +50,8 @@ async function loadCriticalData({
|
|
|
49
50
|
throw new Response('Not found', {status: 404});
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
redirectIfHandleIsLocalized(request, {handle: params.blogHandle, data: blog});
|
|
54
|
+
|
|
52
55
|
return {blog};
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -128,6 +131,7 @@ const BLOGS_QUERY = `#graphql
|
|
|
128
131
|
) @inContext(language: $language) {
|
|
129
132
|
blog(handle: $blogHandle) {
|
|
130
133
|
title
|
|
134
|
+
handle
|
|
131
135
|
seo {
|
|
132
136
|
title
|
|
133
137
|
description
|