-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Luis Mas Serral
committed
May 11, 2023
1 parent
c039ba7
commit 62729db
Showing
2 changed files
with
266 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# These variables are only available locally in MiniOxygen | ||
# More info about this vars here: https://shopify.dev/docs/custom-storefronts/hydrogen/environment-variables | ||
|
||
SESSION_SECRET="foobar" | ||
PUBLIC_STOREFRONT_API_TOKEN="your-store-token" | ||
PUBLIC_STOREFRONT_API_VERSION="2023-01" | ||
PUBLIC_STORE_DOMAIN="your-store.myshopify.com" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,276 @@ | ||
# Hydrogen template: Hello World | ||
# Crossing Minds Hydrogen Demo Store | ||
|
||
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen. | ||
This repository contains a demo store using [Shopify's Hydrogen](https://hydrogen.shopify.dev/) together with our recommendation client [Beam React](https://www.npmjs.com/package/@crossingminds/beam-react). | ||
Hydrogen is Shopify’s stack for headless commerce, designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. | ||
|
||
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen) | ||
[Get familiar with Remix](https://remix.run/docs/en/v1) | ||
## Installation | ||
|
||
## What's included | ||
**Requirements:** | ||
|
||
- Remix | ||
- Hydrogen | ||
- Oxygen | ||
- Shopify CLI | ||
- ESLint | ||
- Prettier | ||
- GraphQL generator | ||
- TypeScript and JavaScript flavors | ||
- Minimal setup of components and routes | ||
- Node.js version 16.14.0 or higher | ||
- [pnpm](https://pnpm.io/) (package manager) | ||
|
||
## Getting started | ||
### _Getting started_ | ||
|
||
**Requirements:** | ||
In order to get the project up and running, you need to configure Shopify, configure Beam and then run `pnpm` to have the app running locally. | ||
|
||
- Node.js version 16.14.0 or higher | ||
### _Shopify configuration_ | ||
|
||
Copy `.env.example` to `.env` and update the values with the ones for you store. If you need more information about those values, you can check the documentation [here](https://shopify.dev/docs/custom-storefronts/hydrogen/environment-variables). | ||
|
||
- _PUBLIC_STOREFRONT_API_TOKEN_: The public access token for the Storefront API. | ||
- _PUBLIC_STOREFRONT_API_VERSION_: The Storefront API version. Defaults to the version of the Storefront API used by Hydrogen. | ||
- _PUBLIC_STORE_DOMAIN_: The domain of the store used to communicate with the Storefront API. | ||
- SESSION_SECRET: A secret used to sign session cookies. Refer to the Remix documentation on using sessions for more information. | ||
|
||
### _Beam configuration_ | ||
|
||
To configure Beam, you need to set 3 parameters in `app/beam/config.tsx`: | ||
|
||
- _databaseId_: database identifier in the same organization as your service account. | ||
- _password_: password for the service account | ||
- _serviceLoginId_: service account with a frontend role | ||
|
||
### _Run project_ | ||
|
||
Once the configuration is completed, you can run project locally with: | ||
|
||
```bash | ||
npm create @shopify/hydrogen@latest -- --template hello-world | ||
pnpm install | ||
pnpm dev | ||
``` | ||
|
||
Remember to update `.env` with your shop's domain and Storefront API token! | ||
## Implementing recommendations | ||
|
||
## Building for production | ||
### _Description_ | ||
|
||
```bash | ||
npm run build | ||
In a Hydrogen project, since Remix is used, we can render both on the server and on the client. Everything that we want to render on the server, we must do it using the _loader_ function of each of the routes, and everything that we want to render on the client, we can do it using the _action_ function and fetcher. | ||
|
||
In the case of recommendations, the decision of where to render will be determined by where the recommendations are obtained. | ||
|
||
### _Server side recommendations_ | ||
|
||
Whenever possible, it's best to render on the server, so recommendations will need to be fetched on the server. Here is an example of a route that gets recommendations and renders them to the server: | ||
|
||
```typescript | ||
export const loader = async ({context, request}: LoaderArgs) => { | ||
const {sessionId} = await getSessionAndSessionId(request); | ||
|
||
const {itemIds: variantIdsForOurFavorites} = | ||
await getPersonalizedRecommendations({ | ||
...BEAM_REACT_OPTIONS, | ||
sessionId, | ||
sessionScenario: 'some scenario id', | ||
maxResults: 8, | ||
}); | ||
|
||
const {nodes: productVariants} = await context.storefront.query<Promise<any>>( | ||
PRODUCTS_BY_VARIANT_QUERY, | ||
{ | ||
variables: variantIdsForOurFavorites.map( | ||
(variantId) => `gid://shopify/ProductVariant/${variantId}`, | ||
), | ||
}, | ||
); | ||
|
||
return json({productVariants}); | ||
}; | ||
|
||
export const shouldRevalidate = () => false; | ||
|
||
export default function Index() { | ||
const {productVariants} = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<div> | ||
<Recommendations productVariants={productVariants} /> | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
## Local development | ||
The first step is to obtain the IDs of the recommendations (they can be products or, as in the case of the home page of the same demo store, collections): | ||
|
||
```bash | ||
npm run dev | ||
```typescript | ||
const {itemIds: variantIdsForOurFavorites} = | ||
await getPersonalizedRecommendations({ | ||
...BEAM_REACT_OPTIONS, | ||
sessionId, | ||
sessionScenario: 'some scenario id', | ||
maxResults: 8, | ||
}); | ||
``` | ||
|
||
With that list of IDs (in the example product variants), we can use Shopify's GraphQL API to get the product variants: | ||
|
||
```typescript | ||
const {nodes: productVariants} = await context.storefront.query<Promise<any>>( | ||
PRODUCTS_BY_VARIANT_QUERY, | ||
{ | ||
variables: variantIdsForOurFavorites.map( | ||
(variantId) => `gid://shopify/ProductVariant/${variantId}`, | ||
), | ||
}, | ||
); | ||
``` | ||
|
||
Once obtained, we will pass them by returning them in the _loader_ function: | ||
|
||
```typescript | ||
return json({productVariants}); | ||
``` | ||
|
||
And now, the React component will be able to obtain and paint them or pass them as a parameter to other components: | ||
|
||
```typescript | ||
const {productVariants} = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<div> | ||
<Recommendations productVariants={productVariants} /> | ||
</div> | ||
); | ||
``` | ||
|
||
It is important to understand this line of code that is indicated in the route. When using fetcher, Remix by default fetches a parallel version of the same page we are on, and replaces it with the current one, resulting in a re-render. To avoid this, in those routes that we do not want to have said automatic update, we can indicate that it should not revalidate. | ||
|
||
```typescript | ||
export const shouldRevalidate = () => false; | ||
``` | ||
|
||
### _Client side recommendations_ | ||
|
||
There may be cases where we need client data to customize recommendations, so we won't be able to render them directly on the server. This is the example of the recommendations that appear in the cart drawer of the demo store. These recommendations are customized according to what products are in the cart, so we cannot obtain them like any other route, since there is no complete navigation. | ||
|
||
In these cases, the correct thing to do is to use a specific route that will serve as an "endpoint" to obtain the recommendations and their associated products. This is where the _action_ function of the route and the use of fetcher come into play. | ||
|
||
```typescript | ||
import {type LoaderArgs, json, redirect} from '@shopify/remix-oxygen'; | ||
|
||
import {PRODUCTS_BY_VARIANT_QUERY} from '~/queries/product'; | ||
import {SHOPIFY_ENTITY_TYPES, getShopifyEntityIdFromId} from '~/utils/shopify'; | ||
|
||
export async function action({request, context}: LoaderArgs) { | ||
const [formData] = await Promise.all([request.formData()]); | ||
let itemIds: string[] = []; | ||
|
||
try { | ||
itemIds = JSON.parse((formData.get('itemIds') as string) || '') as string[]; | ||
} catch { | ||
return json({nodes: []}); | ||
} | ||
|
||
const {nodes} = await context.storefront.query<Promise<any>>( | ||
PRODUCTS_BY_VARIANT_QUERY, | ||
{ | ||
variables: { | ||
ids: (itemIds as string[]).map((itemId) => | ||
getShopifyEntityIdFromId( | ||
SHOPIFY_ENTITY_TYPES.PRODUCT_VARIANT, | ||
itemId, | ||
), | ||
), | ||
}, | ||
}, | ||
); | ||
|
||
return json(nodes); | ||
} | ||
|
||
export const loader = async () => { | ||
return redirect('/'); | ||
}; | ||
|
||
export default function CartRecommendations() { | ||
return <></>; | ||
} | ||
``` | ||
|
||
The first thing is to have a route with the _action_ function implemented. That function will receive the parameters that we pass to it in the request body. | ||
|
||
```typescript | ||
const [formData] = await Promise.all([request.formData()]); | ||
let itemIds: string[] = []; | ||
|
||
try { | ||
itemIds = JSON.parse((formData.get('itemIds') as string) || '') as string[]; | ||
} catch { | ||
return json({nodes: []}); | ||
} | ||
``` | ||
|
||
Now we can use those IDs to obtain the product variants using the GraphQL API from Shopify. | ||
|
||
```typescript | ||
const {nodes} = await context.storefront.query<Promise<any>>( | ||
PRODUCTS_BY_VARIANT_QUERY, | ||
{ | ||
variables: { | ||
ids: (itemIds as string[]).map((itemId) => | ||
getShopifyEntityIdFromId(SHOPIFY_ENTITY_TYPES.PRODUCT_VARIANT, itemId), | ||
), | ||
}, | ||
}, | ||
); | ||
``` | ||
|
||
Finally, a JSON is returned in the same way with all the data that our components need. | ||
|
||
```typescript | ||
return json(nodes); | ||
``` | ||
|
||
Since we've created a new route, that means our route could be accessible via a URL like any other page. To avoid this, in addition to not returning anything in the main function (safety belt), the important thing is to make a redirect to where we consider within the _loader_ function. In the case of the example, a redirection is made to the home page. | ||
|
||
```typescript | ||
export const loader = async () => { | ||
return redirect('/'); | ||
}; | ||
|
||
export default function CartRecommendations() { | ||
return <></>; | ||
} | ||
``` | ||
|
||
Now that we have our "endpoint", the way to call it is using fetcher. The component that is responsible for getting the recommendations will make a call to the route we have created by submitting a form. | ||
|
||
```typescript | ||
const contextItems = cart.lines.edges.map((lineEdge) => { | ||
return { | ||
itemId: getIdFromShopifyEntityId( | ||
SHOPIFY_ENTITY_TYPES.PRODUCT_VARIANT, | ||
lineEdge.node.merchandise.id, | ||
), | ||
}; | ||
}); | ||
|
||
const {itemIds} = await getPersonalizedRecommendations({ | ||
...BEAM_REACT_OPTIONS, | ||
sessionId, | ||
contextItems, | ||
sessionWithContextScenario: | ||
RECOMMENDATION_SCENARIOS.CART_FREQUENTLY_PURCHASED_TOGETHER, | ||
maxResults: 8 + contextItems.length, | ||
}); | ||
|
||
fetcher.submit( | ||
{itemIds: JSON.stringify(itemIds)}, | ||
{method: 'post', action: '/cartRecommendations'}, | ||
); | ||
``` | ||
|
||
That call will be captured by fetcher, processed and made available to our component using fetcher.data (a complete example of this can be found in `app/hooks/useCartRecommendations.tsx`). | ||
|
||
```typescript | ||
useEffect(() => { | ||
if (fetcher.state === 'idle' && fetcher.data) { | ||
setCartRecommendations(removeRepeatedCartRecommendations(fetcher.data)); | ||
setCartRecommendationsLoading(false); | ||
setCartRecommendationsError(false); | ||
} | ||
}, [fetcher]); | ||
``` | ||
|
||
### _Summary_ | ||
|
||
As we have seen, despite the fact that rendering can be done both on the server and on the client, obtaining the data from the Shopify GraphQL API is always done on the server. |