PortableText not showing up in browser for a Product component in an ecommerce project

3 replies
Last updated: Aug 30, 2022
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:

  1. Verify the data is actually there: Add a console.log(body) or console.log(products) right before your return statement to see if the body data is actually being fetched. If it's undefined or an empty array, that confirms the query issue.

  2. Check your schema: Make sure your Product schema actually has a body field defined as an array of block content:

    {
      name: 'body',
      type: 'array',
      of: [{type: 'block'}]
    }
  3. API version: If you're using a recent Sanity API version, be aware that the default perspective changed to published in API v2025-02-19. Make sure your product documents are actually published, not just drafts. You can test this by temporarily adding perspective: 'previewDrafts' to your client configuration.

  4. Compare with your blog query: Since you mentioned this exact setup works for your blog, compare the GROQ queries between the two projects. The blog query likely has the proper expansion syntax (body[]{...}) that the product query is missing.

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.

Hi User. I see you have
blurb.en
and am curious if you also have that property on
body
. Could you try using
body.en
as your portable text value?
That did it! Thank you! I was looking through the schema earlier and noticed the type being localeBlockType, so this makes total sense.
Great! Happy to hear it. 😀

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.

Was this answer helpful?