hydrogen-forge 0.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 (118) hide show
  1. package/README.md +212 -0
  2. package/dist/commands/add.d.ts +7 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +123 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/create.d.ts +8 -0
  7. package/dist/commands/create.d.ts.map +1 -0
  8. package/dist/commands/create.js +160 -0
  9. package/dist/commands/create.js.map +1 -0
  10. package/dist/commands/setup-mcp.d.ts +7 -0
  11. package/dist/commands/setup-mcp.d.ts.map +1 -0
  12. package/dist/commands/setup-mcp.js +179 -0
  13. package/dist/commands/setup-mcp.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +50 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/generators.d.ts +6 -0
  19. package/dist/lib/generators.d.ts.map +1 -0
  20. package/dist/lib/generators.js +470 -0
  21. package/dist/lib/generators.js.map +1 -0
  22. package/dist/lib/utils.d.ts +17 -0
  23. package/dist/lib/utils.d.ts.map +1 -0
  24. package/dist/lib/utils.js +101 -0
  25. package/dist/lib/utils.js.map +1 -0
  26. package/package.json +54 -0
  27. package/templates/starter/.env.example +21 -0
  28. package/templates/starter/.graphqlrc.ts +27 -0
  29. package/templates/starter/README.md +117 -0
  30. package/templates/starter/app/assets/favicon.svg +28 -0
  31. package/templates/starter/app/components/AddToCartButton.tsx +102 -0
  32. package/templates/starter/app/components/Aside.tsx +136 -0
  33. package/templates/starter/app/components/CartLineItem.tsx +229 -0
  34. package/templates/starter/app/components/CartMain.tsx +131 -0
  35. package/templates/starter/app/components/CartSummary.tsx +315 -0
  36. package/templates/starter/app/components/CollectionFilters.tsx +330 -0
  37. package/templates/starter/app/components/CollectionGrid.tsx +141 -0
  38. package/templates/starter/app/components/Footer.tsx +218 -0
  39. package/templates/starter/app/components/Header.tsx +296 -0
  40. package/templates/starter/app/components/PageLayout.tsx +174 -0
  41. package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
  42. package/templates/starter/app/components/ProductCard.tsx +151 -0
  43. package/templates/starter/app/components/ProductForm.tsx +156 -0
  44. package/templates/starter/app/components/ProductGallery.tsx +164 -0
  45. package/templates/starter/app/components/ProductGrid.tsx +64 -0
  46. package/templates/starter/app/components/ProductImage.tsx +23 -0
  47. package/templates/starter/app/components/ProductItem.tsx +44 -0
  48. package/templates/starter/app/components/ProductPrice.tsx +97 -0
  49. package/templates/starter/app/components/SearchDialog.tsx +599 -0
  50. package/templates/starter/app/components/SearchForm.tsx +68 -0
  51. package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
  52. package/templates/starter/app/components/SearchResults.tsx +161 -0
  53. package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
  54. package/templates/starter/app/entry.client.tsx +21 -0
  55. package/templates/starter/app/entry.server.tsx +53 -0
  56. package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
  57. package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  58. package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
  59. package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
  60. package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
  61. package/templates/starter/app/lib/context.ts +60 -0
  62. package/templates/starter/app/lib/fragments.ts +234 -0
  63. package/templates/starter/app/lib/orderFilters.ts +90 -0
  64. package/templates/starter/app/lib/redirect.ts +23 -0
  65. package/templates/starter/app/lib/search.ts +79 -0
  66. package/templates/starter/app/lib/session.ts +72 -0
  67. package/templates/starter/app/lib/variants.ts +46 -0
  68. package/templates/starter/app/root.tsx +209 -0
  69. package/templates/starter/app/routes/$.tsx +11 -0
  70. package/templates/starter/app/routes/[robots.txt].tsx +117 -0
  71. package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
  72. package/templates/starter/app/routes/_index.tsx +167 -0
  73. package/templates/starter/app/routes/account.$.tsx +9 -0
  74. package/templates/starter/app/routes/account._index.tsx +5 -0
  75. package/templates/starter/app/routes/account.addresses.tsx +516 -0
  76. package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
  77. package/templates/starter/app/routes/account.orders._index.tsx +222 -0
  78. package/templates/starter/app/routes/account.profile.tsx +133 -0
  79. package/templates/starter/app/routes/account.tsx +97 -0
  80. package/templates/starter/app/routes/account_.authorize.tsx +5 -0
  81. package/templates/starter/app/routes/account_.login.tsx +7 -0
  82. package/templates/starter/app/routes/account_.logout.tsx +11 -0
  83. package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
  84. package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
  85. package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
  86. package/templates/starter/app/routes/blogs._index.tsx +109 -0
  87. package/templates/starter/app/routes/cart.$lines.tsx +70 -0
  88. package/templates/starter/app/routes/cart.tsx +117 -0
  89. package/templates/starter/app/routes/collections.$handle.tsx +161 -0
  90. package/templates/starter/app/routes/collections._index.tsx +133 -0
  91. package/templates/starter/app/routes/collections.all.tsx +122 -0
  92. package/templates/starter/app/routes/discount.$code.tsx +48 -0
  93. package/templates/starter/app/routes/pages.$handle.tsx +88 -0
  94. package/templates/starter/app/routes/policies.$handle.tsx +93 -0
  95. package/templates/starter/app/routes/policies._index.tsx +69 -0
  96. package/templates/starter/app/routes/products.$handle.tsx +232 -0
  97. package/templates/starter/app/routes/search.tsx +426 -0
  98. package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
  99. package/templates/starter/app/routes.ts +9 -0
  100. package/templates/starter/app/styles/app.css +574 -0
  101. package/templates/starter/app/styles/reset.css +139 -0
  102. package/templates/starter/app/styles/tailwind.css +116 -0
  103. package/templates/starter/customer-accountapi.generated.d.ts +543 -0
  104. package/templates/starter/env.d.ts +7 -0
  105. package/templates/starter/eslint.config.js +247 -0
  106. package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  107. package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
  108. package/templates/starter/guides/search/search.jpg +0 -0
  109. package/templates/starter/guides/search/search.md +335 -0
  110. package/templates/starter/package.json +71 -0
  111. package/templates/starter/postcss.config.js +6 -0
  112. package/templates/starter/public/.gitkeep +0 -0
  113. package/templates/starter/react-router.config.ts +13 -0
  114. package/templates/starter/server.ts +59 -0
  115. package/templates/starter/storefrontapi.generated.d.ts +1264 -0
  116. package/templates/starter/tailwind.config.js +83 -0
  117. package/templates/starter/tsconfig.json +67 -0
  118. package/templates/starter/vite.config.ts +32 -0
@@ -0,0 +1,161 @@
1
+ import {redirect, useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/collections.$handle';
3
+ import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
4
+ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
6
+ import {ProductItem} from '~/components/ProductItem';
7
+ import type {ProductItemFragment} from 'storefrontapi.generated';
8
+
9
+ export const meta: Route.MetaFunction = ({data}) => {
10
+ return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
11
+ };
12
+
13
+ export async function loader(args: Route.LoaderArgs) {
14
+ // Start fetching non-critical data without blocking time to first byte
15
+ const deferredData = loadDeferredData(args);
16
+
17
+ // Await the critical data required to render initial state of the page
18
+ const criticalData = await loadCriticalData(args);
19
+
20
+ return {...deferredData, ...criticalData};
21
+ }
22
+
23
+ /**
24
+ * Load data necessary for rendering content above the fold. This is the critical data
25
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
26
+ */
27
+ async function loadCriticalData({context, params, request}: Route.LoaderArgs) {
28
+ const {handle} = params;
29
+ const {storefront} = context;
30
+ const paginationVariables = getPaginationVariables(request, {
31
+ pageBy: 8,
32
+ });
33
+
34
+ if (!handle) {
35
+ throw redirect('/collections');
36
+ }
37
+
38
+ const [{collection}] = await Promise.all([
39
+ storefront.query(COLLECTION_QUERY, {
40
+ variables: {handle, ...paginationVariables},
41
+ // Add other queries here, so that they are loaded in parallel
42
+ }),
43
+ ]);
44
+
45
+ if (!collection) {
46
+ throw new Response(`Collection ${handle} not found`, {
47
+ status: 404,
48
+ });
49
+ }
50
+
51
+ // The API handle might be localized, so redirect to the localized handle
52
+ redirectIfHandleIsLocalized(request, {handle, data: collection});
53
+
54
+ return {
55
+ collection,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Load data for rendering content below the fold. This data is deferred and will be
61
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
62
+ * Make sure to not throw any errors here, as it will cause the page to 500.
63
+ */
64
+ function loadDeferredData({context}: Route.LoaderArgs) {
65
+ return {};
66
+ }
67
+
68
+ export default function Collection() {
69
+ const {collection} = useLoaderData<typeof loader>();
70
+
71
+ return (
72
+ <div className="collection">
73
+ <h1>{collection.title}</h1>
74
+ <p className="collection-description">{collection.description}</p>
75
+ <PaginatedResourceSection<ProductItemFragment>
76
+ connection={collection.products}
77
+ resourcesClassName="products-grid"
78
+ >
79
+ {({node: product, index}) => (
80
+ <ProductItem
81
+ key={product.id}
82
+ product={product}
83
+ loading={index < 8 ? 'eager' : undefined}
84
+ />
85
+ )}
86
+ </PaginatedResourceSection>
87
+ <Analytics.CollectionView
88
+ data={{
89
+ collection: {
90
+ id: collection.id,
91
+ handle: collection.handle,
92
+ },
93
+ }}
94
+ />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
100
+ fragment MoneyProductItem on MoneyV2 {
101
+ amount
102
+ currencyCode
103
+ }
104
+ fragment ProductItem on Product {
105
+ id
106
+ handle
107
+ title
108
+ featuredImage {
109
+ id
110
+ altText
111
+ url
112
+ width
113
+ height
114
+ }
115
+ priceRange {
116
+ minVariantPrice {
117
+ ...MoneyProductItem
118
+ }
119
+ maxVariantPrice {
120
+ ...MoneyProductItem
121
+ }
122
+ }
123
+ }
124
+ ` as const;
125
+
126
+ // NOTE: https://shopify.dev/docs/api/storefront/2022-04/objects/collection
127
+ const COLLECTION_QUERY = `#graphql
128
+ ${PRODUCT_ITEM_FRAGMENT}
129
+ query Collection(
130
+ $handle: String!
131
+ $country: CountryCode
132
+ $language: LanguageCode
133
+ $first: Int
134
+ $last: Int
135
+ $startCursor: String
136
+ $endCursor: String
137
+ ) @inContext(country: $country, language: $language) {
138
+ collection(handle: $handle) {
139
+ id
140
+ handle
141
+ title
142
+ description
143
+ products(
144
+ first: $first,
145
+ last: $last,
146
+ before: $startCursor,
147
+ after: $endCursor
148
+ ) {
149
+ nodes {
150
+ ...ProductItem
151
+ }
152
+ pageInfo {
153
+ hasPreviousPage
154
+ hasNextPage
155
+ endCursor
156
+ startCursor
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ` as const;
@@ -0,0 +1,133 @@
1
+ import {useLoaderData, Link} from 'react-router';
2
+ import type {Route} from './+types/collections._index';
3
+ import {getPaginationVariables, Image} from '@shopify/hydrogen';
4
+ import type {CollectionFragment} from 'storefrontapi.generated';
5
+ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
6
+
7
+ export async function loader(args: Route.LoaderArgs) {
8
+ // Start fetching non-critical data without blocking time to first byte
9
+ const deferredData = loadDeferredData(args);
10
+
11
+ // Await the critical data required to render initial state of the page
12
+ const criticalData = await loadCriticalData(args);
13
+
14
+ return {...deferredData, ...criticalData};
15
+ }
16
+
17
+ /**
18
+ * Load data necessary for rendering content above the fold. This is the critical data
19
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
20
+ */
21
+ async function loadCriticalData({context, request}: Route.LoaderArgs) {
22
+ const paginationVariables = getPaginationVariables(request, {
23
+ pageBy: 4,
24
+ });
25
+
26
+ const [{collections}] = await Promise.all([
27
+ context.storefront.query(COLLECTIONS_QUERY, {
28
+ variables: paginationVariables,
29
+ }),
30
+ // Add other queries here, so that they are loaded in parallel
31
+ ]);
32
+
33
+ return {collections};
34
+ }
35
+
36
+ /**
37
+ * Load data for rendering content below the fold. This data is deferred and will be
38
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
39
+ * Make sure to not throw any errors here, as it will cause the page to 500.
40
+ */
41
+ function loadDeferredData({context}: Route.LoaderArgs) {
42
+ return {};
43
+ }
44
+
45
+ export default function Collections() {
46
+ const {collections} = useLoaderData<typeof loader>();
47
+
48
+ return (
49
+ <div className="collections">
50
+ <h1>Collections</h1>
51
+ <PaginatedResourceSection<CollectionFragment>
52
+ connection={collections}
53
+ resourcesClassName="collections-grid"
54
+ >
55
+ {({node: collection, index}) => (
56
+ <CollectionItem
57
+ key={collection.id}
58
+ collection={collection}
59
+ index={index}
60
+ />
61
+ )}
62
+ </PaginatedResourceSection>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ function CollectionItem({
68
+ collection,
69
+ index,
70
+ }: {
71
+ collection: CollectionFragment;
72
+ index: number;
73
+ }) {
74
+ return (
75
+ <Link
76
+ className="collection-item"
77
+ key={collection.id}
78
+ to={`/collections/${collection.handle}`}
79
+ prefetch="intent"
80
+ >
81
+ {collection?.image && (
82
+ <Image
83
+ alt={collection.image.altText || collection.title}
84
+ aspectRatio="1/1"
85
+ data={collection.image}
86
+ loading={index < 3 ? 'eager' : undefined}
87
+ sizes="(min-width: 45em) 400px, 100vw"
88
+ />
89
+ )}
90
+ <h5>{collection.title}</h5>
91
+ </Link>
92
+ );
93
+ }
94
+
95
+ const COLLECTIONS_QUERY = `#graphql
96
+ fragment Collection on Collection {
97
+ id
98
+ title
99
+ handle
100
+ image {
101
+ id
102
+ url
103
+ altText
104
+ width
105
+ height
106
+ }
107
+ }
108
+ query StoreCollections(
109
+ $country: CountryCode
110
+ $endCursor: String
111
+ $first: Int
112
+ $language: LanguageCode
113
+ $last: Int
114
+ $startCursor: String
115
+ ) @inContext(country: $country, language: $language) {
116
+ collections(
117
+ first: $first,
118
+ last: $last,
119
+ before: $startCursor,
120
+ after: $endCursor
121
+ ) {
122
+ nodes {
123
+ ...Collection
124
+ }
125
+ pageInfo {
126
+ hasNextPage
127
+ hasPreviousPage
128
+ startCursor
129
+ endCursor
130
+ }
131
+ }
132
+ }
133
+ ` as const;
@@ -0,0 +1,122 @@
1
+ import type {Route} from './+types/collections.all';
2
+ import {useLoaderData} from 'react-router';
3
+ import {getPaginationVariables, Image, Money} from '@shopify/hydrogen';
4
+ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5
+ import {ProductItem} from '~/components/ProductItem';
6
+ import type {CollectionItemFragment} from 'storefrontapi.generated';
7
+
8
+ export const meta: Route.MetaFunction = () => {
9
+ return [{title: `Hydrogen | Products`}];
10
+ };
11
+
12
+ export async function loader(args: Route.LoaderArgs) {
13
+ // Start fetching non-critical data without blocking time to first byte
14
+ const deferredData = loadDeferredData(args);
15
+
16
+ // Await the critical data required to render initial state of the page
17
+ const criticalData = await loadCriticalData(args);
18
+
19
+ return {...deferredData, ...criticalData};
20
+ }
21
+
22
+ /**
23
+ * Load data necessary for rendering content above the fold. This is the critical data
24
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
25
+ */
26
+ async function loadCriticalData({context, request}: Route.LoaderArgs) {
27
+ const {storefront} = context;
28
+ const paginationVariables = getPaginationVariables(request, {
29
+ pageBy: 8,
30
+ });
31
+
32
+ const [{products}] = await Promise.all([
33
+ storefront.query(CATALOG_QUERY, {
34
+ variables: {...paginationVariables},
35
+ }),
36
+ // Add other queries here, so that they are loaded in parallel
37
+ ]);
38
+ return {products};
39
+ }
40
+
41
+ /**
42
+ * Load data for rendering content below the fold. This data is deferred and will be
43
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
44
+ * Make sure to not throw any errors here, as it will cause the page to 500.
45
+ */
46
+ function loadDeferredData({context}: Route.LoaderArgs) {
47
+ return {};
48
+ }
49
+
50
+ export default function Collection() {
51
+ const {products} = useLoaderData<typeof loader>();
52
+
53
+ return (
54
+ <div className="collection">
55
+ <h1>Products</h1>
56
+ <PaginatedResourceSection<CollectionItemFragment>
57
+ connection={products}
58
+ resourcesClassName="products-grid"
59
+ >
60
+ {({node: product, index}) => (
61
+ <ProductItem
62
+ key={product.id}
63
+ product={product}
64
+ loading={index < 8 ? 'eager' : undefined}
65
+ />
66
+ )}
67
+ </PaginatedResourceSection>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ const COLLECTION_ITEM_FRAGMENT = `#graphql
73
+ fragment MoneyCollectionItem on MoneyV2 {
74
+ amount
75
+ currencyCode
76
+ }
77
+ fragment CollectionItem on Product {
78
+ id
79
+ handle
80
+ title
81
+ featuredImage {
82
+ id
83
+ altText
84
+ url
85
+ width
86
+ height
87
+ }
88
+ priceRange {
89
+ minVariantPrice {
90
+ ...MoneyCollectionItem
91
+ }
92
+ maxVariantPrice {
93
+ ...MoneyCollectionItem
94
+ }
95
+ }
96
+ }
97
+ ` as const;
98
+
99
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/product
100
+ const CATALOG_QUERY = `#graphql
101
+ query Catalog(
102
+ $country: CountryCode
103
+ $language: LanguageCode
104
+ $first: Int
105
+ $last: Int
106
+ $startCursor: String
107
+ $endCursor: String
108
+ ) @inContext(country: $country, language: $language) {
109
+ products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
110
+ nodes {
111
+ ...CollectionItem
112
+ }
113
+ pageInfo {
114
+ hasPreviousPage
115
+ hasNextPage
116
+ startCursor
117
+ endCursor
118
+ }
119
+ }
120
+ }
121
+ ${COLLECTION_ITEM_FRAGMENT}
122
+ ` as const;
@@ -0,0 +1,48 @@
1
+ import {redirect} from 'react-router';
2
+ import type {Route} from './+types/discount.$code';
3
+
4
+ /**
5
+ * Automatically applies a discount found on the url
6
+ * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
7
+ *
8
+ * @example
9
+ * Example path applying a discount and optional redirecting (defaults to the home page)
10
+ * ```js
11
+ * /discount/FREESHIPPING?redirect=/products
12
+ *
13
+ * ```
14
+ */
15
+ export async function loader({request, context, params}: Route.LoaderArgs) {
16
+ const {cart} = context;
17
+ const {code} = params;
18
+
19
+ const url = new URL(request.url);
20
+ const searchParams = new URLSearchParams(url.search);
21
+ let redirectParam =
22
+ searchParams.get('redirect') || searchParams.get('return_to') || '/';
23
+
24
+ if (redirectParam.includes('//')) {
25
+ // Avoid redirecting to external URLs to prevent phishing attacks
26
+ redirectParam = '/';
27
+ }
28
+
29
+ searchParams.delete('redirect');
30
+ searchParams.delete('return_to');
31
+
32
+ const redirectUrl = `${redirectParam}?${searchParams}`;
33
+
34
+ if (!code) {
35
+ return redirect(redirectUrl);
36
+ }
37
+
38
+ const result = await cart.updateDiscountCodes([code]);
39
+ const headers = cart.setCartId(result.cart.id);
40
+
41
+ // Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000)
42
+ // If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie
43
+ // on localhost:3000
44
+ return redirect(redirectUrl, {
45
+ status: 303,
46
+ headers,
47
+ });
48
+ }
@@ -0,0 +1,88 @@
1
+ import {useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/pages.$handle';
3
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
4
+
5
+ export const meta: Route.MetaFunction = ({data}) => {
6
+ return [{title: `Hydrogen | ${data?.page.title ?? ''}`}];
7
+ };
8
+
9
+ export async function loader(args: Route.LoaderArgs) {
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 {...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, params}: Route.LoaderArgs) {
24
+ if (!params.handle) {
25
+ throw new Error('Missing page handle');
26
+ }
27
+
28
+ const [{page}] = await Promise.all([
29
+ context.storefront.query(PAGE_QUERY, {
30
+ variables: {
31
+ handle: params.handle,
32
+ },
33
+ }),
34
+ // Add other queries here, so that they are loaded in parallel
35
+ ]);
36
+
37
+ if (!page) {
38
+ throw new Response('Not Found', {status: 404});
39
+ }
40
+
41
+ redirectIfHandleIsLocalized(request, {handle: params.handle, data: page});
42
+
43
+ return {
44
+ page,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Load data for rendering content below the fold. This data is deferred and will be
50
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
51
+ * Make sure to not throw any errors here, as it will cause the page to 500.
52
+ */
53
+ function loadDeferredData({context}: Route.LoaderArgs) {
54
+ return {};
55
+ }
56
+
57
+ export default function Page() {
58
+ const {page} = useLoaderData<typeof loader>();
59
+
60
+ return (
61
+ <div className="page">
62
+ <header>
63
+ <h1>{page.title}</h1>
64
+ </header>
65
+ <main dangerouslySetInnerHTML={{__html: page.body}} />
66
+ </div>
67
+ );
68
+ }
69
+
70
+ const PAGE_QUERY = `#graphql
71
+ query Page(
72
+ $language: LanguageCode,
73
+ $country: CountryCode,
74
+ $handle: String!
75
+ )
76
+ @inContext(language: $language, country: $country) {
77
+ page(handle: $handle) {
78
+ handle
79
+ id
80
+ title
81
+ body
82
+ seo {
83
+ description
84
+ title
85
+ }
86
+ }
87
+ }
88
+ ` as const;
@@ -0,0 +1,93 @@
1
+ import {Link, useLoaderData} from 'react-router';
2
+ import type {Route} from './+types/policies.$handle';
3
+ import {type Shop} from '@shopify/hydrogen/storefront-api-types';
4
+
5
+ type SelectedPolicies = keyof Pick<
6
+ Shop,
7
+ 'privacyPolicy' | 'shippingPolicy' | 'termsOfService' | 'refundPolicy'
8
+ >;
9
+
10
+ export const meta: Route.MetaFunction = ({data}) => {
11
+ return [{title: `Hydrogen | ${data?.policy.title ?? ''}`}];
12
+ };
13
+
14
+ export async function loader({params, context}: Route.LoaderArgs) {
15
+ if (!params.handle) {
16
+ throw new Response('No handle was passed in', {status: 404});
17
+ }
18
+
19
+ const policyName = params.handle.replace(
20
+ /-([a-z])/g,
21
+ (_: unknown, m1: string) => m1.toUpperCase(),
22
+ ) as SelectedPolicies;
23
+
24
+ const data = await context.storefront.query(POLICY_CONTENT_QUERY, {
25
+ variables: {
26
+ privacyPolicy: false,
27
+ shippingPolicy: false,
28
+ termsOfService: false,
29
+ refundPolicy: false,
30
+ [policyName]: true,
31
+ language: context.storefront.i18n?.language,
32
+ },
33
+ });
34
+
35
+ const policy = data.shop?.[policyName];
36
+
37
+ if (!policy) {
38
+ throw new Response('Could not find the policy', {status: 404});
39
+ }
40
+
41
+ return {policy};
42
+ }
43
+
44
+ export default function Policy() {
45
+ const {policy} = useLoaderData<typeof loader>();
46
+
47
+ return (
48
+ <div className="policy">
49
+ <br />
50
+ <br />
51
+ <div>
52
+ <Link to="/policies">← Back to Policies</Link>
53
+ </div>
54
+ <br />
55
+ <h1>{policy.title}</h1>
56
+ <div dangerouslySetInnerHTML={{__html: policy.body}} />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/Shop
62
+ const POLICY_CONTENT_QUERY = `#graphql
63
+ fragment Policy on ShopPolicy {
64
+ body
65
+ handle
66
+ id
67
+ title
68
+ url
69
+ }
70
+ query Policy(
71
+ $country: CountryCode
72
+ $language: LanguageCode
73
+ $privacyPolicy: Boolean!
74
+ $refundPolicy: Boolean!
75
+ $shippingPolicy: Boolean!
76
+ $termsOfService: Boolean!
77
+ ) @inContext(language: $language, country: $country) {
78
+ shop {
79
+ privacyPolicy @include(if: $privacyPolicy) {
80
+ ...Policy
81
+ }
82
+ shippingPolicy @include(if: $shippingPolicy) {
83
+ ...Policy
84
+ }
85
+ termsOfService @include(if: $termsOfService) {
86
+ ...Policy
87
+ }
88
+ refundPolicy @include(if: $refundPolicy) {
89
+ ...Policy
90
+ }
91
+ }
92
+ }
93
+ ` as const;