CoursesEditorialized ecommerce experiencesNext block: Curated products with references

Editorialized ecommerce experiences

Lesson
3

Next block: Curated products with references

Resolve deeply nested references in your GROQ query and create a polymorphic component to render your content.
Log in to mark your progress for each Lesson and Task

Your current campaign needs to highlight specific products that are part of the collection. The "products" block allows creators to choose a product, and one of its specific variants.

In your Sanity studio, update the body field with another block.

Add a "products" block to the body field with two references: the BIZU Trinket Tray and ZAPI Incense Burner.

The products block in your body field should now look like this:

Not that each reference in the products array contains an additional reference field which allows you to select one of that product's variants.

In your Studio files, open the productWithVariant schema type to see how these two reference fields work – where the variant reference field is dynamically filtered and validated to only allow variants of the currently selected product.

These are the sorts of small details that have extremely high impact for content creators. These references point at up-to-date sources of truth with product content automatically updated from Shopify.

Looking at the stringified version of the block being rendered on the page now, you'll notice _type: reference in the data. By default, a GROQ query for a reference will only return its ID.

You'll need to update the query for the page in the Hydrogen app. The updated page query below maps over each item in the body array, and selectively returns specific data depending on the _type of block.

Update PAGE_QUERY to resolve product references in the products block.
./app/sanity/queries.ts
import groq from 'groq';
export const PAGE_QUERY = groq`*[_type == "page" && slug.current == $slug][0]{
...,
body[]{
_key,
_type,
...,
_type == "products" => {
layout,
products[]{
_key,
productWithVariant {
"product": product->{
"title": store.title,
"image": store.previewImageUrl,
"slug": store.slug,
"price": store.priceRange,
},
"variant": variant->{
"title": store.title,
"image": store.previewImageUrl,
"price": store.price,
},
}
}
}
}
}`;

Now this query resolves references and returns data from those documents. However, since the query has changed, its Type will need to be recreated.

In your Studio directory, update your Types
Terminal
npx sanity@latest typegen generate

You should now see the reshaped data being rendered onto the page.

Now you'll need to add a component to take this data and render it. Below is some example code for you to paste into your project.

In a production project, some of these components would be better split out into their own files to make their use more generic. We're optimising for speed in this module, so it's all contained in one file.

Create a new component for the Products block
./app/components/ProductsBlock.tsx
import type {PAGE_QUERYResult, PriceRange} from '~/sanity/sanity.types';
type BodyWithoutNull = NonNullable<
NonNullable<PAGE_QUERYResult>['body']
>[number];
type ProductsBlockProps = Extract<BodyWithoutNull, {_type: 'products'}>;
export function ProductsBlock({products, layout}: ProductsBlockProps) {
return Array.isArray(products) ? (
<div className="p-4 bg-blue-50">
<div className="container mx-auto grid grid-cols-2 gap-4">
{products.map((product) =>
layout === 'pill' ? (
<ProductPill key={product._key} {...product} />
) : (
<ProductCard key={product._key} {...product} />
),
)}
</div>
</div>
) : null;
}
type ProductWithVariant = NonNullable<ProductsBlockProps['products']>[number];
function ProductCard({productWithVariant}: ProductWithVariant) {
if (!productWithVariant) {
return null;
}
const productImage =
productWithVariant?.variant?.image || productWithVariant?.product?.image;
const price =
productWithVariant?.variant?.price || productWithVariant?.product?.price;
return (
<div className="grid grid-cols-1 gap-2">
{productImage ? (
<img
src={productImage}
className="w-full aspect-square object-cover rounded-lg"
alt={productWithVariant?.product?.title || ''}
/>
) : null}
{productWithVariant?.product?.title ? (
<h2 className="text-lg font-bold">
{productWithVariant.product.title}
</h2>
) : null}
{price ? <ProductPrice price={price} /> : null}
</div>
);
}
function ProductPill({productWithVariant}: ProductWithVariant) {
if (!productWithVariant) {
return null;
}
const productImage =
productWithVariant?.variant?.image || productWithVariant?.product?.image;
const price =
productWithVariant?.variant?.price || productWithVariant?.product?.price;
return (
<div className="flex items-center gap-4 rounded-full bg-blue-100 p-2">
{productImage ? (
<img
src={productImage}
className="w-20 h-20 object-cover rounded-full shadow-inner"
alt={productWithVariant?.product?.title || ''}
/>
) : null}
<div>
{productWithVariant?.product?.title ? (
<h2 className="font-bold">{productWithVariant.product.title}</h2>
) : null}
{price ? <ProductPrice price={price} /> : null}
</div>
</div>
);
}
function ProductPrice({price}: {price: PriceRange | number}) {
if (typeof price === 'number') {
return <span>${price.toFixed(2)}</span>;
} else if (
typeof price.minVariantPrice === 'number' &&
typeof price.maxVariantPrice === 'number'
) {
return (
<span>
${price.minVariantPrice.toFixed(2)} - $
{price.maxVariantPrice.toFixed(2)}
</span>
);
}
return null;
}
Add the ProductsBlock component to the BLOCKS object
./app/routes/$slug.tsx
import {AccordionBlock} from '~/components/AccordionBlock';
import {ProductsBlock} from '~/components/ProductsBlock';
const BLOCKS: Record<string, (props: any) => JSX.Element | null> = {
accordion: AccordionBlock,
products: ProductsBlock,
_unknown: (props: any) => <pre>{JSON.stringify(props, null, 2)}</pre>,
};

You should now see the products block rendered on the page – experiment with the order of products, editing the selected variant, and switching the "layout" field value.

This products block could be considered polymorphic because it contains a field called layout which determines whether the products render as "cards" or "pills." These sorts of visual-specific fields should be kept to a minimum and given semantic meaning where possible.

For example, a color selector should prompt an author to choose between "primary" or "secondary" instead of "blue" or "red."

It may be tempting to begin building all manner of design controls for things like padding, margin, fonts or even raw CSS fields (we've seen it all!) but we strongly caution against this.

Where possible, let the front end decide how content should be rendered with smartly applied logic. Turning Sanity into a design tool is a sure way to build rigid, overly complex content schemas that are based around your website design at a point in time – instead of the most meaningful way to author and structure content.

You have 5 uncompleted tasks in this lesson
0 of 5