Product

This covers implementation and customization of the Product (PDP) page.

Page Component

pages/p/[productId].js

Data Model

The product page expects the API to return data in the following structure. You can also add any additional data needed for custom UI elements.

{
  "pageData": {
    "title": "Page Title",                                // the document title
    "product": {                                          // info for the product.  You can add to this as needed.
      "id": "1",                                          // the product id
      "url": "/p/1",                                      // the URL for the product page
      "name": "Product 1",                                // the name of the product
      "price": 10.99,                                     // the price as a number
      "priceText": "$10.99",                              // the price as formatted text with currency
      "rating": 4.5,                                      // the product rating
      "description": "product description",               // the product description
      "specs": "product specs",                           // the product specs - this is just a suggestion.  Feel free to add any additional fields needed for the UI.
      "media": {                                          // images and videos for the MediaCarousel component
        "full": [{                                        // an array of full size images
          "src": "https://domain.com/path/to/image",      // the URL of the full size image
          "alt": "alt text",                              // alt text for the full size image
          "type": "image",                                // "image" or "video" - by default entries will be treated as images
          "magnify": {                                    // optional - provides a high-res image for manigfication on hover in desktop browsers
            "height": 1200,                               // the height of the high-res image
            "width": 1200,                                // the width of the high-res image
            "src": "https://domain.com/path/to/image"     // the URL of the high res image
          }
        }],
        "thumbnails": [{                                  // an array of thumbnails to display below the main image carousel
          "src": "https://domain.com/path/to/image",      // the thumbnail URL
          "alt": "alt text"                               // alt text for the thumbnail
        }]
      },
      "sizes": [{                                         // an array of available sizes
        "id": "sm",                                       // the size code
        "text": "SM"                                      // text to display on the button corresponding to this size
      }],
      "colors": [{                                        // an array of available colors
        "text": "Red",                                    // optional - text to display below the color button
        "id": "red",                                      // the color code
        "image": {                                        // the image for the color swatch
          "src": "https://domain.com/path/to/image",      // the URL for the color swatch
          "alt": "red"                                    // alt text for the color swatch
        },
        "media": {                                        // overrides the `media` on the base `product` object when this color is selected
          "full": [{                                      // this is how you can change images and thumbnails when the user selects a different color
            "src": "https://domain.com/path/to/image",
            "alt": "Product 1",
            "type": "image",                              
            "magnify": {
              "height": 1200,
              "width": 1200,
              "src": "https://domain.com/path/to/image",
            }
          }],
          "thumbnails": [{
            "src": "https://domain.com/path/to/image",
            "alt": "green"
          }]
        }
      }]
    }
  }
}

UI

Fetching user-specific data

Often a product page will contain personalized information such as product recommendations that is different for each user and thus not cacheable. All non-cacheable information should be fetched in a separate request after the page component mounts to preserve the cacheability of the main product URL. The starter app contains an example of this in components/product/SuggestedProducts.js. It uses React's useEffect hook with an empty array to fetch personalized data from the API when the page mounts:

// components/product/SuggestedProducts.js

export default function SuggestedProducts({ product }) {
  const [suggestedProducts, setSuggestedProducts] = useState(null)

  // Fetch suggested products when the product page is mounted
  useEffect(() => {
    fetch(`/api/p/${encodeURIComponent(product.id)}/suggestions`)
      .then(res => res.json())
      .then(result => setSuggestedProducts(result))
  }, [])

  return (
    <div>
      <Typography variant="h6" component="h3">
        Suggested Products
      </Typography>
      {/* ... */}
    </div>
  )
}

Changing the images when the user selects a color

Coming soon

Capturing the user's selections

Users can typically select a number of options on the product page. Some of the most common are size, color, and quantity. In the starter app, these are stored in pageData, not in the product object, but as a peer to it:

{
  "pageData": {
    "product": {
      // data from the API (see "Data Model" above)...
    }
    "quantity": 1,    // the value of the quantity selector
    "color": {        // the object corresponding to the selected color
      "id": 'red',
      "name": 'red',
      // ...
    },
    "size": {         // the object corresponding to the selected size
      "id": "md",
      "name": "MD",
      // ...
    }
  }
}

Adding a product to the cart

In the starter app, when the user taps the "Add to Cart" button, the enclosing form element is submitted, triggering the onSubmit handler. It extracts the user's selections from the pageData state and posts them to /api/cart:

// pages/p/[productId].js

const Product = React.memo(lazyProps => {
  // ...

  const [store, updateStore] = useLazyState(lazyProps, { pageData: { quantity: 1 } })
  const product = get(store, 'pageData.product') || {}
  const color = get(store, 'pageData.color')
  const size = get(store, 'pageData.size')

  // Adds an item to the cart
  const handleSubmit = async event => {
    event.preventDefault() // prevent the page location from changing
    setAddToCartInProgress(true) // disable the add to cart button until the request is finished

    try {
      // send the data to the server
      const { cartCount } = await fetch('/api/cart', {
        method: 'post',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: product.id,
          color: get(color, 'id'),
          size: get(size, 'id'),
          quantity,
        }),
      }).then(res => res.json())

      // open the confirmation dialog
      setConfirmationOpen(true)

      // update the number of items in the cart in the header
      actions.updateCartCount(cartCount)
    } finally {
      // re-enable the add to cart button
      setAddToCartInProgress(false)
    }
  }

  // ...
})

If you add additional options to the PDP, those selections should be added to the data posted to /api/cart above. By default, a confirmation dialog is displayed upon receiving a successful response from the server. You may also choose to navigate directly to the cart or checkout. You can do so by changing handleSubmit.

Integration with 3rd party ecommerce platforms

To connect the add to cart flow to a 3rd party ecommerce platform, you can either call their add to cart API endpont directly from the submitHandler above, or from the /api/cart request handler in pages/api/cart.js. The latter option can be convenient when you need to make the 3rd party API easier for your UI to communicate with.

Syncing variant data

In the case you have product variant data which will need to be fetched, you can sync the product data model using the DataBindingProvider. For example, if the user changes a product option or product color, you can have new product data fetched for the new selection:

<DataBindingProvider
  remote="/api/p/{product.id}?color={color.id}"
  // along with the other necessary props
>
  <Price bind="product.price">
  <ColorSelector />
</DataBindingProvider>

In this example, as soon as product.id or color.id changes, this remote API call will be fetched and the state will be updated with the response data.

This feature will also work in AMP if you use bind to get the values from the data model.