# Course: Editorialized ecommerce experiences
https://www.sanity.io/learn/course/editorialized-ecommerce-experiences

A catalogue of products is only a small part of the story. Empower content creators to embellish product data with rich product display pages and dynamic collection filters that tell a complete picture of your offering.

---

## Navigation

## Contents

1. [Creating campaign pages](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/creating-campaign-pages) · [markdown](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/creating-campaign-pages.md)
2. [Simple block: Accordion](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/simple-block-accordion) · [markdown](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/simple-block-accordion.md)
3. [Next block: Curated products with references](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/next-block-curated-products) · [markdown](https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/next-block-curated-products.md)

---

## Lesson 1: Creating campaign pages
https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/creating-campaign-pages

With Sanity, Shopify and Hydrogen connected it's time to build custom, highly curated landing pages with fine-grained control over presentation.

In [Sanity and Shopify with Hydrogen](https://www.sanity.io/learn/course/sanity-and-shopify-with-hydrogen) you setup the basics to connect Sanity, Shopify and a Hydrogen front end. With these systems now all working together it's time to create rich experiences.



In the following exercises you'll create a campaign page for a specific set of products. This is the sort of editorial work your content creators are likely going to need to do on a regular basis.



With Sanity, you can prepare the building blocks they need to instantly create new landing pages while benefiting from referencing up to date external content like product data.



The `shopify` Studio template you've used comes with a number of pre-configured blocks which you could add to or modify. They've been made available to this portable text field.



- [ ] In your Studio, **create** a new page type document.


![Portable text field showing available blocks](https://cdn.sanity.io/images/3do82whm/next/6ed6d69b54d7977f22d47444c20e9287f2a6a8e6-2144x1388.png)

In your Sanity Studio, take a look at the `portableTextType.ts` schema type file. It is an `array` type field that first contains a `block` type – and then multiple other object types. That's where this list was configured.



> [!TIP]
> The Portable Text editor is not just for rich text, but also block content!



## Page building



Looking at page documents in the Studio, the `body` field is just an `array` type field. But, since one of its fields is a `block` type field, Sanity Studio presents renders it with the Portable Text editor – instead of the default array type UI. 



The Portable Text editor allows you to write rich text *and* block content.



While the data is the same – an array of objects – no matter which editor you use, for this module we're making the editorial decision that page content will only ever be block content. For this the default array editor is simpler to use.



Let's copy the block types from this portable text field and replace the `body` field in `page` documents.



- [ ] **Update** the `body` field in your `page` type schema


```typescript:./schemaTypes/documents/pageType.ts
defineField({
  name: 'body',
  type: 'array',
  group: 'editorial',
  of: [
    defineArrayMember({ type: 'accordion' }),
    defineArrayMember({ type: 'callout' }),
    defineArrayMember({ type: 'grid' }),
    defineArrayMember({ type: 'images' }),
    defineArrayMember({ type: 'imageWithProductHotspots', title: 'Image with Hotspots' }),
    defineArrayMember({ type: 'instagram' }),
    defineArrayMember({ type: 'products' }),
  ]
}),
```

The `body` field should now look like this:



![Sanity Studio with array field open](https://cdn.sanity.io/images/3do82whm/next/6eb2ce535406b7ba38ccfefb36953a7c171cf8e0-2144x1388.png)

You can start creating content with these blocks, but most importantly, you'll need to update your Hydrogen front end to render them.



### Generate Types



For a smoother developer experience in the following exercises, generate Types from your Studio schema and copy the definitions into the Hydrogen app.



> [!TIP]
> Read more about [Sanity TypeGen](https://www.sanity.io/learn/apis-and-sdks/sanity-typegen) in the documentation



Inside your Studio project:



- [ ] **Extract** the current schema types to a JSON file


```:Terminal
npx sanity@latest schema extract
```

- [ ] **Create** a typegen configuration file so that a file containing your Types are written directly to your Hydrogen app when `typegen generate` is run in your Studio


```json:./sanity-typegen.json
{
  "path": "../sanity-and-hydrogen/app/sanity/queries.ts",
  "schema": "schema.json",
  "generates": "../sanity-and-hydrogen/app/sanity/sanity.types.ts"
}
```

- [ ] **Generate** types from that JSON file


```:Terminal
npx sanity@latest typegen generate
```

You should now have a file `sanity.types.ts` inside your Hydrogen app.



## Create a page route



Now in your Hydrogen app, you'll need to setup a route to render `page` type documents.



- [ ] **Create** a new document to contain your GROQ queries


```typescript:./app/sanity/queries.ts
import groq from 'groq';

export const PAGE_QUERY = groq`*[_type == "page" && slug.current == $slug][0]`
```

- [ ] **Create** a new route in your Hydrogen app


```tsx:./app/routes/$slug.tsx
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {useLoaderData} from '@remix-run/react';
import type {PAGE_QUERYResult} from '~/sanity/sanity.types';
import {PAGE_QUERY} from '~/sanity/queries';

const BLOCKS: Record<string, (props: any) => JSX.Element | null> = {
  _unknown: (props: any) => <pre>{JSON.stringify(props, null, 2)}</pre>,
};

export async function loader({params, context: {sanity}}: LoaderFunctionArgs) {
  const query = PAGE_QUERY;
  const initial = await sanity.loadQuery<PAGE_QUERYResult>(query, params);

  if (!initial.data) {
    throw new Response('Not found', {status: 404});
  }

  return json({initial});
}

export default function Page() {
  const {initial} = useLoaderData<typeof loader>();
  const page = initial?.data;

  return Array.isArray(page?.body) ? (
    <main>
      {page.body.map((block) =>
        block._type in BLOCKS
          ? BLOCKS[block._type]({key: block._key, ...block})
          : BLOCKS._unknown({key: block._key, ...block}),
      )}
    </main>
  ) : null;
}
```

This route renders each array field item as a unique component. If a matching component is not found, the item's content is *stringified* and rendered to the page.



### Create a page



Back in your Sanity Studio, create a page



- [ ] **Publish** a new page document with the title "The Blue Collection" and generate a `slug`

- [ ] **Add **an "Accordion" block to the `body` field


Visit [http://localhost:3000/the-blue-collection](http://localhost:3000/the-blue-collection) and you should see a page something like this:



![A plain webpage showing JSON data](https://cdn.sanity.io/images/3do82whm/next/63f8fd1986db9ad161e802dce756204bc8d127a2-2144x1388.png)

Now, let's style this "accordion" block.



---

## Lesson 2: Simple block: Accordion
https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/simple-block-accordion

An "accordion" block is useful to display content such as frequently asked questions or terms and conditions.



## Prepare your content



In the Studio, add an Accordion item with the following two groups to the Body field with the following text:



```
What's the best way to take care of Jesmonite?

To clean any Jesmonite products, wipe down with a non-abrasive damp cloth. We coat our products with a waterproof sealant, but please do not leave Jesmonite to soak.
```

```
Is Jesmonite safe to eat from?

Whilst Jesmonite is a non-toxic, environmentally friendly material, we don’t recommend eating off the products, but can be used to serve dry things like nuts or whole fruit or to protect surfaces from hot pots & plates.
```

## Prepare your front end



Thankfully, HTML gives us details and summary tags to natively show and hide content. All we need to do is to render our Sanity content into the correct tags.



Take note also in the component below how the imported `PortableText` component handles the array of objects that field creates, and renders HTML.



- [ ] **Create** a new file to your Hydrogen project


```tsx:./app/components/AccordionBlock.tsx
import {PortableText} from '@portabletext/react';
import type {Accordion} from '~/sanity/sanity.types';

export function AccordionBlock(props: Accordion) {
  return Array.isArray(props.groups) ? (
    <div className="flex flex-col bg-blue-50 divide-y divide-blue-100">
      {props.groups.map((group) => (
        <details key={group._key} className="p-4 w-full max-w-lg mx-auto">
          <summary className="font-bold py-2">{group.title}</summary>
          {Array.isArray(group.body) ? (
            <PortableText value={group.body} />
          ) : null}
        </details>
      ))}
    </div>
  ) : null;
}
```

> [!TIP]
> Take note of how *defensive coding* practices check any value is not `null` before attempting to render it. This is especially useful when your front end leverages Visual Editing and you are viewing incomplete, draft documents.



To render this block, add it to the `BLOCKS` object back in the page route.



- [ ] **Update** `BLOCKS` in the page route


```typescript:./app/routes/$slug.tsx
import {AccordionBlock} from '~/components/AccordionBlock';

const BLOCKS: Record<string, (props: any) => JSX.Element | null> = {
  accordion: AccordionBlock,
  _unknown: (props: any) => <>{JSON.stringify(props)}</>,
};
```

You should now see styled, collapsible drop downs for the accordion block type in your Hydrogen project.



Now you've got a handle on taking an array of block content created in Sanity and rendering them in Hydrogen, let's take on something a little more advanced.



### A note on reusable content



If your accordion is being used for FAQ's, and those FAQ's may be relevant in multiple locations – you should consider creating a dedicated `faq` document type and making this accordion block an array of references.



---

## Lesson 3: Next block: Curated products with references
https://www.sanity.io/learn/course/editorialized-ecommerce-experiences/next-block-curated-products

Resolve deeply nested references in your GROQ query and create a polymorphic component to render your content.

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.



## Prepare your content



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:



![Sanity Studio showing two product references](https://cdn.sanity.io/images/3do82whm/next/705f0c5f63821e3f280f63c021a7b158799a9580-2144x1388.png)

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.



## Resolving references



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.


```typescript:./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.



### Rendering the products block



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


```tsx:./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


```tsx:./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.



![Two product cards shown inside Sanity Studio's Presentation tool](https://cdn.sanity.io/images/3do82whm/next/e38cd78186be2403af0097ce5035c3765fa6d976-2144x1388.png)

### A note on visual controls in structured content 



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.







---

## Related Resources

- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
