Joint session with Vercel: How to build intelligent storefronts (May 15th)

PortableText not displaying in Product component but works in Blog

3 repliesLast updated: Nov 29, 2025

Hi everyone! I am trying to display the body data from a Product component with PortableText, and cannot seem to get it to show up in the browser. I had this exact same setup working perfectly with a blog, and was able to fetch and display the Body data, however when I am fetching data for a 'Product' and want to display the text in the 'body', it doesn't seem to show up on this ecommerce project, but it DOES show up on the blog project. Here is the [slug].js file, can anyone tell me why PortableText isn't showing up, but everything else is?

import Link from "next/link";
import { useRouter } from "next/router";
import groq from "groq";
import {
	PortableTextComponentsProvider,
	PortableText,
} from "@portabletext/react";
import { client } from "../../lib/sanity";
import imageUrlBuilder from "@sanity/image-url";

const builder = imageUrlBuilder(client);
function urlFor(source) {
	return builder.image(source);
}

const ptComponents = {
	types: {
		image: ({ value }) => {
			if (!value?.asset?._ref) {
				return null;
			}
			return (
				<img
					alt={value.alt || " "}
					loading="lazy"
					src={urlFor(value).width(320).height(240).fit("max").auto("format")}
				/>
			);
		},
	},
	block: {
		normal: ({ children }) => <p>{children}</p>,
	},
};

export default function Product({ products }) {
	const router = useRouter();
	if (router.isFallback) {
		return (
			<section>
				<h1>Loading...</h1>
			</section>
		);
	}
	const {
		defaultProductVariant = {},
		title = "Missing title",
		blurb,
		body = [],
	} = products;
	const { images, price = "" } = defaultProductVariant;

	return (
		<article>
			<h1>{title}</h1>
			<p>${price}</p>
			<hr />
			<img src={urlFor(images[0]).width(300)} />
			<PortableText value={body} components={ptComponents} />
			<p>{blurb.en}</p>
			<Link href="/">
				<a>Back home</a>
			</Link>
		</article>
	);
}

const query = groq`*[_type == "product" && slug.current == $slug][0]{
    title,
    image,
    price,
	body,
    blurb,
    "slug": slug.current,
    defaultProductVariant
}`;

export const getStaticProps = async (context) => {
	const { slug = "" } = context.params;

	const products = await client.fetch(query, { slug });

	return {
		props: {
			products,
		},
	};
};

export async function getStaticPaths() {
	const paths = await client.fetch(
		groq`*[_type == "product" && defined(slug.current)][].slug.current`
	);

	return {
		paths: paths.map((slug) => ({ params: { slug } })),
		fallback: false,
	};
}
AI Update

I can see your issue! Looking at your code, you're destructuring body from the products object, but in your GROQ query, you're fetching the body field at the top level. However, the body field likely contains block content that references other documents or assets, and you need to expand those references in your query.

The most likely issue is that your body field contains block content that needs to be properly expanded. Try updating your query to this:

const query = groq`*[_type == "product" && slug.current == $slug][0]{
    title,
    image,
    price,
    body[]{
      ...,
      markDefs[]{
        ...,
        _type == "internalLink" => {
          "slug": @.reference->slug
        }
      },
      _type == "image" => {
        ...,
        asset->
      }
    },
    blurb,
    "slug": slug.current,
    defaultProductVariant
}`;

The key change is body[]{...} which properly expands the array of block content. This syntax tells GROQ to expand each item in the body array, handle image assets (with asset->), and resolve any internal link references in markDefs.

A few other things to check:

According to the Presenting Portable Text guide, when you have references to other documents (like images or internal links) in your Portable Text, you need to use GROQ's join syntax to properly fetch the referenced data. The body[]{...} syntax with asset dereferencing is essential for PortableText to render correctly.

Show original thread
3 replies

Was this answer helpful?

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Related contributions