Product Pages
Updated over a week ago

The Product page

๐Ÿ‘ Heads up

  • If you are using Starter Kit, there's no need need to manually create the Sections and Components below.

  • All the code examples below are just an implementation suggestions. For brevity, the styles are omitted.

  • For this guide, we will be creating Sections and Components locally. Make sure you have your Local Development Environment ready.

๐Ÿ‘‰ Remember to commit and push newly created Sections and Components to your remote repository to sync the changes with Shogun Frontend.

The Product page, commonly known as PDP, will display all relevant product-specific information. Customers might land on this page directly or via the homepage. This page will allow customers to select between one or more product variants, change quantity, and add a product to the cart.

Products template

The Product CMS group, imported and updated via BigCommerce or Shopify, will be the focus of this portion of the guide.

Unlike the homepage, there is no need to create a page for each product. Attaching the Product CMS group to a template will auto-generate a page for each content item within the group.

With that in mind, we will create our first template:

  1. Navigate to Templates by clicking the icon on the sidebar.

  2. Click ADD TEMPLATE.

  3. Name the template Products.

  4. Under USE THIS TEMPLATE FOR... select Products.

  5. Click SAVE TEMPLATE.

A CMS Group can only be linked to one template. In this example, there is already a Products template, and Products is grayed out in the screenshot above.

Saving the template will auto-generate the product pages. They will be blank until we create the necessary components and sections and add those to the template.

If you open any of these product pages youโ€™ll see a blank page, and we will address this next. First, mark some pages as Ready to Publish, so that the published store will have pages to navigate.

Creating the ProductBox section

We will now create a Section that will hold all the product details, named ProductBox.

sections/ProductBox/index.js

// ProductBox section
import * as React from 'react'
import Carousel from 'Components/Carousel'
import Select from 'Components/Select'
import NumberInput from 'Components/NumberInput'

const ProductBox = ({ product }) => {
const { name, variants, media, descriptionHtml = '' } = product || {}
// Local state to deal with product quantity that will added to the Cart
const [productQuantity, setProductQuantity] = React.useState(1)
// Local state to handle the product variant select
const [currentVariant, setCurrentVariant] = React.useState(variants && variants[0])
const { price, storefrontId } = currentVariant || {}

// Map through `media` array to create our `productMedia` array that will
// be passed to Carousel as prop
const productMedia =
media &&
media.map(({ details }) => ({
name: details.name,
src: details.src,
alt: details.alt,
width: details.width,
height: details.height,
}))

// Array that we will pass to our Select component to allow users
// select between product's variant
const variantOptions =
variants &&
variants.map(variant => ({
value: variant.storefrontId,
text: variant.name,
}))

const handleProductQuantity = (_, quantityAsNumber) => {
setProductQuantity(quantityAsNumber)
}

const handleVariantsSelect = event => {
if (!variants) return

// We want to find the variant that match with our local selected variant
const selectedVariant = variants.find(
variant => variant.storefrontId === event.currentTarget.value,
)

if (!selectedVariant) return
setCurrentVariant(selectedVariant)
}

return (
<div>
<div>
<Carousel media={productMedia} />
</div>
<section>
<h1>
{name}
</h1>
<strong>
${price}
</strong>
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
<hr />
<div>
<label>
Variants
<Select options={variantOptions} onChange={handleVariantsSelect} />
</label>
</div>
<div}>
<label>
Quantity
<NumberInput
inputProps={{ 'aria-label': 'Product quantity' }}
/>
</label>
</div>
<div>
<button>Add to Cart</button>
</div>
</section>
</div>
)
}

export default ProductBox

Next, we will create a variable matching the prop that the section will receive.

  1. Set the TYPE as Reference

  2. Under CONTENT TYPE select Products.

  3. Under variants select externalId, storefrontId, name and price.

  4. Select details under media and finally descriptionHtml.

Navigate back to the Product template, and we will add the ProductBox section to our template.

  1. Click on ProductBox in the sidebar and connect the variable PRODUCT to Current Products. This ensures that each product page will pull in dynamic content from the Product CMS Group.

  2. Save the template.

Each product page will now display product-specific details in the ProductBox section. However, if you try to add the given product to the cart, nothing will happen. We will next connect everything with the frontend-checkout package.

Creating the AddToCartButton

We need to create a new component to handle the add to cart action: create a new component called AddToCartButton or a name of your preference.

This component will handle the logic of adding a product to the cart. In the example code below, we take into account the product's inventory level and trigger the CartDrawer to open. We will cover that in the next step.

components/AddToCartButton/index.js

// ProductBox section
import * as React from 'react'
import Carousel from 'Components/Carousel'
import Select from 'Components/Select'
import NumberInput from 'Components/NumberInput'

const ProductBox = ({ product }) => {
const { name, variants, media, descriptionHtml = '' } = product || {}
// Local state to deal with product quantity that will added to the Cart
const [productQuantity, setProductQuantity] = React.useState(1)
// Local state to handle the product variant select
const [currentVariant, setCurrentVariant] = React.useState(variants && variants[0])
const { price, storefrontId } = currentVariant || {}

// Map through `media` array to create our `productMedia` array that will
// be passed to Carousel as prop
const productMedia =
media &&
media.map(({ details }) => ({
name: details.name,
src: details.src,
alt: details.alt,
width: details.width,
height: details.height,
}))

// Array that we will pass to our Select component to allow users
// select between product's variant
const variantOptions =
variants &&
variants.map(variant => ({
value: variant.storefrontId,
text: variant.name,
}))

const handleProductQuantity = (_, quantityAsNumber) => {
setProductQuantity(quantityAsNumber)
}

const handleVariantsSelect = event => {
if (!variants) return

// We want to find the variant that match with our local selected variant
const selectedVariant = variants.find(
variant => variant.storefrontId === event.currentTarget.value,
)

if (!selectedVariant) return
setCurrentVariant(selectedVariant)
}

return (
<div>
<div>
<Carousel media={productMedia} />
</div>
<section>
<h1>
{name}
</h1>
<strong>
${price}
</strong>
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
<hr />
<div>
<label>
Variants
<Select options={variantOptions} onChange={handleVariantsSelect} />
</label>
</div>
<div}>
<label>
Quantity
<NumberInput
inputProps={{ 'aria-label': 'Product quantity' }}
/>
</label>
</div>
<div>
<button>Add to Cart</button>
</div>
</section>
</div>
)
}

export default ProductBox

With the AddToCartButton ready, we will add it to the ProductBox:

sections/ProductBox/index.js

// ProductBox section
import * as React from 'react'
import Carousel from 'Components/Carousel'
import Select from 'Components/Select'
import NumberInput from 'Components/NumberInput'
import AddToCartButton from 'Components/AddToCartButton'

const ProductBox = ({ product }) => {
const { name, variants, media, descriptionHtml = '' } = product || {}
// Local state to deal with product quantity that will added to the Cart
const [productQuantity, setProductQuantity] = React.useState(1)
// Local state to handle the product variant select
const [currentVariant, setCurrentVariant] = React.useState(variants && variants[0])
const { price, storefrontId } = currentVariant || {}

// Map through `media` array to create our `productMedia` array that will
// be passed to Carousel as prop
const productMedia =
media &&
media.map(({ details }) => ({
name: details.name,
src: details.src,
alt: details.alt,
width: details.width,
height: details.height,
}))

// Array that we will pass to our Select component to allow users
// select between product's variant
const variantOptions =
variants &&
variants.map(variant => ({
value: variant.storefrontId,
text: variant.name,
}))

const handleProductQuantity = (_, quantityAsNumber) => {
setProductQuantity(quantityAsNumber)
}

const handleVariantsSelect = event => {
if (!variants) return

// We want to find the variant that match with our local selected variant
const selectedVariant = variants.find(
variant => variant.storefrontId === event.currentTarget.value,
)

if (!selectedVariant) return
setCurrentVariant(selectedVariant)
}

return (
<div>
<div>
<Carousel media={productMedia} />
</div>
<section>
<h1>
{name}
</h1>
<strong>
${price}
</strong>
<div dangerouslySetInnerHTML={{ __html: descriptionHtml }} />
<hr />
<div>
<label>
Variants
<Select options={variantOptions} onChange={handleVariantsSelect} />
</label>
</div>
<div}>
<label>
Quantity
<NumberInput
inputProps={{ 'aria-label': 'Product quantity' }}
/>
</label>
</div>
<div>
<AddToCartButton productId={storefrontId} quantity={productQuantity} />
</div>
</section>
</div>
)
}

export default ProductBox

๐Ÿšง If your Shopify store is using the REST API instead of GraphQL, make sure to use externalId instead of storefrontId. If you are using a BigCommerce store, make sure to use the id from the product.

Did this answer your question?