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>
)
}
See the complete list-products example on GitHub for a full implementation with authentication, error handling, and multi-location inventory.