Lesson
5
Render page builder blocks
Setup the unique components for each individual "block" to render on the page.
Log in to mark your progress for each Lesson and Task
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 a component to render the Hero block
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
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"> ← </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
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
src/components/blocks/SplitImage.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> );}
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 pagesrc/components/PageBuilder.tsx
import { Hero } from "@/components/blocks/Hero";import { Features } from "@/components/blocks/Features";import { SplitImage } from "@/components/blocks/SplitImage";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
componentsrc/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 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.
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.
You have 6 uncompleted tasks in this lesson
0 of 6