Skip to main content

Retrieving Product Details

This guide shows you how to fetch and display detailed product information for individual product pages, including images, pricing, inventory, and SEO metadata.

Fetch a single product

Use the getByContextProduct function to retrieve detailed information for a specific product:

import { getByContextProduct } from "@epcc-sdk/sdks-shopper"

const response = await getByContextProduct({
path: { product_id: productId },
query: { include: ["main_image"] },
})

const product = response.data?.data
const mainImages = response.data?.included?.main_images || []

Handle missing products

Check if the product exists and handle the not found case:

import { notFound } from "next/navigation"

export default async function ProductPage({ params }) {
const response = await getByContextProduct({
path: { product_id: params.id },
query: { include: ["main_image"] },
})

if (!response.data) {
notFound()
}

const product = response.data.data
// Continue with product display...
}

Extract product information

Safely extract product attributes with fallback values:

import { extractProductImage } from "@epcc-sdk/sdks-shopper"

const name = product.attributes?.name || "Unnamed Product"
const description = product.attributes?.description || "No description available"
const sku = product.attributes?.sku || "No SKU"

const mainImage = extractProductImage(product, mainImages)
const imageUrl = mainImage?.link?.href || "/placeholder.jpg"

Extract pricing information

Get product pricing with support for sale prices:

// Extract pricing information
const priceData = product.meta?.display_price?.without_tax

// Enhance price data with original price if available
if (priceData && product.meta?.original_price) {
priceData.original_price = product.meta.original_price.without_tax?.amount
}

Display product pricing

Create a component to show pricing with sale price handling:

function ProductPrice({ priceData }) {
if (!priceData) {
return <div className="text-gray-500">Price not available</div>
}

const currentPrice = priceData.amount
const originalPrice = priceData.original_price
const currency = priceData.currency
const isOnSale = originalPrice && originalPrice > currentPrice

const formatPrice = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount / 100)
}

return (
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-gray-900">
{formatPrice(currentPrice)}
</span>
{isOnSale && (
<span className="text-lg text-gray-500 line-through">
{formatPrice(originalPrice)}
</span>
)}
</div>
)
}

Generate SEO metadata

Create dynamic metadata for better search engine visibility:

export async function generateMetadata({ params }) {
const response = await getByContextProduct({
path: { product_id: params.id },
query: { include: ["main_image"] },
})

if (!response.data) {
return {
title: "Product Not Found",
}
}

const product = response.data.data
const mainImage = extractProductImage(
product,
response.data.included?.main_images || []
)

const name = product.attributes?.name || "Unnamed Product"
const description = product.attributes?.description || "No description available"
const imageUrl = mainImage?.link?.href || "/placeholder.jpg"

return {
title: name,
description: description,
openGraph: {
title: name,
description: description,
images: [imageUrl],
type: "website",
},
twitter: {
card: "summary_large_image",
title: name,
description: description,
images: [imageUrl],
},
}
}

Add structured data

Include JSON-LD structured data for rich search results:

// Create structured data for SEO
const productJsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name,
description,
sku,
image: imageUrl,
}

// Add price information if available
if (priceData) {
productJsonLd.offers = {
"@type": "Offer",
price: priceData.amount / 100,
priceCurrency: priceData.currency,
}
}

Build product detail layout

Create a responsive product detail page layout:

import Image from "next/image"
import Link from "next/link"

export default function ProductDetail({ product, mainImage, priceData }) {
const name = product.attributes?.name || "Unnamed Product"
const description = product.attributes?.description || ""
const sku = product.attributes?.sku || "No SKU"
const imageUrl = mainImage?.link?.href || "/placeholder.jpg"

return (
<div className="max-w-5xl mx-auto p-6">
<Link href="/" className="inline-block mb-6 text-blue-600 hover:underline">
← Back to products
</Link>

<div className="grid md:grid-cols-2 gap-8">
<div className="relative h-96 overflow-hidden rounded-lg">
<Image
src={imageUrl}
alt={name}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-contain"
/>
</div>

<div>
<h1 className="text-3xl font-semibold mb-2">{name}</h1>
<p className="text-sm text-gray-500 mb-4">SKU: {sku}</p>

<div className="mt-4 mb-4">
<ProductPrice priceData={priceData} />
</div>

<div className="mt-6">
<h2 className="text-xl font-medium mb-2">Description</h2>
<p className="text-gray-700">{description}</p>
</div>
</div>
</div>
</div>
)
}

Handle inventory display

Integrate inventory levels into the product detail page:

import { fetchInventoryLocations, fetchProductStock } from "../lib/inventory"
import { MultiLocationInventory } from "./components"

export default async function ProductPage({ params }) {
const response = await getByContextProduct({
path: { product_id: params.id },
query: { include: ["main_image"] },
})

if (!response.data) {
notFound()
}

const product = response.data.data

// Fetch inventory data in parallel for better performance
const [inventoryLocations, productStock] = await Promise.all([
fetchInventoryLocations(),
fetchProductStock(product.id),
])

return (
<div className="max-w-5xl mx-auto p-6">
{/* Product layout */}
<div className="grid md:grid-cols-2 gap-8">
{/* Product image */}
<div className="relative h-96">
<Image
src={imageUrl}
alt={name}
fill
className="object-contain"
/>
</div>

<div>
<h1 className="text-3xl font-semibold mb-2">{name}</h1>

{/* Price and inventory */}
<div className="mt-4 mb-4">
<ProductPrice priceData={priceData} />
<MultiLocationInventory
stock={productStock}
locations={inventoryLocations}
className="text-sm mt-1"
/>
</div>

{/* Description */}
<div className="mt-6">
<h2 className="text-xl font-medium mb-2">Description</h2>
<p className="text-gray-700">{description}</p>
</div>
</div>
</div>
</div>
)
}

Complete implementation

Here's a complete product detail page with all features:

import {
getByContextProduct,
extractProductImage,
} from "@epcc-sdk/sdks-shopper"
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
import { Metadata } from "next"
import {
ProductPrice,
MultiLocationInventory,
} from "../../components"
import {
fetchInventoryLocations,
fetchProductStock,
getStructuredDataAvailability,
} from "../../../lib/inventory"

export async function generateMetadata({ params }) {
const response = await getByContextProduct({
path: { product_id: params.id },
query: { include: ["main_image"] },
})

if (!response.data) {
return { title: "Product Not Found" }
}

const product = response.data.data
const mainImage = extractProductImage(
product,
response.data.included?.main_images || []
)

const name = product.attributes?.name || "Unnamed Product"
const description = product.attributes?.description || "No description available"
const imageUrl = mainImage?.link?.href || "/placeholder.jpg"

return {
title: name,
description: description,
openGraph: {
title: name,
description: description,
images: [imageUrl],
type: "website",
},
twitter: {
card: "summary_large_image",
title: name,
description: description,
images: [imageUrl],
},
}
}

export default async function ProductPage({ params }) {
const response = await getByContextProduct({
path: { product_id: params.id },
query: { include: ["main_image"] },
})

if (!response.data) {
notFound()
}

const product = response.data.data
const mainImage = extractProductImage(
product,
response.data.included?.main_images || []
)

const name = product.attributes?.name || "Unnamed Product"
const description = product.attributes?.description || "No description available"
const sku = product.attributes?.sku || "No SKU"
const imageUrl = mainImage?.link?.href || "/placeholder.jpg"

// Extract pricing information
const priceData = product.meta?.display_price?.without_tax

// Enhance price data with original price if available
if (priceData && product.meta?.original_price) {
priceData.original_price = product.meta.original_price.without_tax?.amount
}

// Fetch inventory data in parallel
const [inventoryLocations, productStock] = await Promise.all([
fetchInventoryLocations(),
fetchProductStock(product.id),
])

// Create structured data for SEO
const productJsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name,
description,
sku,
image: imageUrl,
}

// Add price and stock information to structured data
if (priceData) {
const stockAvailability = productStock
? getStructuredDataAvailability({
available: productStock.attributes.available,
allocated: productStock.attributes.allocated,
total: productStock.attributes.total,
})
: "https://schema.org/OutOfStock"

productJsonLd.offers = {
"@type": "Offer",
price: priceData.amount / 100,
priceCurrency: priceData.currency,
availability: stockAvailability,
}
}

return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}
/>
<div className="min-h-screen p-4 bg-gray-50">
<main className="max-w-5xl mx-auto bg-white p-6 rounded shadow-sm">
<Link
href="/"
className="inline-block mb-6 text-blue-600 hover:underline"
>
← Back to products
</Link>

<div className="grid md:grid-cols-2 gap-8">
<div className="relative h-96 overflow-hidden rounded-lg">
<Image
src={imageUrl}
alt={name}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-contain"
/>
</div>

<div>
<h1 className="text-3xl font-semibold mb-2">{name}</h1>
<p className="text-sm text-gray-500 mb-4">SKU: {sku}</p>

<div className="mt-4 mb-4">
<ProductPrice priceData={priceData} />
<MultiLocationInventory
stock={productStock}
locations={inventoryLocations}
className="text-sm mt-1"
/>
</div>

<div className="mt-6">
<h2 className="text-xl font-medium mb-2">Description</h2>
<p className="text-gray-700">{description}</p>
</div>
</div>
</div>
</main>
</div>
</>
)
}

Add error handling

Create a not found page for missing products:

// not-found.tsx
import Link from "next/link"

export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4">
Product Not Found
</h1>
<p className="text-gray-600 mb-6">
The product you're looking for doesn't exist or has been removed.
</p>
<Link
href="/"
className="inline-block bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Back to Products
</Link>
</div>
</div>
)
}
tip

See the complete list-products example on GitHub for a full implementation with authentication, error handling, and multi-location inventory.