# Course: Build landing pages with Next.js
https://www.sanity.io/learn/course/page-building

Give your content authors the creative freedom they need to produce landing pages by assembling individual blocks while still benefitting from structured content.

---

## Navigation

**Track:** [Work-ready Next.js](https://www.sanity.io/learn/track/work-ready-next-js) · [View as markdown](https://www.sanity.io/learn/track/work-ready-next-js.md)

## Contents

1. [An introduction to page builders](https://www.sanity.io/learn/course/page-building/an-introduction-to-page-builders) · [markdown](https://www.sanity.io/learn/course/page-building/an-introduction-to-page-builders.md)
2. [Create page builder schema types](https://www.sanity.io/learn/course/page-building/create-page-builder-schema-types) · [markdown](https://www.sanity.io/learn/course/page-building/create-page-builder-schema-types.md)
3. [Improve authoring with previews and thumbnails](https://www.sanity.io/learn/course/page-building/improved-ui-with-previews-and-thumbnails) · [markdown](https://www.sanity.io/learn/course/page-building/improved-ui-with-previews-and-thumbnails.md)
4. [Render pages](https://www.sanity.io/learn/course/page-building/rendering-pages) · [markdown](https://www.sanity.io/learn/course/page-building/rendering-pages.md)
5. [Render page builder blocks](https://www.sanity.io/learn/course/page-building/rendering-page-builder-blocks) · [markdown](https://www.sanity.io/learn/course/page-building/rendering-page-builder-blocks.md)
6. [Creating a "home" page](https://www.sanity.io/learn/course/page-building/creating-a-home-page) · [markdown](https://www.sanity.io/learn/course/page-building/creating-a-home-page.md)
7. [Drag and drop in Visual Editing](https://www.sanity.io/learn/course/page-building/drag-and-drop-in-visual-editing) · [markdown](https://www.sanity.io/learn/course/page-building/drag-and-drop-in-visual-editing.md)
8. [Scaling page builders and pitfalls](https://www.sanity.io/learn/course/page-building/scaling-page-builders-and-pitfalls) · [markdown](https://www.sanity.io/learn/course/page-building/scaling-page-builders-and-pitfalls.md)

---

## Lesson 1: An introduction to page builders
https://www.sanity.io/learn/course/page-building/an-introduction-to-page-builders

Setup your page builder the right way with Sanity and Next.js, understanding the process and best practices. With editing affordances your content creators will understand and appreciate.

By the end of this course, you'll have a robust page builder that allows you to generate pages with a set of reusable "blocks". Not only will you learn how to implement new blocks but you'll understand every step of the process, and ensure best practices for your editors.



But before we get into things, why don't we quickly discuss what a page builder *is*? Dependent on your experience, you may have a range of different expectations as to what a page builder actually is.



## What is a page builder?



Think of a page builder as a set of stackable LEGO blocks, where each block is a piece of content that can be used to build a page. This is an apt description, because it's rare that a page will go left and right, it'll almost always go from top to bottom.



> [!NOTE]
> The drag-and-drop functionality in Sanity's Visual Editing can be used to move items that are laid out horizontally, even if the same content is represented vertically in the Sanity Studio.



In simple terms, described as Sanity Schema Types, a page builder is an array of objects.



With this in mind, a page builder allows you to move blocks vertically on a page, and to add and remove blocks on a page. If you build a new component for it, it can then be used on any page and added to the selection of blocks within your page builder.



### Keep an open mind



While this course focuses primarily on page building for web pages, the same approach of being able to create an ordered list of different shapes of content may also be used in other front ends like applications.



> [!NOTE]
> **Consider this: **The Portable Text Editor is also "just" an array of objects. And yet its purpose can conjure a very different idea of what content is suitable to enter into it, and how it may be presented!



As much as possible, your page builder should be modeling **content**, not **presentation**. While rare, consider future opportunities where other applications may consume this same content and display it in a different way, or a different context, with different meaning.



## Why is it important?



With a page builder, you're allowing your content team to build pages without relying on developers. They can change the order of content, use repeatable components to create consistency and ultimately generate new pages in a fraction of the time.



What's very useful about the above, is the fact that when you build a new block, your whole team has access to that block to use throughout any of the pages that use the page builder.



## What problems does it solve?



- **Speed**: Content teams can build pages in a fraction of the time.

- **Consistency**: Repeatable components can be used to create consistency across pages.

- **Flexibility**: Content teams can change the order of content and add and remove blocks from a page.


## What problems does it create?



You have to think about how you will structure your page builder. Whether you use a single page builder throughout all of your pages, or whether you want to have specific page builders for different pages.



You shouldn't use it for something like a blog, where almost all blog posts follow the same formulaic structure of a big chunk of text with a few images.



Always remember, as soon as you add a page builder, you're handing over the reins for page layout to your content team, and a lack of rigidity can result in inconsistent user experiences.



Over time your authors may have many requests for unique blocks, and your page builder becomes difficult to maintain or understand. A strict adherence to consistency can reduce the likelihood of "runaway schema."



## About the author



My name is Jono, and I'm the founder of [Roboto Studio](https://robotostudio.com/?utm-source=sanity-learn). I have been building websites for many years, focusing on delivering the best editorial experiences with cutting-edge technologies. I wrote this course to simplify the process of creating a page builder with Sanity and Next.js.



The goal is to provide a straightforward course on crafting a solid page builder that allows your end users to have the best editorial experience. After years of building websites with Sanity and Next.js, I wrote this course based on what I wish I had learned when I first started building.



Throughout this course, you will learn the process of building blocks and the positive and negative implications of content modeling decisions. By the end of the course you will be able to design and manage page builder blocks easily and have an understanding of the best practices.



Now, let's go ahead and build the page builder blocks.



---

## Lesson 2: Create page builder schema types
https://www.sanity.io/learn/course/page-building/create-page-builder-schema-types

Setup the initial "blocks" of content and set the foundation of your page builder schema types.

> [!NOTE]
> The schema types you'll add in this lesson follow on from those created in the [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) course.



The choices you make at this stage will determine the efficiency of content creation. Keeping your schema simple and well-structured is the key to effortless authoring.



## Learning objectives



By the end of this lesson, you'll be able to:



- Structure a page builder

- Implement new blocks

- Understand when to use references vs objects


## Setting up a page builder



The page builder is typically an [array of](https://www.sanity.io/docs/array-type) [`object`](https://www.sanity.io/docs/array-type) or [`reference`](https://www.sanity.io/docs/array-type) types that can be reordered. It's the container for all your building blocks. With Sanity, there are no pre-built blocks, but it's fast and easy to create what you need.



- If you use **objects**, the content is simpler to query but trapped within the document.

- If you use **references**, the content can be reused between documents, and your queries must resolve them.


### Create the block schema types



Let's start by creating your first `pageBuilder` block. Note that this example uses an object type to create the block. 



Since you are new to page builders, the following example will start with objects, and the reasons for this choice will [be explained later](https://www.sanity.io/learn/course/page-building/scaling-page-builders-and-pitfalls#s-2e72d1b5ac82).



The next step is to add a `splitImage` block, a simple layout with text on one side and an image on the other, either left or right. You've definitely seen this block on many websites.



[Here's a link for what this block could look like](https://v0.dev/chat/zwZtgP2aDSB?b=b_LmbyJ1dlp4v)



- [ ] **Create** the `splitImage` block


```typescript:src/sanity/schemaTypes/blocks/splitImageType.ts
import { defineField, defineType } from "sanity";

export const splitImageType = defineType({
  name: "splitImage",
  type: "object",
  fields: [
    defineField({
      name: "orientation",
      type: "string",
      options: {
        list: [
          { value: "imageLeft", title: "Image Left" },
          { value: "imageRight", title: "Image Right" },
        ],
      },
    }),
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "image",
      type: "image",
    }),
  ],
  preview: {
    select: {
      title: "title",
      media: "image",
    },
    prepare({ title, media }) {
      return {
        title,
        subtitle: "Text and Image",
        media
      };
    },
  },
});
```

Now, let's add a `hero` block. This is a simple block with a title text and image. Despite the schemas looking very similar, you would usually have a hero at the top of a page, so it's a good idea to have a dedicated block for it.



You may have noticed that the code snippets use a block field for the text. This is known as portable text, a powerful way to render rich text within Sanity. While it's more complex than a simple string, its flexibility makes it incredibly useful.



> [!TIP]
> See [Presenting Portable Text](https://www.sanity.io/learn/developer-guides/presenting-block-text) in the documentation for more details



[Here's a link for what this block could look like](https://v0.dev/chat/MiPurXiE59K?b=b_gUmeXji6heI)



- [ ] **Create** the `hero` block


```typescript:src/sanity/schemaTypes/blocks/heroType.ts
import { defineField, defineType } from "sanity";

export const heroType = defineType({
  name: "hero",
  type: "object",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "text",
      type: "blockContent",
    }),
    defineField({
      name: "image",
      type: "image",
    }),
  ],
});
```

> [!WARNING]
> Getting an error with `blockContent` missing? The schema types you'll add in this lesson follow on from those created in the [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) course.



An "FAQ Block" is an ideal block for using references, allowing the same document to be reused in multiple places.



If you have a list of FAQs that you want to show on multiple pages, you can create a single FAQ document and reference it from each page rather than duplicating the FAQ content. This makes it easier to maintain since you only need to update the content in one place.



Let's create a FAQ document type and reference it in a block. First, let's create the FAQ document schema:



In this example, the name of the block is `faqBlock`



[Here's a link for what this block could look like](https://v0.dev/chat/zl4hrYbDzOg?b=b_lTKHW2wTTS4)



- [ ] **Create** the `faq` document schema type


```typescript:src/sanity/schemaTypes/faqType.ts
import { defineField, defineType } from "sanity";

export const faqType = defineType({
  name: "faq",
  title: "FAQ",
  type: "document",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "body",
      type: "blockContent",
    }),
  ],
});
```

- [ ] **Create** the `faqAccordion` block, which will reference the FAQ document.


```typescript:src/sanity/schemaTypes/blocks/faqsType.ts
import { defineField, defineType } from "sanity";

export const faqsType = defineType({
  name: "faqs",
  title: "FAQs",
  type: "object",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "faqs",
      title: "FAQs",
      type: "array",
      of: [{ type: "reference", to: [{ type: "faq" }] }],
    }),
  ],
});
```

Finally, one more block. This one is a features block, and this is going to get a little more complex with an array of features (as a block) inside a block.



[Here's a link for what this block could look like](https://v0.dev/chat/z0zxFeAPm1z?b=b_ZpymafJ4kCT)



- [ ] **Create** the `features` block


```typescript:src/sanity/schemaTypes/blocks/featuresType.ts
import { defineField, defineType } from "sanity";

export const featuresType = defineType({
  name: "features",
  type: "object",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "features",
      type: "array",
      of: [
        defineField({
          name: "feature",
          type: "object",
          fields: [
            defineField({
              name: "title",
              type: "string",
            }),
            defineField({
              name: "text",
              type: "string",
            }),
          ],
        }),
      ],
    }),
  ],
});
```

### Create the page builder schema type



Okay great, you have got your blocks. Now let's put them together in your page builder component. The order of the blocks is how it will appear when a user adds a new block to the array. 



- [ ] **Create** the `pageBuilder` schema type


```typescript:src/sanity/schemaTypes/pageBuilderType.ts
import { defineType, defineArrayMember } from "sanity";
 
export const pageBuilderType = defineType({
  name: "pageBuilder",
  type: "array",
  of: [
    defineArrayMember({ type: "hero" }),
    defineArrayMember({ type: "splitImage" }),
    defineArrayMember({ type: "features" }),
    defineArrayMember({ type: "faqs" }),
  ],
});
```

### Create the page document type



The schema types in your Sanity Studio for now are only useful for writing blog posts. The pages being built with this schema have a different intention to those, and so should be stored in a distinct schema type.



- [ ] **Create** a `page` document schema type


```typescript:src/sanity/schemaTypes/pageType.ts
import { DocumentIcon } from "@sanity/icons";
import { defineField, defineType } from "sanity";

export const pageType = defineType({
  name: "page",
  title: "Page",
  type: "document",
  icon: DocumentIcon,
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "slug",
      type: "slug",
      options: {
        source: "title",
      },
    }),
    defineField({
      name: "content",
      type: "pageBuilder",
    }),
    defineField({
      name: "mainImage",
      type: "image",
      options: {
        hotspot: true,
      },
    }),    
  ],
  preview: {
    select: {
      title: "title",
      subtitle: "slug.current",
    },
  },
});
```

### Add your new types to the Studio schema



Finally, update the schema types index file to import all of these newly created schema types.



- [ ] **Update** your registered schema types


```typescript:src/sanity/schemaTypes/index.ts
// ...all your existing imports
import { pageType } from "./pageType";
import { pageBuilderType } from "./pageBuilderType";
import { faqType } from "./faqType";
import { faqsType } from "./blocks/faqsType";
import { featuresType } from "./blocks/featuresType";
import { heroType } from "./blocks/heroType";
import { splitImageType } from "./blocks/splitImageType";

export const schema: { types: SchemaTypeDefinition[] } = {
  types: [
    // ...all your existing schema types
    pageType,
    pageBuilderType,
    faqType,
    faqsType,
    featuresType,
    heroType,
    splitImageType,
  ],
};
```

### Update Studio Structure



The desk structure will now include `page` and `faq` type documents, but won't display them nicely with plurals.



- [ ] **Update** the Studio's structure configuration


```typescript:src/sanity/structure.ts
import type { StructureResolver } from "sanity/structure";

// https://www.sanity.io/docs/structure-builder-cheat-sheet
export const structure: StructureResolver = (S) =>
  S.list()
    .title("Blog")
    .items([
      S.documentTypeListItem("post").title("Posts"),
      S.documentTypeListItem("category").title("Categories"),
      S.documentTypeListItem("author").title("Authors"),
      S.divider(),
      S.documentTypeListItem("page").title("Pages"),
      S.documentTypeListItem("faq").title("FAQs"),
      S.divider(),
      ...S.documentTypeListItems().filter(
        (item) =>
          item.getId() &&
          !["post", "category", "author", "page", "faq"].includes(item.getId()!)
      ),
    ]);
```

## Check it's working



With all of these new schema types registered you should now be able to create Page type documents with the Page Builder field allowing you to add any one of four blocks.



![Sanity Studio showing a page being edited with a block selector menu open](https://cdn.sanity.io/images/3do82whm/next/325dc7ddd53dfa0c57f833c81537e49e9497a4b9-2240x1480.png)

Now that your page builder schema is set up, all the fundamental building blocks are in place. Next, you can add new blocks, reorder them, and update the array as needed.



---

## Lesson 3: Improve authoring with previews and thumbnails
https://www.sanity.io/learn/course/page-building/improved-ui-with-previews-and-thumbnails

Updates to the configuration of your page builder schema types can dramatically improve the content creation experience.

Next, you will enhance the user interface (UI) with previews, thumbnails, and filters. These additions will help editors quickly find and use the blocks they need to create pages.



Previews are the snippets of information that appear in the list view of the page builder. They're the first thing your editors will see when they're adding a new block to the page builder.



Here's an example of what a *good* preview looks like:



![Image](https://cdn.sanity.io/images/3do82whm/next/13884f45681aae33d08e747c953622cad5ae7694-1346x1286.webp)

## What makes a good preview?



Previews should always have consistency. Consistency creates familiarity and familiarity improves user experience. The more consistency you have in your previews and page builders, the faster it will be for your editors to create pages.



`object` and `document` schema types in Sanity Studio have a preview property which allow the following to be customized:



- `title`: This is the title of the block, or the most important headline. Think what section a marketer would care about the most.

- `subtitle`: Set this to the block name.

- `media`: If the block has an image, use that, otherwise use an icon as a fallback.


Let's revisit our blocks from the last lesson and improve the readability.



Note, this lesson uses the [default icons from Sanity](https://icons.sanity.build/) to minimize dependencies. However, in a real-world project, [Lucide](https://lucide.dev/) may be preferred as it has a larger icon selection.



## Using prepare and preview



Pay attention to the `preview` and `prepare` functions. This is where you are defining how the block appears in the preview.



In the example below, there is a block with a title, subtitle and media.



![Image](https://cdn.sanity.io/images/3do82whm/next/8a3ca601a1b52ef91aa885e970c2b6ccc83339fa-2864x554.png)

The pink cat on the left-hand side is the `media`, this can either be an image, or an icon. However, you should never leave it blank.



- [ ] **Update** the `splitImage` schema type to include an `icon` and `preview`


```typescript:src/sanity/schemaTypes/blocks/splitImageType.ts
import { defineField, defineType } from "sanity";
import { BlockContentIcon } from "@sanity/icons";

export const splitImageType = defineType({
  name: "splitImage",
  // ...all other settings
  icon: BlockContentIcon,
  preview: {
    select: {
      title: "title",
      media: "image",
    },
    prepare({title, media}) {
      return {
        title: title,
        subtitle: "Split Image",
        media: media ?? BlockContentIcon,
      };
    },
  },
});

```

Insert a "Split Image" block and give it some content. The preview should now look like this:



![Image](https://cdn.sanity.io/images/3do82whm/next/169973097460feecb773977cd3a896db509387bd-2240x1488.png)

This example is a simple block with a title, subtitle and media. The `prepare` function is used to set the title and media for the preview.



- [ ] **Update** the `hero` type block


```typescript:src/sanity/schemaTypes/blocks/heroType.ts
import { defineField, defineType } from "sanity";
import { TextIcon } from "@sanity/icons";

export const heroType = defineType({
  name: "hero",
  // ...other settings
  icon: TextIcon,
  preview: {
    select: {
      title: "title",
      media: "image",
    },
    prepare({ title, media }) {
      return {
        title,
        subtitle: "Hero",
        media: media ?? TextIcon,
      };
    },
  },
});
```

- [ ] **Update** the faqs type block


```typescript:src/sanity/schemaTypes/blocks/faqsType.ts
import { defineField, defineType } from "sanity";
import { HelpCircleIcon } from "@sanity/icons";

export const faqsType = defineType({
  name: "faqs",
  // ...other settings
  icon: HelpCircleIcon,
  preview: {
    select: {
      title: "title",
    },
    prepare({ title }) {
      return {
        title,
        subtitle: "FAQs",
      };
    },
  },
});
```

- [ ] **Update** the features type block


```typescript:src/sanity/schemaTypes/blocks/featuresType.ts
import { defineField, defineType } from "sanity";
import { StarIcon } from "@sanity/icons";

export const featuresType = defineType({
  name: "features",
  // ...other settings
  icon: StarIcon,
  preview: {
    select: {
      title: "title",
    },
    prepare({ title }) {
      return {
        title,
        subtitle: "Features",
      };
    },
  },
});
```

## Adding thumbnails



Customized icons are good, but a visual preview of what a block can look like is even better.



To implement these previews, you need to update your page builder schema. For this particular example, you'll add a `grid` view to the `options` property.



This creates a grid view of the block, and the preview image is taken from the `static` folder.



It should look something like this:



![Sanity Studio showing the "add item" picker with thumbnails](https://cdn.sanity.io/images/3do82whm/next/a041688bb996a45ef745f89b43e74d3f9711d8d5-2240x1480.png)

- [ ] **Update** the page builder schema type


```typescript:src/sanity/schemaTypes/pageBuilderType.ts
export const pageBuilderType = defineType({
  // ... previous configuration
  options: {
    insertMenu: {
      views: [
        {
          name: "grid",
          previewImageUrl: (schemaType) => `/block-previews/${schemaType}.png`,
        },
      ],
    },
  },
});
```

### Create your own thumbnails



Next.js provides a public folder in the root of your application for serving static images. You could place your own images in this directory, ensuring they adhere to the following specifications:



- Dimensions: 600x400px (maintain consistent sizing)

- Format: PNG with transparent background

- Naming: Match schema type names (e.g., `hero.png`, `splitImage.png`)


There is a community file built for designing these, [it is available here](https://www.figma.com/community/file/1404904715260176924/arrayfield-template-for-sanity).



### Pre-designed thumbnails



For the blocks in this lesson, we have prepared some example thumbnails you can use. Download these example images and place them in your application at `/public/block-previews`



- [ ] **Download** the example thumbnails, and extract them into a `/public/block-previews` directory


Click the "Add item" button now and you should see the preview images.



It's time to finally start getting this content to show up in your Next.js application. First let's create a new dynamic route for rendering page documents.



---

## Lesson 4: Render pages
https://www.sanity.io/learn/course/page-building/rendering-pages

Create a new dynamic route to render "page" documents and create links to them within Sanity Studio for an interactive live preview within Presentation.

You've created your perfect schema, improved your editorial experience by adding thumbnails and now it's time to get your page builder blocks wired up on the frontend.



## Learning objectives



Now that you've created "page" type documents in the Studio, you'll need the Next.js application to query them at a dynamic route.



By the end of this chapter, you'll be able to:



- Query for and render any page by its slug


## Query a page



You are going to create a query to fetch the page and its page builder content from the Content Lake. 



Let's break down what's happening in this query.



The `->` operator in Sanity is used for dereferencing documents. When you have a reference to another document (like our FAQs), by default you only get the reference ID. Adding `->` tells Sanity to "follow" that reference and include the full content of the referenced document in your query results. This is particularly useful when you need the actual content immediately and want to avoid making multiple separate queries.



- [ ] **Update** the file with all your queries to include one for an individual page:


```typescript:src/sanity/lib/queries.ts
// ...all other queries

export const PAGE_QUERY =
  defineQuery(`*[_type == "page" && slug.current == $slug][0]{
  ...,
  content[]{
    ...,
    _type == "faqs" => {
      ...,
      faqs[]->
    }
  }
}`);
```

This query also demonstrates a shorthand syntax of the GROQ function `select()`, by which you can handle individual blocks differently by checking their `_type`.



Since you've changed schema types and queries, it's time to regenerate Types as well.



```sh
npm run typegen
```

> [!NOTE]
> This command was setup in the [Generate TypeScript Types](https://www.sanity.io/learn/course/content-driven-web-application-foundations/generate-typescript-types) lesson of the [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) course.



## Render a page



Before rendering individual blocks, the Next.js application needs a route to  render any individual page.



- [ ] **Create** a new route for rendering any page document by its unique slug.


```tsx:src/app/(frontend)/[slug]/page.tsx
import { sanityFetch } from "@/sanity/lib/live";
import { PAGE_QUERY } from "@/sanity/lib/queries";

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { data: page } = await sanityFetch({
    query: PAGE_QUERY,
    params: await params,
  });

  return <div>{JSON.stringify(page)}</div>;
}
```

## Update the Presentation tool resolver



To create a link between your Sanity Studio documents and their locations in the front-end, update the resolve function created for your Presentation tool to generate dynamic links to your live preview.



- [ ] **Update** the document locations resolver


```typescript:src/sanity/presentation/resolve.ts
import {
  defineLocations,
  PresentationPluginOptions,
} from "sanity/presentation";

export const resolve: PresentationPluginOptions["resolve"] = {
  locations: {
    // ...other locations
    page: defineLocations({
      select: {
        title: "title",
        slug: "slug.current",
      },
      resolve: (doc) => ({
        locations: [
          {
            title: doc?.title || "Untitled",
            href: `/${doc?.slug}`,
          },
        ],
      }),
    }),
  },
};
```

You should now be able to open any page in Presentation by clicking the "Used on one page" link at the top of the document editor.



![Sanity Studio showing live preview of a new page document](https://cdn.sanity.io/images/3do82whm/next/4d769ae7eedca4c869b826cfdec32d215886e7d4-2240x1488.png)

Now you can create pages in Sanity Studio and preview them live in the Presentation tool. The next step is to render each block as a unique component.



---

## Lesson 5: Render page builder blocks
https://www.sanity.io/learn/course/page-building/rendering-page-builder-blocks

Setup the unique components for each individual "block" to render on the page.

The example components in this lesson have been given deliberately simple designs. Feel free to redesign them with much more *flair*.



You'll notice also the props for each component has been typed from the `PAGE_QUERYResult` generated from Sanity TypeGen. The type itself looks *quite* gnarly, but it will be constantly updated as you make future changes to your schema types and queries.



## Create block components



- [ ] **Create** a component to render the Hero block


```tsx:src/components/blocks/hero.tsx
import { PortableText } from "next-sanity";
import Image from "next/image";
import { Title } from "@/components/title";
import { urlFor } from "@/sanity/lib/image";
import { PAGE_QUERYResult } from "@/sanity/types";

type HeroProps = Extract<
  NonNullable<NonNullable<PAGE_QUERYResult>["content"]>[number],
  { _type: "hero" }
>;

export function Hero({ title, text, image }: HeroProps) {
  return (
    <section className="isolate w-full aspect-[2/1] py-16 relative overflow-hidden">
      <div className="relative flex flex-col justify-center items-center gap-8 h-full z-20">
        {title ? (
          <h1 className="text-2xl md:text-4xl lg:text-6xl font-semibold text-white text-pretty max-w-3xl">
            {title}
          </h1>
        ) : null}
        <div className="prose-lg lg:prose-xl prose-invert flex items-center">
          {text ? <PortableText value={text} /> : null}
        </div>
      </div>
      <div className="absolute inset-0 bg-pink-500 opacity-50 z-10" />
      {image ? (
        <Image
          className="absolute inset-0 object-cover blur-sm"
          src={urlFor(image).width(1600).height(800).url()}
          width={1600}
          height={800}
          alt=""
        />
      ) : null}
    </section>
  );
}
```

- [ ] **Create** a component to render the FAQs block


```tsx:src/components/blocks/faqs.tsx
import { PAGE_QUERYResult } from "@/sanity/types";
import { PortableText } from "next-sanity";

type FAQsProps = Extract<
  NonNullable<NonNullable<PAGE_QUERYResult>["content"]>[number],
  { _type: "faqs" }
>;

export function FAQs({ _key, title, faqs }: FAQsProps) {
  return (
    <section className="container mx-auto flex flex-col gap-8 py-16">
      {title ? (
        <h2 className="text-xl mx-auto md:text-2xl lg:text-5xl font-semibold text-slate-800 text-pretty max-w-3xl">
          {title}
        </h2>
      ) : null}
      {Array.isArray(faqs) ? (
        <div className="max-w-2xl mx-auto border-b border-pink-200">
          {faqs.map((faq) => (
            <details
              key={faq._id}
              className="group [&[open]]:bg-pink-50 transition-colors duration-100 px-4 border-t border-pink-200"
              name={_key}
            >
              <summary className="text-xl font-semibold text-slate-800 list-none cursor-pointer py-4 flex items-center justify-between">
                {faq.title}
                <span className="transform origin-center rotate-90 group-open:-rotate-90 transition-transform duration-200">
                  &larr;
                </span>
              </summary>
              <div className="pb-4">
                {faq.body ? <PortableText value={faq.body} /> : null}
              </div>
            </details>
          ))}
        </div>
      ) : null}
    </section>
  );
}
```

- [ ] **Create** a component to render the Features block


```tsx:src/components/blocks/features.tsx
import { PAGE_QUERYResult } from "@/sanity/types";

type FeaturesProps = Extract<
  NonNullable<NonNullable<PAGE_QUERYResult>["content"]>[number],
  { _type: "features" }
>;

export function Features({ features, title }: FeaturesProps) {
  return (
    <section className="container mx-auto flex flex-col gap-8 py-16">
      {title ? (
        <h2 className="text-xl mx-auto md:text-2xl lg:text-5xl font-semibold text-slate-800 text-pretty max-w-3xl">
          {title}
        </h2>
      ) : null}

      {Array.isArray(features) ? (
        <div className="grid grid-cols-3 gap-8">
          {features.map((feature) => (
            <div key={feature._key} className="flex flex-col gap-4">
              <h3 className="text-xl font-semibold text-slate-800">
                {feature.title}
              </h3>
              <p className="text-lg text-slate-600">{feature.text}</p>
            </div>
          ))}
        </div>
      ) : null}
    </section>
  );
}
```

- [ ] **Create** a component to render the Split Image block


```tsx:src/components/blocks/split-image.tsx
import Image from "next/image";
import { urlFor } from "@/sanity/lib/image";
import { PAGE_QUERYResult } from "@/sanity/types";
import { stegaClean } from "next-sanity";

type SplitImageProps = Extract<
  NonNullable<NonNullable<PAGE_QUERYResult>["content"]>[number],
  { _type: "splitImage" }
>;

export function SplitImage({ title, image, orientation }: SplitImageProps) {
  return (
    <section
      className="container mx-auto flex gap-8 py-16 data-[orientation='imageRight']:flex-row-reverse"
      data-orientation={stegaClean(orientation) || "imageLeft"}
    >
      {image ? (
        <Image
          className="rounded-xl w-2/3 h-auto"
          src={urlFor(image).width(800).height(600).url()}
          width={800}
          height={600}
          alt=""
        />
      ) : null}
      <div className="w-1/3 flex items-center">
        {title ? (
          <h2 className="text-3xl mx-auto md:text-5xl lg:text-8xl font-light text-pink-500 text-pretty max-w-3xl">
            {title}
          </h2>
        ) : null}
      </div>
    </section>
  );
}
```

## Render the page builder content



Now we have components for each block, we need to render them in order. 



Each array item has a distinct `_type` attribute, which you can switch over to render the correct component.



Each item also contains a unique (to the array) `_key` value, which can be passed to React as a `key` prop—required by React for performant and consistent rendering of an array. 



We have also passed the remaining props to the block component using the spread operator.



- [ ] **Create** the `PageBuilder` component to render all the content of the page


```tsx:src/components/page-builder.tsx
import { Hero } from "@/components/blocks/hero";
import { Features } from "@/components/blocks/features";
import { SplitImage } from "@/components/blocks/split-image";
import { FAQs } from "@/components/blocks/faqs";
import { PAGE_QUERYResult } from "@/sanity/types";

type PageBuilderProps = {
  content: NonNullable<PAGE_QUERYResult>["content"];
};

export function PageBuilder({ content }: PageBuilderProps) {
  if (!Array.isArray(content)) {
    return null;
  }

  return (
    <main>
      {content.map((block) => {
        switch (block._type) {
          case "hero":
            return <Hero key={block._key} {...block} />;
          case "features":
            return <Features key={block._key} {...block} />;
          case "splitImage":
            return <SplitImage key={block._key} {...block} />;
          case "faqs":
            return <FAQs key={block._key} {...block} />;
          default:
            // This is a fallback for when we don't have a block type
            return <div key={block._key}>Block not found: {block._type}</div>;
        }
      })}
    </main>
  );
}
```

- [ ] **Update** the dynamic page route to use the `PageBuilder` component


```tsx:src/app/(frontend)/[slug]/page.tsx
import { PageBuilder } from "@/components/page-builder";
import { sanityFetch } from "@/sanity/lib/live";
import { PAGE_QUERY } from "@/sanity/lib/queries";

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { data: page } = await sanityFetch({
    query: PAGE_QUERY,
    params: await params,
  });

  return page?.content ? <PageBuilder content={page.content} /> : null;
}
```

You should now be able to create page documents, use all of the blocks from the Page Builder array we have created, and preview changes as you author them.



![Sanity Studio Presentation tool showing a website layout](https://cdn.sanity.io/images/3do82whm/next/24b3f7de0a18f9708281c266ec86366427261ebf-2240x1480.png)

For now you have click-to-edit functionality in Presentation. Before going any further, let's use everything we've built so far to create the application's home page.



---

## Lesson 6: Creating a "home" page
https://www.sanity.io/learn/course/page-building/creating-a-home-page

Create a "singleton" document to store distinct content that is globally relevant to the application.

A quick side mission before going further. A website's "home" page is typically used to show the same sort of content that our page builder can generate. So it makes sense to reuse this content structure on the home page like we would any other page.



For any editable piece of content that has global relevance to an application—like site name, navigation, footer text, etc—most often you will use a "singleton" document. That is, a document type of which there should only ever be one in a dataset and it likely has a distinct `_id` value.



## Site settings schema type



We'll keep the site settings simple for now. A new document type with just a single field—a reference to a page, which will be used as the home page on the site.



- [ ] **Create** the `siteSettings` schema type


```typescript:src/sanity/schemaTypes/siteSettingsType.ts
import { defineField, defineType } from "sanity";
import { ControlsIcon } from "@sanity/icons";

export const siteSettingsType = defineType({
  name: "siteSettings",
  title: "Site Settings",
  type: "document",
  icon: ControlsIcon,
  fields: [
    defineField({
      name: "homePage",
      type: "reference",
      to: [{ type: "page" }],
    }),
  ],
  preview: {
    prepare() {
      return {
        title: "Site Settings",
      };
    },
  },
});
```

- [ ] **Register** `siteSettings` to your Studio schema types


```typescript:src/sanity/schemaTypes/index.ts
// ...all other imports
import { siteSettingsType } from "./siteSettingsType";

export const schema: { types: SchemaTypeDefinition[] } = {
  types: [
    // ...all other types
    siteSettingsType,
  ],
};
```

Singleton documents can be invoked with a distinct `_id` value by configuring it in your structure builder configuration.



**Update** the structure builder configuration to include a singleton siteSettings document.



```typescript:src/sanity/structure.ts
export const structure: StructureResolver = (S) =>
  S.list()
    .title("Blog")
    .items([
      // ...all other items
      S.listItem()
        .id("siteSettings")
        .schemaType("siteSettings")
        .title("Site Settings")
        .child(
          S.editor()
            .id("siteSettings")
            .schemaType("siteSettings")
            .documentId("siteSettings")
        ),
      ...S.documentTypeListItems().filter(
        (item) =>
          item.getId() &&
          ![
            // ...all other ignored types
            "siteSettings",
          ].includes(item.getId()!)
      ),
    ]);
```

> [!TIP]
> See more examples of what you can do in the [Structure Builder cheat sheet](https://www.sanity.io/learn/studio/structure-builder-cheat-sheet)



You should now see the Site Settings document on the left hand side of your Structure tool. Instead of opening a list of documents, it opens a single one.



- [ ] **Select** a "Home Page" reference, and **publish** the Site Settings


### There can only be one



To prevent the creation of any more site settings documents, the type can be removed from the "Create" menu at the top left of your Studio.



- [ ] **Update** your Studio config to remove this document type from the list 


```typescript:sanity.config.ts
export default defineConfig({
  // ...all other settings
  document: {
    newDocumentOptions: (prev) => prev.filter((item) => item.templateId !== "siteSettings"),
  },
});

```

## Query the home page



The current "page" document type query relies on a page slug, so you'll need a different query for this site settings document first, and then query that page.



- [ ] **Update** your `queries.ts` file to include this home page query


```typescript:src/sanity/lib/queries.ts
// ...all other queries

export const HOME_PAGE_QUERY = defineQuery(`*[_id == "siteSettings"][0]{
    homePage->{
      ...,
      content[]{
        ...,
        _type == "faqs" => {
          ...,
          faqs[]->
        }
      }      
    }
  }`);
```

- [ ] **Run** the following command to update your schema extraction and generated types


```sh:Terminal
pnpm run typegen
```

> [!NOTE]
> This command was setup in the [Generate TypeScript Types](https://www.sanity.io/learn/course/content-driven-web-application-foundations/generate-typescript-types) lesson of the [Content-driven web application foundations](https://www.sanity.io/learn/course/content-driven-web-application-foundations) course.



Then update your home page route file similar to the dynamic route for individual pages, but for just this distinct home page.



- [ ] **Update** the home page route


```typescript:src/app/(frontend)/page.tsx
import { PageBuilder } from "@/components/page-builder";
import { sanityFetch } from "@/sanity/lib/live";
import { HOME_PAGE_QUERY } from "@/sanity/lib/queries";

export default async function Page() {
  const { data: page } = await sanityFetch({
    query: HOME_PAGE_QUERY,
  });

  return page?.homePage?.content ? (
    <PageBuilder content={page?.homePage.content} />
  ) : null;
}
```

The front page of your application at [http://localhost:3000](http://localhost:3000) should now show the page selected in your Site Settings document.



Excellent! You might now imagine how you would build other global content like your heading, footer and navigation menus into this same Site Settings document.



Let's keep enhancing the editing experience in the next lesson.



---

## Lesson 7: Drag and drop in Visual Editing
https://www.sanity.io/learn/course/page-building/drag-and-drop-in-visual-editing

Allow authors to re-order blocks on page, without editing the document.

The same functionality you setup in [Add drag-and-drop elements](https://www.sanity.io/learn/course/visual-editing-with-next-js/add-drag-and-drop-elements) can be used here for your Page Builder array. This way authors can reorder array items on the page without needing to use the document editor.



> [!NOTE]
> You can setup drag-and-drop for *any* array type field. Consider adding it to the Features and FAQs blocks as well.



## Adding drag handles



Drag-and-drop support in Presentation requires the outer DOM element of an array—and every DOM element for an item within the array—to contain additional `data-sanity` attributes. These attributes are created with a `createDataAttribute` function exported from `next-sanity` and require the ID and Type of the source document.



Additionally, for fast on-page changes, a `useOptimistic` hook is provided by `next-sanity`. Using this hook will require changing to a client component.



The `PageBuilder` component you created in a previous lesson is where we can create and set these attributes, for the `content` array and its individual blocks.



- [ ] **Update** your `PageBuilder` component to add attributes for drag-and-drop


```tsx:src/components/page-builder.tsx
"use client";

import { Hero } from "@/components/blocks/hero";
import { Features } from "@/components/blocks/features";
import { SplitImage } from "@/components/blocks/split-image";
import { FAQs } from "@/components/blocks/faqs";
import { PAGE_QUERYResult } from "@/sanity/types";
import { client } from "@/sanity/lib/client";
import { createDataAttribute } from "next-sanity";
import { useOptimistic } from "next-sanity/hooks";

type PageBuilderProps = {
  content: NonNullable<PAGE_QUERYResult>["content"];
  documentId: string;
  documentType: string;
};

const { projectId, dataset, stega } = client.config();
export const createDataAttributeConfig = {
  projectId,
  dataset,
  baseUrl: typeof stega.studioUrl === "string" ? stega.studioUrl : "",
};

export function PageBuilder({
  content,
  documentId,
  documentType,
}: PageBuilderProps) {
  const blocks = useOptimistic<
    NonNullable<PAGE_QUERYResult>["content"] | undefined,
    NonNullable<PAGE_QUERYResult>
  >(content, (state, action) => {
    if (action.id === documentId) {
      return action?.document?.content?.map(
        (block) => state?.find((s) => s._key === block?._key) || block
      );
    }
    return state;
  });

  if (!Array.isArray(blocks)) {
    return null;
  }

  return (
    <main
      data-sanity={createDataAttribute({
        ...createDataAttributeConfig,
        id: documentId,
        type: documentType,
        path: "content",
      }).toString()}
    >
      {blocks.map((block) => {
        const DragHandle = ({ children }: { children: React.ReactNode }) => (
          <div
            data-sanity={createDataAttribute({
              ...createDataAttributeConfig,
              id: documentId,
              type: documentType,
              path: `content[_key=="${block._key}"]`,
            }).toString()}
          >
            {children}
          </div>
        );

        switch (block._type) {
          case "hero":
            return (
              <DragHandle key={block._key}>
                <Hero {...block} />
              </DragHandle>
            );
          case "features":
            return (
              <DragHandle key={block._key}>
                <Features {...block} />
              </DragHandle>
            );
          case "splitImage":
            return (
              <DragHandle key={block._key}>
                <SplitImage {...block} />
              </DragHandle>
            );
          case "faqs":
            return (
              <DragHandle key={block._key}>
                <FAQs {...block} />
              </DragHandle>
            );
          default:
            // This is a fallback for when we don't have a block type
            return <div key={block._key}>Block not found: {block._type}</div>;
        }
      })}
    </main>
  );
}
```

The `PageBuilder` component now requires the source document ID and document type. 



- [ ] **Update** your routes that load this component to include these props.


```tsx:src/app/(frontend)/[slug]/page.tsx
import { PageBuilder } from "@/components/PageBuilder";
import { sanityFetch } from "@/sanity/lib/live";
import { PAGE_QUERY } from "@/sanity/lib/queries";

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { data: page } = await sanityFetch({
    query: PAGE_QUERY,
    params: await params,
  });

  return page?.content ? (
    <PageBuilder
      documentId={page._id}
      documentType={page._type}
      content={page.content}
    />
  ) : null;
}
```

Don't forget the home page route as well



```tsx:src/app/(frontend)/page.tsx
import { PageBuilder } from "@/components/PageBuilder";
import { sanityFetch } from "@/sanity/lib/live";
import { HOME_PAGE_QUERY } from "@/sanity/lib/queries";

export default async function Page() {
  const { data: page } = await sanityFetch({
    query: HOME_PAGE_QUERY,
  });

  return page?.homePage?.content ? (
    <PageBuilder
      documentId={page?.homePage._id}
      documentType={page?.homePage._type}
      content={page?.homePage.content}
    />
  ) : null;
}
```

### Test it out



Within Presentation you should now see the "drag handle" icon (two columns of three dots) when hovering over the outer edge of each block.



Click and hold, to drag-and-drop. Additionally, hold shift while dragging to zoom the page out and see the entire array at once.



You've created your page builder, wired it up to work on the frontend, and used the visual editor to rearrange block order.



You have built the gold standard of editorial experience for your end users. Great job!



## Time to review



What remains is to learn about some of the pitfalls and challenges of using the visual editor at scale, which will be covered in the final lesson.



---

## Lesson 8: Scaling page builders and pitfalls
https://www.sanity.io/learn/course/page-building/scaling-page-builders-and-pitfalls

How to keep your page builder tidy as your project grows over time.

Your page builder works just fine at this stage. But what happens when you have 20 more components, 100 more pages, and 10 more users? This lesson covers those questions and the pitfalls ahead.



## The pitfalls



### Don't include too many variations



If a block has too many variations, you're going to run into a lot of edge cases. It's also difficult to manage because you will have to pick a thumbnail as the `main` image for the block. If your variation is different from the `main` image it becomes confusing for the content team to differentiate between the variations.



Here's a good rule of thumb: if you have more than two variations of a block, you should consider splitting them into individual blocks.



### Is it a page builder or a document?



Think carefully about modeling your content as a page builder or a document type. If you want your content to be more rigid and you want to be able to reuse the same block in multiple places, then you should use a document type.



Alternatively, if you want your content to be reordered, have different layouts, or have different components, then you should use a page builder.



### Paradox of choice for marketers



To help your marketing team create pages efficiently, limit the number of block options available. Too many choices can confuse and overwhelm, leading to mistakes and delays in content creation.



For example, if a "features" block is unnecessary for a "case study," remove it from the options for that document type. This streamlines the process and makes it easier for new team members to navigate.



![Image](https://cdn.sanity.io/images/3do82whm/next/dc699183c3d94393b51f91fd1954093b2460ad32-1590x2078.png)

### Use references sparingly



This is the number one mistake I see in the wild.



Avoid making your entire page builder an array of references; it's more difficult to scale. The reason is that often, you won't need to use the same referenced block in multiple places.



One of the few exceptions is a repeated call to action where the text is identical or you might have two different versions that appear on many different pages. This is a good use case for references. If there is not a clear need to re-use content across many pages—use an object.



### Remember to prune



As you scale your website and your page builder, you will naturally have blocks that you no longer use. You should prune them regularly to eliminate technical debt.



If you do need to start removing blocks, consider the course [Handling schema changes confidently](https://www.sanity.io/learn/course/handling-schema-changes-confidently).



## Go deeper



What you've built in this course is a basic implementation using default settings. The dynamic nature of building Sanity Schema with TypeScript lends itself to opinionated abstractions. Take a look at these resources for some inspiration:



> [!TIP]
> [Vyuh Framework's "Structure Plugin"](https://docs.vyuh.tech/guides/sanity/structure-plugin/)



## Start building!



You've made it to the end of the course. You're now fully equipped to start building your own page builders with confidence. We hope this structured approach to page building will make content management simpler and more efficient for your team.



We'd love to see the page builders that you create, [tag us on X](https://x.com/sanity_io) or [join our community](https://slack.sanity.io/) to share any projects you create 



---

## Related Resources

- [Track overview](https://www.sanity.io/learn/track/work-ready-next-js.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
