Published on November 22, 2024
The Contentful Developer Hero Program is about recognizing, supporting, and empowering the most passionate and talented developers in our community. We want to spotlight and reward developers who are actively engaged with Contentful, creating a bridge for collaboration between developers and our team. You can learn more about the program here.
In this post, Developer Hero Ignacio demonstrates how to build a product details page using the powerful combination of Commerce Layer, Cloudinary and Contentful (naturally). Follow along with this tutorial to build a product page for a fetching beanie, and soon you'll have the makings of your very own composable commerce empire. Just in time for Black Friday!
Commerce Layer is an API-first commerce engine, battery-charged with a full suite of APIs, webhooks, and dev tools, focusing on the transactional side of the ecommerce experience (e.g., cart and order management).
On the other hand, Cloudinary is an end-to-end image and video-management solution, offering comprehensive APIs and covering everything from image and video uploads, storage, manipulations, and optimizations to delivery, all being powered by a fast content delivery network (CDN).
This tutorial will guide you through using these platforms cohesively so each of them have a crucial and well-defined responsibility, enabling and facilitating the overall management experience:
Cloudinary: providing asset management and access to our product images.
Commerce Layer: Enabling the ecommerce capabilities of the website by managing our product SKUs.
Contentful: being the main data entry point and host of the rich content associated with our product, as well as serving as the connector with Commerce Layer.
We'll go through the steps of building a Product Details Page (PDP) which will be responsible for displaying all the relevant information of our product so a final user can make the decision of buying it or not.
All the code in this tutorial can be found in this repository.
To follow along, you’ll need:
A Contentful account. Sign up if you do not have one.
A Commerce Layer account. Sign up if you do not have one.
A Cloudinary account. Sign up if you do not have one.
Intermediate knowledge of JavaScript.
Node.js version 20 or above.
You'll also benefit from knowing a bit of Next.js, even though the data-fetching principles used in this post can be applied to any framework.
In order to complete your initial setup for Commerce Layer, you can follow steps 1 and 2 of their onboarding tutorial here. We don't need to complete steps 3 and 4 since we won't be placing orders in this guide.
After having run the commercelayer seed command, the following resources will be created in your Commerce Layer organization:
This is not an introductory Commerce Layer course, so I will limit myself to providing a diagram of the minimum required entities we need to take into account for our demo to work. I do encourage you though to take a deep look into both the developer documentation and the data model if you're not that familiar with the terms we'll cover here.
The Commerce Layer model is quite a thing! And as you can see I've omitted a lot of resources the seeder script has created for us. The model being this modular is one of the virtues of Commerce Layer as it allows every resource to have a single and well-defined responsibility.
In order to understand it, we have to read it from top to bottom, that is, starting from SKU: An SKU has many prices, but each price belongs to a single price list, a price list relates with many markets, and so on.
You can browse all these resources in the Resources
tab of the dashboard.
Now that the minimal configuration is in place, we can start creating some SKUs by going into the Apps tab and clicking on the SKUs app. I created SKUs for black, green, red, and purple beanies using the same configuration you can see in the second image.
Now we'll look for the Price Lists app and open it. You can use the Price List that was created as part of the initial seeding process. I'll go and create a new one by clicking on the New
button. Here, we'll give it a name and set a currency for our list.
After being created, we can click on the New Price
button and add prices for our SKUs.
After creating all the prices, you'll end up with something like this:
Last but not least, we'll go into the Products section of the Apps
tab and look for the Inventory app so we add a stock quantity for each SKU.
You'll see that we have direct access to our Stock Locations. Let's get into the US Warehouse
stock location that was created during the seeding process. When clicking on the + Stock Item
button, we'll be prompted to select a SKU and the available quantity in this location.
After having added a stock item for all of our SKUs, we'll end up with a list like this in the Stock Location screen:
Great! Since Commerce Layer manages the transactional aspect of our ecommerce, and we won't get into the nitty-gritty of cart management and order placement, this configuration is sufficient. We now have to create the API credentials needed by Contentful so we can link both systems.
In order to create them, we have to go into the API Credentials
tab, click on the New
button and select the Integration
type. Give it a meaningful name and choose the Admin
role.
Copy and save the Client ID, Client secret, the Authorization Scopes and the Base endpoint values. We'll need them now that we're getting back into our beloved Contentful!
But before doing so (and do this now rather than later), we also have to create a new API Credentials of type Sales channel. These will be needed for our frontend app to fetch data from Commerce Layer.
It's important that these are different from the Integration
credentials we created before, as well as being of type Sales channel, since we don't want the front end to have the minimal possibility of being able to operate with sensitive data. These credentials will only have read access and are for public use. Again, copy everything and store the values somewhere for now.
Contentful will be responsible for having a Product
entry which will contain Variant
entries, being that this is a mirror of the SKUs that are part of the same product. This content type will also have a Rich Text field for the description of the SKU and an Attributes field, which is a multiref field for Attribute
entries and has the label and value for the attributes of the variant.
First, we need to install the Commerce Layer application in our environment. Click on the Apps
tab present in the main Contentful dashboard, then on Marketplace
. Then, click on the ecommerce tab, choose the Commerce Layer app, and finally, install and authorize access for the application in the desired environment.
Now the Installation screen will show up. Enter the credentials values obtained from Commerce Layer in the last section and click Install.
Now that our app was successfully installed, we can first create the Attribute
content type. This will be responsible for defining the variant-defining attributes.
For our demo, I'll create four entries, one for each color.
Variant-defining attributes can grow really fast in more complex product models. For example, we could also have a Size attribute, leading to more possible combinations for our products. To keep it simple, we'll just work with Color in this guide.
Another important thing to note is that Commerce Layer also has the ability to add metadata to each SKU. We're not using that to store the attributes of each SKU in this example, since we want these to be localizable, but Contentful is the perfect fit for that purpose.
Let's continue with the Variant
content type. One of the most important fields is a short text field named “SKU” which will contain the direct relationship with the SKU obtained from Commerce Layer.
It's important that we choose the Commerce Layer appearance when configuring the field:
Now, let's add the validation to our Attributes field so we can only select Attribute
entries.
Now we can create the Product content type, which will be kind of a “wrapper” for our variants/SKUs, so we have a single entry point when getting the product using the Contentful API.
The most important piece of this content type is the Variants
field since it's a many-ref field for Variant
entries.
Now that we have our content model in place, we can start creating some entries getting the SKUs from Commerce Layer.
Let's start with the Variant
entries. When creating one of these, you'll see that the appearance of the SKU
field looks like this:
When clicking on “Add SKU,” a new screen will show up with the SKUs we created in Commerce Layer.
You can choose any of them and associate it with the entry. Now the SKU literal string will be the value for our SKU field. You can also go directly into the page of this SKU in Commerce Layer by clicking on the external link icon.
You can repeat this process for all of the SKUs you want to create for a specific product. I'll do it thrice more so I have four SKUs to play with.
For each of the SKUs, I'll also add an Attribute
entry defining the variant color. Here's an example of the entry for the green color:
Now, let's proceed and create a Product
entry, which will contain our Variant
entries.
Note how I have added four variants to this product and that its slug is classic-beanie
. By doing this, we have a single entry point which we can use to get all of the product’s data when visiting the Product Details Page on the website.
As far as Contentful is concerned, that's all we have to do. This is a good moment for you to also obtain your Contentful API Key (basically your space ID and CDA/CPA), since we'll need it later to complete the code integration. Go to Settings > API keys
in Contentful and generate a new API key. Your space ID and CPA/CDA access token will be listed there. Save these for later.
Now let's get into Cloudinary!
Cloudinary will serve as the storage for our product assets. At this point, you should already have created your Cloudinary account and have got access to your Cloud. When you land on your dashboard, you'll see something like this:
Before getting into exciting stuff, I'd want us to check the API creds off.
At the top of the Dashboard you'll find the Product Environment
section. Here you can get your Cloud Name. This is all we'll need for our demo. Since we won't be interacting with write-access endpoints, we don't need to create an API Key.
Now click on the Assets
tab (red highlighted section on the dashboard image) so we can start uploading some images.
In order to upload an image, you can click on the blue Upload
button at the top-right corner of the Media Library screen.
After doing so, you'll be prompted to choose your asset so it gets uploaded into Cloudinary.
In order for our integration to work as expected, we need to add a tag to each of the images we upload with the respective SKU code (the one we defined in Commerce Layer). This will become handy later in order to get the images of our product automatically. You can do so by uncollapsing the Advanced section at the bottom of the Upload screen.
For example, I'll upload the Black Beanie image, adding the tag BLKB001, and defining the public ID (asset identifier) of the image as black-beanie
.
You can repeat the same process for every image you want to upload for your product. In my case, I uploaded four beanie images, one for each color, adding the respective SKU as a tag. You'll have access to them in the code repository as well. 😉
If you forgot to add the tag while uploading the image, that's okay! You can also add it by double clicking on the image, going into the Metadata
tab, add the tag, then click on Save.
Cloudinary has many (actually a ton!) of cool features to transform your assets. This is not a Cloudinary-centric tutorial, but I do encourage you to take a look at everything you can do with Cloudinary here!
FYI — I only had a real image for the black beanie, but by using the Cloudinary APIs, I removed the background and changed the dimensions of that image. I also created the rest of the images using these APIs and by changing contrast and colors, and you can see the result is pretty good!
Before moving on, we need to make sure we have unrestricted the Resource List
image type.
Cloudinary has several image types which can be granularly restricted, which means that in order to get them using the Cloudinary API, we need to provide a signature.
For the sake of simplicity, we'll just make sure the restriction is disabled for our demo to work, otherwise we'll not be able to fetch the images without providing the signature. You have to evaluate whether you're fine with this or if your use case actually requires you to restrict these type of images.
Click on the Configure cog icon at the bottom of the Cloudinary UI's right sidebar, then click on the Security tab, and finally uncheck Resource list
from the Restricted image types.
Finally, make sure to scroll all the way down and click the orange Save button so we actually save this change!
We now have everything we need to start building our React application, the third parties setup, as well as the credentials.
We'll use Next.js as the frontend framework to build our website. We can start by running the Next command:
npx create-next-app ecommerce-pdp
Secondary configuration, like TypeScript, linting, Prettier, alias paths, src folder, etc., is up to you, but for this guide, I'll use the App Router instead of the Pages Router. The data-fetching principles we'll use can also be replicated in the Pages Router by using methods like getServerSideProps
. You can also apply the fetching logic we'll use in a normal React application built from scratch or using something like Vite.
After creating the project, you'll end up with something like this:
We can delete the src/app/page.jsx
and src/app/page.module.css
files, we won't need them. Let's also update the src/app/globals.css
file with the following code. It's basically a simple normalizer.
We'll create a product/[slug]/page.jsx
file within the app folder. In this file, we'll be able to access the slug of the page when we visit a route like http://localhost:3000/product/my-product-slug
.
We can start with something really simple, just so we see the slug on the page. Try and visit the URL I used above, you'll see the my-product-slug
text in a blank page.
Now we need to start fetching the data from Contentful, that is our Product entry.
Start by creating a .env
file adding the following variables:
CONTENTFUL_SPACE_ID=<your-space-id>
CONTENTFUL_TOKEN=<your-cda/cpa-token>
If you use the CDA token, make sure you publish any change you make to the Product and Variant entries.
Now run the following so we have the Contentful dependency installed:
npm install contentful
Then we can create a src/services/contentful.js
file which will contain our Contentful client and the getProduct function to fetch the Product entry.
We can now update our page file by using this function to fetch the entry from Contentful and then we're able to see our Product data.
We're now able to see something like this when visiting the page with the slug of the created product:
Great! We got the data from Contentful, but we're not quite done yet. We now need to fetch the data from Commerce Layer so we can get the price and inventory of our SKUs.
We can get started by running the following install command:
npm install @commercelayer/js-auth @commercelayer/sdk
@commercelayer/js-auth: a library that wraps around the authentication API so we're able to get an access token.
@commercelayer/sdk: a library that allows us to easily interact with the Commerce Layer API.
Let's go ahead and add three new variables to our .env
file:
COMMERCELAYER_CLIENT_ID=
COMMERCELAYER_CLIENT_SECRET=
COMMERCELAYER_ORGANIZATION_SLUG=
# You can get the scope from the API Credentials page
COMMERCELAYER_SCOPE="market:id:{id} stock_location:id:{id}"
These are the Sales Channel API credentials we created above at the end of the Commerce Layer Project Configuration section. Remember that the organization slug is part of the base endpoint (https://slug.commercelayer.io
).
Now we can create a new services/commercelayer.js
file with the functions we will need in order to interact with the Commerce Layer API.
In the getToken
function, we use the authentication
method from the @commercelayer/js-auth library
passing the clientId, clientSecret, and the organization slug using the client_credentials
authentication method (you can read more about them here) in order to obtain the authentication token and its expiry. With the help of this function, we'll get the token so we're able to store it in a cookie for future use.
In the getSkus
function, we use the Commerce Layer client passing again the organization slug and our access token received by parameter. We then call the skus.list()
method specifying code_in: skuCodes.join(',')
as the filter. This basically means: Give me all the SKUs whose code matches with any of the provided in the list separated by comma (skuCodes.join(','))
.
Since we need to resolve the token before we start making any requests, this has to happen before even trying to render any server component, hence, the logic must be handled in the src/middleware.js
file, which, as per the Next.js documentation:
“The middleware.js|ts file is used to write Middleware and run code on the server before a request is completed. (...) Middleware executes before routes are rendered. It's particularly useful for implementing custom server-side logic like authentication, logging, or handling redirects.”
With this, we'll always be checking if the cookie is truthy. If not, we use our getToken
function to hit the Commerce Layer API, then store the newly generated token in the ecommerce-token cookie, which will be available in RSCs.
Additionally, I've added an exported config
object with some rules for the matcher property so our middleware only gets called with our web page requests (more information here).
With this in place, we're finally able to hit the commerce API and get our SKUs's data. Let's modify our src/app/product/[slug]/page.jsx
file a little bit to make use of the getSkus
function and also to render the information.
As of now, the UI will look something like this:
We have the product and its variants (SKUs), we have its ecommerce data such as inventory and price. We're missing the most appealing and flashy piece, the images!
First, let's add the last environment variable to our .env
file: CLOUDINARY_CLOUD_NAME
, and set the value you obtained from the Cloudinary dashboard.
In order to get the images from Cloudinary, we can use the following special endpoint: https://res.cloudinary.com/{cloud-name}/image/list/${tag}.json.
By specifying the cloud name and a tag, we can get all the images that have this tag. This is exactly why we previously added the SKU code of every Commerce Layer SKU as a tag to our product images.
Let's create a new file src/services/cloudinary.js
which will contain the following code:
The getImagesByTag
function receives a tag and gets all the images that have been tagged with it. Then, we return an object with the tag and an array with the URLs of these images.
Now, we can update our PDP component so we get the images along with the ecommerce data:
If you use the Next.js Image
component as I did, you'll also need to update the next.config.mjs
file, so, we enable the Cloudinary domain to be used in the image src property:
With this, we're able to see all of the relevant data of our SKUs in the UI!
So far, I've tried to keep the UI simple so we're able to see the important pieces: fetching the data and being able to use it to render something on the UI. Now, we'll add a nice touch of design so it looks more appealing. 😉 This is, of course, just to put the pieces together in a nicer way as an example of how you can build your own PDP.
Let's create a product-details-page.module.css
file in a new src/components
folder with the following:
Let's then create a new file product-details-page.jsx
in the same folder. This component will be responsible for handling the state of our app so we can change the selected SKU using some inputs of type “radio” and see how the UI reacts to this.
Finally, let's update our src/app/product/[slug]/product.jsx
file with this piece of code:
With all of this in place, we're now able to see a nicer product details page, allowing us to change between the different SKUs and see the information on the page change accordingly.
Well, that was a lot! We've integrated several headless solutions, creating a framework to build a complete ecommerce experience for products with color variants.
We left a lot of things out. We barely got into Commerce Layer and its complete model for ecommerce transactional operations, which also allows you to add items to a cart and place an order.
Another example is Cloudinary, which has many plug-and-play UI widgets that connect with your cloud and use images based on the filter criteria you provide.
They actually have a Product Gallery widget which might be a perfect fit for this scenario! I encourage you to take a look into the extensive list of features that all of these vendors provide so you can boost your solution even more.
Small reminder that you can find all the code (+ some small improvements) in this repository.
Thanks for reading!
Subscribe for updates
Build better digital experiences with Contentful updates direct to your inbox.