npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@okendo/shopify-hydrogen

v2.2.7

Published

Okendo React components for Shopify Hydrogen 2 (Remix)

Downloads

1,033

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

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:

  1. 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 }
	}
}
'
  1. 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 }
	}
}
'
  1. 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 }
	}
}
'
  1. 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 }
	}
}
'
  1. 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 }
	}
}
'
  1. 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
  1. 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
		}
	}
}
  1. 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
		}
	}
}
  1. 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
		}
	}
}
  1. 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
		}
	}
}
  1. 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
		}
	}
}
  1. 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

Installation

This package provides:

  • one function: getOkendoProviderData,
  • one provider: OkendoProvider,
  • two React components: OkendoStarRating and OkendoReviews.

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 by getOkendoProviderData: 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 use await with getOkendoProviderData:

    • 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 use await with getOkendoProviderData:

    • 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 calling getOkendoProviderData), you can provide your own placeholder by using the placeholder property of OkendoStarRating.

We now have the Okendo Star Rating widget visible on our page:

Okendo's Star Rating widget

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 calling getOkendoProviderData), you can provide your own placeholder by using the placeholder property of OkendoReviews.

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:

Okendo's Star Rating and Reviews widgets

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>
  );
}