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,90 @@
1
+ /**
2
+ * Field name constants for order filtering
3
+ */
4
+ export const ORDER_FILTER_FIELDS = {
5
+ NAME: 'name',
6
+ CONFIRMATION_NUMBER: 'confirmation_number',
7
+ } as const;
8
+
9
+ /**
10
+ * Parameters for filtering customer orders, see: https://shopify.dev/docs/api/customer/latest/queries/customer#returns-Customer.fields.orders.arguments.query
11
+ */
12
+ export interface OrderFilterParams {
13
+ /** Order name or number (e.g., "#1001" or "1001") */
14
+ name?: string;
15
+ /** Order confirmation number */
16
+ confirmationNumber?: string;
17
+ }
18
+
19
+ /**
20
+ * Sanitizes a filter value to prevent injection attacks or malformed queries.
21
+ * Allows only alphanumeric characters, underscore, and dash.
22
+ * @param value - The input string to sanitize
23
+ * @returns The sanitized string
24
+ */
25
+ function sanitizeFilterValue(value: string): string {
26
+ // Only allow alphanumeric, underscore, and dash
27
+ // Remove anything else to prevent injection
28
+ return value.replace(/[^a-zA-Z0-9_\-]/g, '');
29
+ }
30
+
31
+ /**
32
+ * Builds a query string for filtering customer orders using the Customer Account API
33
+ * @param filters - The filter parameters
34
+ * @returns A formatted query string for the GraphQL query parameter, or undefined if no filters
35
+ * @example
36
+ * buildOrderSearchQuery(\{ name: '1001' \}) // returns "name:1001"
37
+ * buildOrderSearchQuery(\{ name: '1001', confirmationNumber: 'ABC123' \}) // returns "name:1001 AND confirmation_number:ABC123"
38
+ */
39
+ export function buildOrderSearchQuery(
40
+ filters: OrderFilterParams,
41
+ ): string | undefined {
42
+ const queryParts: string[] = [];
43
+
44
+ if (filters.name) {
45
+ // Remove # if present and trim
46
+ const cleanName = filters.name.replace(/^#/, '').trim();
47
+ const sanitizedName = sanitizeFilterValue(cleanName);
48
+ if (sanitizedName) {
49
+ queryParts.push(`name:${sanitizedName}`);
50
+ }
51
+ }
52
+
53
+ if (filters.confirmationNumber) {
54
+ const cleanConfirmation = filters.confirmationNumber.trim();
55
+ const sanitizedConfirmation = sanitizeFilterValue(cleanConfirmation);
56
+ if (sanitizedConfirmation) {
57
+ queryParts.push(`confirmation_number:${sanitizedConfirmation}`);
58
+ }
59
+ }
60
+
61
+ return queryParts.length > 0 ? queryParts.join(' AND ') : undefined;
62
+ }
63
+
64
+ /**
65
+ * Parses order filter parameters from URLSearchParams
66
+ * @param searchParams - The URL search parameters
67
+ * @returns Parsed filter parameters
68
+ * @example
69
+ * const url = new URL('https://example.com/orders?name=1001&confirmation_number=ABC123');
70
+ * parseOrderFilters(url.searchParams) // returns \{ name: '1001', confirmationNumber: 'ABC123' \}
71
+ */
72
+ export function parseOrderFilters(
73
+ searchParams: URLSearchParams,
74
+ ): OrderFilterParams {
75
+ const filters: OrderFilterParams = {};
76
+
77
+ const name = searchParams.get(ORDER_FILTER_FIELDS.NAME);
78
+ if (name) {
79
+ filters.name = name;
80
+ }
81
+
82
+ const confirmationNumber = searchParams.get(
83
+ ORDER_FILTER_FIELDS.CONFIRMATION_NUMBER,
84
+ );
85
+ if (confirmationNumber) {
86
+ filters.confirmationNumber = confirmationNumber;
87
+ }
88
+
89
+ return filters;
90
+ }
@@ -0,0 +1,23 @@
1
+ import {redirect} from 'react-router';
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
+ }
@@ -0,0 +1,79 @@
1
+ import type {
2
+ PredictiveSearchQuery,
3
+ RegularSearchQuery,
4
+ } from 'storefrontapi.generated';
5
+
6
+ type ResultWithItems<Type extends 'predictive' | 'regular', Items> = {
7
+ type: Type;
8
+ term: string;
9
+ error?: string;
10
+ result: {total: number; items: Items};
11
+ };
12
+
13
+ export type RegularSearchReturn = ResultWithItems<
14
+ 'regular',
15
+ RegularSearchQuery
16
+ >;
17
+ export type PredictiveSearchReturn = ResultWithItems<
18
+ 'predictive',
19
+ NonNullable<PredictiveSearchQuery['predictiveSearch']>
20
+ >;
21
+
22
+ /**
23
+ * Returns the empty state of a predictive search result to reset the search state.
24
+ */
25
+ export function getEmptyPredictiveSearchResult(): PredictiveSearchReturn['result'] {
26
+ return {
27
+ total: 0,
28
+ items: {
29
+ articles: [],
30
+ collections: [],
31
+ products: [],
32
+ pages: [],
33
+ queries: [],
34
+ },
35
+ };
36
+ }
37
+
38
+ interface UrlWithTrackingParams {
39
+ /** The base URL to which the tracking parameters will be appended. */
40
+ baseUrl: string;
41
+ /** The trackingParams returned by the Storefront API. */
42
+ trackingParams?: string | null;
43
+ /** Any additional query parameters to be appended to the URL. */
44
+ params?: Record<string, string>;
45
+ /** The search term to be appended to the URL. */
46
+ term: string;
47
+ }
48
+
49
+ /**
50
+ * A utility function that appends tracking parameters to a URL. Tracking parameters are
51
+ * used internally by Shopify to enhance search results and admin dashboards.
52
+ * @example
53
+ * ```ts
54
+ * const baseUrl = 'www.example.com';
55
+ * const trackingParams = 'utm_source=shopify&utm_medium=shopify_app&utm_campaign=storefront';
56
+ * const params = { foo: 'bar' };
57
+ * const term = 'search term';
58
+ * const url = urlWithTrackingParams({ baseUrl, trackingParams, params, term });
59
+ * console.log(url);
60
+ * // Output: 'https://www.example.com?foo=bar&q=search%20term&utm_source=shopify&utm_medium=shopify_app&utm_campaign=storefront'
61
+ * ```
62
+ */
63
+ export function urlWithTrackingParams({
64
+ baseUrl,
65
+ trackingParams,
66
+ params: extraParams,
67
+ term,
68
+ }: UrlWithTrackingParams) {
69
+ let search = new URLSearchParams({
70
+ ...extraParams,
71
+ q: encodeURIComponent(term),
72
+ }).toString();
73
+
74
+ if (trackingParams) {
75
+ search = `${search}&${trackingParams}`;
76
+ }
77
+
78
+ return `${baseUrl}?${search}`;
79
+ }
@@ -0,0 +1,72 @@
1
+ import type {HydrogenSession} from '@shopify/hydrogen';
2
+ import {
3
+ createCookieSessionStorage,
4
+ type SessionStorage,
5
+ type Session,
6
+ } from 'react-router';
7
+
8
+ /**
9
+ * This is a custom session implementation for your Hydrogen shop.
10
+ * Feel free to customize it to your needs, add helper methods, or
11
+ * swap out the cookie-based implementation with something else!
12
+ */
13
+ export class AppSession implements HydrogenSession {
14
+ public isPending = false;
15
+
16
+ #sessionStorage;
17
+ #session;
18
+
19
+ constructor(sessionStorage: SessionStorage, session: Session) {
20
+ this.#sessionStorage = sessionStorage;
21
+ this.#session = session;
22
+ }
23
+
24
+ static async init(request: Request, secrets: string[]) {
25
+ const storage = createCookieSessionStorage({
26
+ cookie: {
27
+ name: 'session',
28
+ httpOnly: true,
29
+ path: '/',
30
+ sameSite: 'lax',
31
+ secrets,
32
+ },
33
+ });
34
+
35
+ const session = await storage
36
+ .getSession(request.headers.get('Cookie'))
37
+ .catch(() => storage.getSession());
38
+
39
+ return new this(storage, session);
40
+ }
41
+
42
+ get has() {
43
+ return this.#session.has;
44
+ }
45
+
46
+ get get() {
47
+ return this.#session.get;
48
+ }
49
+
50
+ get flash() {
51
+ return this.#session.flash;
52
+ }
53
+
54
+ get unset() {
55
+ this.isPending = true;
56
+ return this.#session.unset;
57
+ }
58
+
59
+ get set() {
60
+ this.isPending = true;
61
+ return this.#session.set;
62
+ }
63
+
64
+ destroy() {
65
+ return this.#sessionStorage.destroySession(this.#session);
66
+ }
67
+
68
+ commit() {
69
+ this.isPending = false;
70
+ return this.#sessionStorage.commitSession(this.#session);
71
+ }
72
+ }
@@ -0,0 +1,46 @@
1
+ import {useLocation} from 'react-router';
2
+ import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types';
3
+ import {useMemo} from 'react';
4
+
5
+ export function useVariantUrl(
6
+ handle: string,
7
+ selectedOptions?: SelectedOption[],
8
+ ) {
9
+ const {pathname} = useLocation();
10
+
11
+ return useMemo(() => {
12
+ return getVariantUrl({
13
+ handle,
14
+ pathname,
15
+ searchParams: new URLSearchParams(),
16
+ selectedOptions,
17
+ });
18
+ }, [handle, selectedOptions, pathname]);
19
+ }
20
+
21
+ export function getVariantUrl({
22
+ handle,
23
+ pathname,
24
+ searchParams,
25
+ selectedOptions,
26
+ }: {
27
+ handle: string;
28
+ pathname: string;
29
+ searchParams: URLSearchParams;
30
+ selectedOptions?: SelectedOption[];
31
+ }) {
32
+ const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname);
33
+ const isLocalePathname = match && match.length > 0;
34
+
35
+ const path = isLocalePathname
36
+ ? `${match![0]}products/${handle}`
37
+ : `/products/${handle}`;
38
+
39
+ selectedOptions?.forEach((option) => {
40
+ searchParams.set(option.name, option.value);
41
+ });
42
+
43
+ const searchString = searchParams.toString();
44
+
45
+ return path + (searchString ? '?' + searchParams.toString() : '');
46
+ }
@@ -0,0 +1,209 @@
1
+ import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
2
+ import {
3
+ Outlet,
4
+ useRouteError,
5
+ isRouteErrorResponse,
6
+ type ShouldRevalidateFunction,
7
+ Links,
8
+ Meta,
9
+ Scripts,
10
+ ScrollRestoration,
11
+ useRouteLoaderData,
12
+ } from 'react-router';
13
+ import type {Route} from './+types/root';
14
+ import favicon from '~/assets/favicon.svg';
15
+ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
16
+ import resetStyles from '~/styles/reset.css?url';
17
+ import tailwindStyles from '~/styles/tailwind.css?url';
18
+ import {PageLayout} from './components/PageLayout';
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') return true;
32
+
33
+ // revalidate when manually revalidating via useRevalidator
34
+ if (currentUrl.toString() === nextUrl.toString()) return true;
35
+
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
39
+ // line below to `return defaultShouldRevalidate` instead.
40
+ // For more details see: https://remix.run/docs/en/main/route/should-revalidate
41
+ return false;
42
+ };
43
+
44
+ /**
45
+ * The main and reset stylesheets are added in the Layout component
46
+ * to prevent a bug in development HMR updates.
47
+ *
48
+ * This avoids the "failed to execute 'insertBefore' on 'Node'" error
49
+ * that occurs after editing and navigating to another page.
50
+ *
51
+ * It's a temporary fix until the issue is resolved.
52
+ * https://github.com/remix-run/remix/issues/9242
53
+ */
54
+ export function links() {
55
+ return [
56
+ {
57
+ rel: 'preconnect',
58
+ href: 'https://cdn.shopify.com',
59
+ },
60
+ {
61
+ rel: 'preconnect',
62
+ href: 'https://shop.app',
63
+ },
64
+ {rel: 'icon', type: 'image/svg+xml', href: favicon},
65
+ ];
66
+ }
67
+
68
+ export async function loader(args: Route.LoaderArgs) {
69
+ // Start fetching non-critical data without blocking time to first byte
70
+ const deferredData = loadDeferredData(args);
71
+
72
+ // Await the critical data required to render initial state of the page
73
+ const criticalData = await loadCriticalData(args);
74
+
75
+ const {storefront, env} = args.context;
76
+
77
+ return {
78
+ ...deferredData,
79
+ ...criticalData,
80
+ publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
81
+ shop: getShopAnalytics({
82
+ storefront,
83
+ publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
84
+ }),
85
+ consent: {
86
+ checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
87
+ storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
88
+ withPrivacyBanner: false,
89
+ // localize the privacy banner
90
+ country: args.context.storefront.i18n.country,
91
+ language: args.context.storefront.i18n.language,
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Load data necessary for rendering content above the fold. This is the critical data
98
+ * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
99
+ */
100
+ async function loadCriticalData({context}: Route.LoaderArgs) {
101
+ const {storefront} = context;
102
+
103
+ const [header] = await Promise.all([
104
+ storefront.query(HEADER_QUERY, {
105
+ cache: storefront.CacheLong(),
106
+ variables: {
107
+ headerMenuHandle: 'main-menu', // Adjust to your header menu handle
108
+ },
109
+ }),
110
+ // Add other queries here, so that they are loaded in parallel
111
+ ]);
112
+
113
+ return {header};
114
+ }
115
+
116
+ /**
117
+ * Load data for rendering content below the fold. This data is deferred and will be
118
+ * fetched after the initial page load. If it's unavailable, the page should still 200.
119
+ * Make sure to not throw any errors here, as it will cause the page to 500.
120
+ */
121
+ function loadDeferredData({context}: Route.LoaderArgs) {
122
+ const {storefront, customerAccount, cart} = context;
123
+
124
+ // defer the footer query (below the fold)
125
+ const footer = storefront
126
+ .query(FOOTER_QUERY, {
127
+ cache: storefront.CacheLong(),
128
+ variables: {
129
+ footerMenuHandle: 'footer', // Adjust to your footer menu handle
130
+ },
131
+ })
132
+ .catch((error: Error) => {
133
+ // Log query errors, but don't throw them so the page can still render
134
+ console.error(error);
135
+ return null;
136
+ });
137
+ return {
138
+ cart: cart.get(),
139
+ isLoggedIn: customerAccount.isLoggedIn(),
140
+ footer,
141
+ };
142
+ }
143
+
144
+ export function Layout({children}: {children?: React.ReactNode}) {
145
+ const nonce = useNonce();
146
+
147
+ return (
148
+ <html lang="en">
149
+ <head>
150
+ <meta charSet="utf-8" />
151
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
152
+ <link rel="stylesheet" href={resetStyles}></link>
153
+ <link rel="stylesheet" href={tailwindStyles}></link>
154
+ <Meta />
155
+ <Links />
156
+ </head>
157
+ <body>
158
+ {children}
159
+ <ScrollRestoration nonce={nonce} />
160
+ <Scripts nonce={nonce} />
161
+ </body>
162
+ </html>
163
+ );
164
+ }
165
+
166
+ export default function App() {
167
+ const data = useRouteLoaderData<RootLoader>('root');
168
+
169
+ if (!data) {
170
+ return <Outlet />;
171
+ }
172
+
173
+ return (
174
+ <Analytics.Provider
175
+ cart={data.cart}
176
+ shop={data.shop}
177
+ consent={data.consent}
178
+ >
179
+ <PageLayout {...data}>
180
+ <Outlet />
181
+ </PageLayout>
182
+ </Analytics.Provider>
183
+ );
184
+ }
185
+
186
+ export function ErrorBoundary() {
187
+ const error = useRouteError();
188
+ let errorMessage = 'Unknown error';
189
+ let errorStatus = 500;
190
+
191
+ if (isRouteErrorResponse(error)) {
192
+ errorMessage = error?.data?.message ?? error.data;
193
+ errorStatus = error.status;
194
+ } else if (error instanceof Error) {
195
+ errorMessage = error.message;
196
+ }
197
+
198
+ return (
199
+ <div className="route-error">
200
+ <h1>Oops</h1>
201
+ <h2>{errorStatus}</h2>
202
+ {errorMessage && (
203
+ <fieldset>
204
+ <pre>{errorMessage}</pre>
205
+ </fieldset>
206
+ )}
207
+ </div>
208
+ );
209
+ }
@@ -0,0 +1,11 @@
1
+ import type {Route} from './+types/$';
2
+
3
+ export async function loader({request}: Route.LoaderArgs) {
4
+ throw new Response(`${new URL(request.url).pathname} not found`, {
5
+ status: 404,
6
+ });
7
+ }
8
+
9
+ export default function CatchAllPage() {
10
+ return null;
11
+ }
@@ -0,0 +1,117 @@
1
+ import type {Route} from './+types/[robots.txt]';
2
+ import {parseGid} from '@shopify/hydrogen';
3
+
4
+ export async function loader({request, context}: Route.LoaderArgs) {
5
+ const url = new URL(request.url);
6
+
7
+ const {shop} = await context.storefront.query(ROBOTS_QUERY);
8
+
9
+ const shopId = parseGid(shop.id).id;
10
+ const body = robotsTxtData({url: url.origin, shopId});
11
+
12
+ return new Response(body, {
13
+ status: 200,
14
+ headers: {
15
+ 'Content-Type': 'text/plain',
16
+
17
+ 'Cache-Control': `max-age=${60 * 60 * 24}`,
18
+ },
19
+ });
20
+ }
21
+
22
+ function robotsTxtData({url, shopId}: {shopId?: string; url?: string}) {
23
+ const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
24
+
25
+ return `
26
+ User-agent: *
27
+ ${generalDisallowRules({sitemapUrl, shopId})}
28
+
29
+ # Google adsbot ignores robots.txt unless specifically named!
30
+ User-agent: adsbot-google
31
+ Disallow: /checkouts/
32
+ Disallow: /checkout
33
+ Disallow: /carts
34
+ Disallow: /orders
35
+ ${shopId ? `Disallow: /${shopId}/checkouts` : ''}
36
+ ${shopId ? `Disallow: /${shopId}/orders` : ''}
37
+ Disallow: /*?*oseid=*
38
+ Disallow: /*preview_theme_id*
39
+ Disallow: /*preview_script_id*
40
+
41
+ User-agent: Nutch
42
+ Disallow: /
43
+
44
+ User-agent: AhrefsBot
45
+ Crawl-delay: 10
46
+ ${generalDisallowRules({sitemapUrl, shopId})}
47
+
48
+ User-agent: AhrefsSiteAudit
49
+ Crawl-delay: 10
50
+ ${generalDisallowRules({sitemapUrl, shopId})}
51
+
52
+ User-agent: MJ12bot
53
+ Crawl-Delay: 10
54
+
55
+ User-agent: Pinterest
56
+ Crawl-delay: 1
57
+ `.trim();
58
+ }
59
+
60
+ /**
61
+ * This function generates disallow rules that generally follow what Shopify's
62
+ * Online Store has as defaults for their robots.txt
63
+ */
64
+ function generalDisallowRules({
65
+ shopId,
66
+ sitemapUrl,
67
+ }: {
68
+ shopId?: string;
69
+ sitemapUrl?: string;
70
+ }) {
71
+ return `Disallow: /admin
72
+ Disallow: /cart
73
+ Disallow: /orders
74
+ Disallow: /checkouts/
75
+ Disallow: /checkout
76
+ ${shopId ? `Disallow: /${shopId}/checkouts` : ''}
77
+ ${shopId ? `Disallow: /${shopId}/orders` : ''}
78
+ Disallow: /carts
79
+ Disallow: /account
80
+ Disallow: /collections/*sort_by*
81
+ Disallow: /*/collections/*sort_by*
82
+ Disallow: /collections/*+*
83
+ Disallow: /collections/*%2B*
84
+ Disallow: /collections/*%2b*
85
+ Disallow: /*/collections/*+*
86
+ Disallow: /*/collections/*%2B*
87
+ Disallow: /*/collections/*%2b*
88
+ Disallow: */collections/*filter*&*filter*
89
+ Disallow: /blogs/*+*
90
+ Disallow: /blogs/*%2B*
91
+ Disallow: /blogs/*%2b*
92
+ Disallow: /*/blogs/*+*
93
+ Disallow: /*/blogs/*%2B*
94
+ Disallow: /*/blogs/*%2b*
95
+ Disallow: /*?*oseid=*
96
+ Disallow: /*preview_theme_id*
97
+ Disallow: /*preview_script_id*
98
+ Disallow: /policies/
99
+ Disallow: /*/*?*ls=*&ls=*
100
+ Disallow: /*/*?*ls%3D*%3Fls%3D*
101
+ Disallow: /*/*?*ls%3d*%3fls%3d*
102
+ Disallow: /search
103
+ Allow: /search/
104
+ Disallow: /search/?*
105
+ Disallow: /apple-app-site-association
106
+ Disallow: /.well-known/shopify/monorail
107
+ ${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''}`;
108
+ }
109
+
110
+ const ROBOTS_QUERY = `#graphql
111
+ query StoreRobots($country: CountryCode, $language: LanguageCode)
112
+ @inContext(country: $country, language: $language) {
113
+ shop {
114
+ id
115
+ }
116
+ }
117
+ ` as const;
@@ -0,0 +1,16 @@
1
+ import type {Route} from './+types/[sitemap.xml]';
2
+ import {getSitemapIndex} from '@shopify/hydrogen';
3
+
4
+ export async function loader({
5
+ request,
6
+ context: {storefront},
7
+ }: Route.LoaderArgs) {
8
+ const response = await getSitemapIndex({
9
+ storefront,
10
+ request,
11
+ });
12
+
13
+ response.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`);
14
+
15
+ return response;
16
+ }