@okendo/shopify-hydrogen
v2.2.7
Published
Okendo React components for Shopify Hydrogen 2 (Remix)
Downloads
1,033
Maintainers
Keywords
Readme
Note: this package is to be used on stores built with Hydrogen v2, based on Remix. If your store is built with the deprecated Hydrogen v1, please use the version 1 of this package.
Okendo Hydrogen 2 (Remix) React Components
This package brings Okendo's review widgets to a Shopify Hydrogen store.
Requirements
- A Shopify store with the Okendo: Product Reviews & UCG app installed and configured.
- For existing merchants, your store must be upgraded to Okendo's Widget Plus widgets. It is free to upgrade. For more information please contact Okendo Support.
- A current Okendo subscription.
- A Shopify Hydrogen app.
Demo Store
Our demo store, which is based on the demo store provided by Shopify, can be found here.
Note: there have been multiple versions of Shopify's Hydrogen demo store. If your project is based on an old version of it, consult the history of our demo store's repository.
Exposition of Shopify Metafields
Okendo Reviews use Product and Shop metafields. You will need to expose these metafields so that they can be retrieved by your Hydrogen app.
At the moment, Shopify does not have a way of exposing Shop Metafields through their admin UI, so the preferred method is to contact Okendo's Support.
Exposing Metafields via GraphQL
You will need a Storefront access token with the following API access scopes:
unauthenticated_read_content
unauthenticated_read_customers
unauthenticated_read_product_listings
unauthenticated_read_product_inventory
unauthenticated_read_product_pickup_locations
unauthenticated_read_product_tags
Follow the instructions on this page to create it.
Using Curl
Open a new terminal or PowerShell window, then:
- Run the following command to expose the
widget_pre_render_style_tags
shop metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "WidgetPreRenderStyleTags"
namespace: "$app:review"
key: "widget_pre_render_style_tags"
type: "multi_line_text_field"
ownerType: SHOP
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
- Run the following command to expose the
widget_pre_render_body_style_tags
shop metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "WidgetPreRenderBodyStyleTags"
namespace: "$app:review"
key: "widget_pre_render_body_style_tags"
type: "multi_line_text_field"
ownerType: SHOP
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
- Run the following command to expose the
reviews_widget_snippet
product metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "ReviewsWidgetSnippet"
namespace: "$app:reviews"
key: "reviews_widget_snippet"
type: "multi_line_text_field"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
- Run the following command to expose the
star_rating_snippet
product metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "StarRatingSnippet"
namespace: "$app:reviews"
key: "star_rating_snippet"
type: "multi_line_text_field"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
- Run the following command to expose the
review_count
product metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "ReviewCount"
namespace: "$app:reviews"
key: "review_count"
type: "number_integer"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
- Run the following command to expose the
average_rating
product metafield:
curl -X POST \
https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
-H 'Content-Type: application/graphql' \
-H 'X-Shopify-Access-Token: {access_token}' \
-d '
mutation {
metafieldDefinitionCreate(
definition: {
name: "AverageRating"
namespace: "$app:reviews"
key: "average_rating"
type: "rating"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition { id name }
userErrors { field message code }
}
}
'
Using GraphQL IDE
Open your GraphQL IDE (such as Postman) and make POST
requests with the following details:
- URL: https://{shop}.myshopify.com/admin/api/2024-10/graphql.json
- Headers: - X-Shopify-Access-Token: {access_token} - Content-Type: application/json
- Execute the following request to expose the
widget_pre_render_style_tags
shop metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "WidgetPreRenderStyleTags"
namespace: "$app:reviews"
key: "widget_pre_render_style_tags"
type: "multi_line_text_field"
ownerType: SHOP
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
- Execute the following request to expose the
widget_pre_render_body_style_tags
shop metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "WidgetPreRenderBodyStyleTags"
namespace: "$app:reviews"
key: "widget_pre_render_body_style_tags"
type: "multi_line_text_field"
ownerType: SHOP
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
- Execute the following request to expose the
reviews_widget_snippet
product metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "ReviewsWidgetSnippet"
namespace: "$app:reviews"
key: "reviews_widget_snippet"
type: "multi_line_text_field"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
- Execute the following request to expose the
star_rating_snippet
product metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "StarRatingSnippet"
namespace: "$app:reviews"
key: "star_rating_snippet"
type: "multi_line_text_field"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
- Execute the following request to expose the
review_count
product metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "ReviewCount"
namespace: "$app:reviews"
key: "review_count"
type: "number_integer"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
- Execute the following request to expose the
average_rating
product metafield:
mutation {
metafieldDefinitionCreate(
definition: {
name: "AverageRating"
namespace: "$app:reviews"
key: "average_rating"
type: "rating"
ownerType: PRODUCT
access: {
admin: PUBLIC_READ
storefront: PUBLIC_READ
}
}
) {
createdDefinition {
id
name
}
userErrors {
field
message
code
}
}
}
References
- https://shopify.dev/api/examples/metafields#step-1-expose-metafields
- https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
Installation
This package provides:
- one function:
getOkendoProviderData
, - one provider:
OkendoProvider
, - two React components:
OkendoStarRating
andOkendoReviews
.
The function getOkendoProviderData
needs to be called in the loader
function of root.tsx
in the Hydrogen 2 store. The data is then passed to OkendoProvider
, which is added to your website's body
and wraps everything in it.
Important:
OkendoProvider
supports two ways of loading the data returned bygetOkendoProviderData
: either as a promise, or as the data itself. Its behaviour is different between the two ways — this is explained below.
Then, the components OkendoStarRating
and OkendoReviews
can be added on the store pages. There are a few more bits of configuration to do, please see below.
The code examples provided in this section are based on the Shopify template store created by running
npm create @shopify/hydrogen@latest
(see Shopify's documentation). You will find the following steps already done in our demo store.
Run:
npm i @okendo/shopify-hydrogen
app/root.tsx
OkendoProvider
supports two ways of loading the data returned by getOkendoProviderData
:
- as a promise: in this case, the query getting the data is deferred, which allows your page to load as quickly as it does without Okendo's widgets. When the data is ready, it is sent to the browser, and Okendo's widgets are rendered. Blank placeholders are shown until the widgets are rendered. You can customise these placeholders to show loading spinners or skeletons that fit well with your store's theme.
- as the data: in this case, the query getting the data needs to complete before your page loads, which can add a couple hundreds milliseconds of loading time. Widgets are then rendered server-side, and so appear as soon as your page loads.
To summarise the differences between the two behaviours:
Pass the promise to
OkendoProvider
— so don't useawait
withgetOkendoProviderData
:- The page loading time won't be increased at all.
- The widgets will be rendered client-side.
- Placeholders (which are customisable) are shown until the widgets are rendered.
Pass the data to
OkendoProvider
— so useawait
withgetOkendoProviderData
:- The page loading time can be increased by a couple hundreds milliseconds.
- The widgets will be rendered server-side.
- The widgets are shown as soon as the page loads — no placeholders needed.
You can easily experiment with the two ways, and decide which is the one you'd like to keep for your store.
Open app/root.tsx
and add the following import:
import {
OkendoProvider,
getOkendoProviderData,
} from "@okendo/shopify-hydrogen";
Locate the loader
function, append okendoProviderData
to the returned data as shown below, and set subscriberId
to your Okendo subscriber ID.
As explained above, set okendoProviderData
to either getOkendoProviderData(...)
, or await getOkendoProviderData(...)
:
return defer(
{
...
okendoProviderData: /* place `await` here if you want server-rendered widgets */ getOkendoProviderData({
context,
subscriberId: "<your-okendo-subscriber-id>",
}),
},
);
Locate the App
function, add the meta
tag oke:subscriber_id
to head
, and place your Okendo subscriber ID in its content:
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
...
Append OkendoProvider
to body
, and pass it the promise — or the data — returned by getOkendoProviderData
. If Content Security Policy is active in your project, you also need to provide the nonce
(available with const nonce = useNonce()
in Shopify's Hydrogen demo store):
...
<body>
<OkendoProvider
nonce={nonce}
okendoProviderData={data.okendoProviderData}
>
...
</OkendoProvider>
</body>
...
app/entry.server.tsx
This is only necessary if Content Security Policy is active in your project.
Locate the call to createContentSecurityPolicy
, and ensure your configuration includes the entries below:
Note that it's necessary to to add the default values ('self'
, etc.) when extending the CSP. The call to createContentSecurityPolicy
should now look like the following:
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
defaultSrc: [
"'self'",
"localhost:*",
"https://cdn.shopify.com",
"https://www.google.com",
"https://www.gstatic.com",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://d3g5hqndtiniji.cloudfront.net",
"https://dov7r31oq5dkj.cloudfront.net",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io",
"https://api.okendo.io",
"data:",
],
imgSrc: [
"'self'",
"https://cdn.shopify.com",
"data:",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://d3g5hqndtiniji.cloudfront.net",
"https://dov7r31oq5dkj.cloudfront.net",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io"
],
mediaSrc: [
"'self'",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://d3g5hqndtiniji.cloudfront.net",
"https://dov7r31oq5dkj.cloudfront.net",
"https://cdn-static.okendo.io"
],
styleSrcElem: [
"'self'",
"'unsafe-inline'",
"https://cdn.shopify.com",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io"
],
scriptSrc: [
"'self'",
"https://cdn.shopify.com",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://dov7r31oq5dkj.cloudfront.net",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io",
"https://api.okendo.io",
"https://www.google.com",
"https://www.gstatic.com"
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com",
"https://d3hw6dc1ow8pp2.cloudfront.net",
"https://dov7r31oq5dkj.cloudfront.net",
"https://cdn.shopify.com",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io"
],
connectSrc: [
"'self'",
"https://monorail-edge.shopifysvc.com",
"localhost:*",
"ws://localhost:*",
"ws://127.0.0.1:*",
"https://api.okendo.io",
"https://cdn-static.okendo.io",
"https://surveys.okendo.io",
"https://api.raygun.com",
"https://www.google.com",
"https://www.gstatic.com",
],
frameSrc: [
"https://www.google.com",
"https://www.gstatic.com"
]
});
app/routes/_index.tsx
Add the following imports:
import {
OkendoStarRating,
type WithOkendoStarRatingSnippet,
} from "@okendo/shopify-hydrogen";
Add the following block just before the RECOMMENDED_PRODUCTS_QUERY
GraphQL query:
const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
fragment OkendoStarRatingSnippet on Product {
okendoStarRatingSnippet: metafield(
namespace: "okendo"
key: "StarRatingSnippet"
) {
value
}
}
` as const;
Then append ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
and ...OkendoStarRatingSnippet
to RECOMMENDED_PRODUCTS_QUERY
:
const RECOMMENDED_PRODUCTS_QUERY = `#graphql
${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
fragment RecommendedProduct on Product {
id
title
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
nodes {
id
url
altText
width
height
}
}
...OkendoStarRatingSnippet
}
query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
products(first: 4, sortKey: UPDATED_AT, reverse: true) {
nodes {
...RecommendedProduct
}
}
}
` as const;
Tweak the type of the products
prop of RecommendedProducts
:
products: Promise<{
products: {
nodes: (RecommendedProductsQuery["products"]["nodes"][0] &
WithOkendoStarRatingSnippet)[];
};
}>;
Add OkendoStarRating
to RecommendedProducts
:
<OkendoStarRating
productId={product.id}
okendoStarRatingSnippet={product.okendoStarRatingSnippet}
/>
For instance, we can add it below the product title, like this:
<Image
data={product.images.nodes[0]}
aspectRatio="1/1"
sizes="(min-width: 45em) 20vw, 50vw"
/>
<h4>{product.title}</h4>
<OkendoStarRating
productId={product.id}
okendoStarRatingSnippet={product.okendoStarRatingSnippet}
/>
<small>
<Money data={product.priceRange.minVariantPrice} />
</small>
Note: if the widgets are rendered client-side (if you don't use
await
when callinggetOkendoProviderData
), you can provide your own placeholder by using theplaceholder
property ofOkendoStarRating
.
We now have the Okendo Star Rating widget visible on our page:
app/routes/products.$handle.tsx
Add the following imports:
import {
OKENDO_PRODUCT_REVIEWS_FRAGMENT,
OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
OkendoReviews,
OkendoStarRating,
type WithOkendoReviewsSnippet,
type WithOkendoStarRatingSnippet,
} from "@okendo/shopify-hydrogen";
Add the following block just before the RECOMMENDED_PRODUCTS_QUERY
GraphQL query:
const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
fragment OkendoStarRatingSnippet on Product {
okendoStarRatingSnippet: metafield(
namespace: "okendo"
key: "StarRatingSnippet"
) {
value
}
}
` as const;
const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
fragment OkendoReviewsSnippet on Product {
okendoReviewsSnippet: metafield(
namespace: "okendo"
key: "ReviewsWidgetSnippet"
) {
value
}
}
` as const;
Then append ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
, ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
, ...OkendoStarRatingSnippet
, and ...OkendoReviewsSnippet
to PRODUCT_FRAGMENT
:
const PRODUCT_FRAGMENT = `#graphql
${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
fragment Product on Product {
id
title
vendor
handle
descriptionHtml
description
options {
name
values
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
...ProductVariant
}
variants(first: 1) {
nodes {
...ProductVariant
}
}
seo {
description
title
}
...OkendoStarRatingSnippet
...OkendoReviewsSnippet
}
${PRODUCT_VARIANT_FRAGMENT}
` as const;
Add OkendoReviews
to Product
:
<OkendoReviews
productId={product.id}
okendoReviewsSnippet={product.okendoReviewsSnippet}
/>
For instance, we can add it below the product section, like this:
<>
<div className="product">
<ProductImage image={selectedVariant?.image} />
<ProductMain
selectedVariant={selectedVariant}
product={product}
variants={variants}
/>
</div>
<OkendoReviews
productId={product.id}
okendoReviewsSnippet={product.okendoReviewsSnippet}
/>
</>
Note: if the widgets are rendered client-side (if you don't use
await
when callinggetOkendoProviderData
), you can provide your own placeholder by using theplaceholder
property ofOkendoReviews
.
Tweak the type of the product
prop of ProductMain
:
product: ProductFragment &
WithOkendoStarRatingSnippet &
WithOkendoReviewsSnippet;
Add OkendoStarRating
to ProductMain
:
<OkendoStarRating
productId={product.id}
okendoStarRatingSnippet={product.okendoStarRatingSnippet}
/>
For instance, we can add it below the product title, like this:
<div className="product-main">
<h1>{title}</h1>
<OkendoStarRating
productId={product.id}
okendoStarRatingSnippet={product.okendoStarRatingSnippet}
/>
<ProductPrice selectedVariant={selectedVariant} />
We now have the Okendo Star Rating and Reviews widgets visible on our product page:
All Reviews Widget - Client Side Only
If you would like to include a copy of the Okendo Reviews Widget which displays all reviews for a given store (to be used on a reviews page for example), please add the OkendoReviewsWidget without supplying the productId
.
Please note the all reviews widget loads on the client not the server.
import { type MetaFunction } from '@remix-run/react';
import { OkendoReviews } from '@okendo/shopify-hydrogen';
export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Okendo All Reviews`}];
};
export default function ReviewsPage() {
return (
<div className="all-reviews">
<h1>All Reviews Widget</h1>
<OkendoReviews />
</div>
);
}