Skip to main content

Adding a CMS Content Block Type

Key Concepts​

CMS Objects​

  • block: A content block is a box for any content type that can be rendered inline: Hero Banners, Video, Text Blocks, etc.
  • page: Page metadata plus content blocks. Will live at a standalone url.
  • navigation: Structured data model for a site navigation tree
  • footer Structured data model for a footer.

For this guide, we are talking about block, and specifically how to add a new type of block, mediaHero. After completing these steps, a merchandiser could add this media hero to any content block on the site.

1. Configure the Schema in the CMS​

Define your new content type in your CMS. Configure the appropriate schemas and fields.

2. Create Content Block Type Definition​

In the @hv/cms package, add a zod schema definition in src/models/block.ts.

For example, here is a zod schema definition for a media block hero that has a several text fields and and some CTAs:

export const mediaHeroBlockSchema = baseBlockSchema.merge(
z.object({
id: z.string(),
type: z.literal('mediaHero'),
eyebrowText: optionalField(z.string()),
header: optionalField(z.string()),
subcopy: optionalField(z.string()),
image: z.object({
type: z.literal('image'),
src: z.string(),
alt: z.string(),
}),
ctas: optionalField(
z.array(
z.object({
label: optionalField(z.string()),
href: optionalField(z.string()),
})
)
),
})
);

export type MediaHeroBlock = z.infer<typeof mediaHeroBlockSchema>;

// Add new block schema so zod will parse it when
// it is included in a content block
export const blockSchema: z.ZodType<unknown> = z.union([
mediaHeroBlockSchema,
...
])


// Add it to type alias registry
export type ContentBlockTypes = {
mediaHero: MediaHeroBlock;
...
}

3. Normalize it in the vendor CMS package​

In your vendor CMS package, such as contentful, modify the block function in normalize.ts to parse the CMS values to the zod schema:

 case 'mediaHero': {
const {
fields: {
ctas = [],
eyebrowText,
header,
image,
subcopy,
},
} = block;

const model: MediaHeroBlock = mediaHeroBlockSchema.parse({
type: 'mediaHero',
id,
ctas: ctas.map((cta: any) => ({
label: cta.fields.label,
href: cta.fields.href,
variant: toButtonVariant(cta.fields.variant),
})),
eyebrowText,
header,
image: {
type: 'image',
src: image.src,
alt: image.alt,
},
subcopy,
});

return model;
}

4. Create the UI​

Create a component in your app that extends the CMS data model as its props and outputs a React UI.

This example component composes some components from the ui library to build a ui for a media hero:

// apps/web/app/_components/content/media/media-hero.tsx
import type { MediaHeroBlock } from '@hv/cms/models';
import { Button } from '@hv/ui/button';
import * as UI from '@hv/ui/hero';

export type MediaHeroProps = MediaHeroBlock & {
priority?: boolean;
};

export function MediaHero({
ctas = [],
eyebrowText,
header,
image,
priority,
subcopy,
}: MediaHeroProps) {
const hasCta = ctas.length > 0;
const hasText = eyebrowText || header || subcopy;

return (
<UI.MediaHero {...orientation} textColor={textColor}>
{image && (
<UI.MediaHeroBackground asChild>
<Image src={image.src} alt={image.alt} fill priority={priority} />
</UI.MediaHeroBackground>
)}
{(hasText || hasCta || hasLink) && (
<UI.MediaHeroContent>
{hasText && (
<UI.MediaHeroTextBlock position={orientation?.textAlignment}>
{eyebrowText && (
<UI.MediaHeroEyebrow className='text-xl'>
{eyebrowText}
</UI.MediaHeroEyebrow>
)}
{header && <UI.MediaHeroHeader>{header}</UI.MediaHeroHeader>}
{subcopy && <UI.MediaHeroSubcopy>{subcopy}</UI.MediaHeroSubcopy>}
</UI.MediaHeroTextBlock>
)}
{hasCta && (
<UI.MediaHeroCTAs direction={orientation?.ctaDirection}>
{ctas.map((cta, idx) => (
<Button asChild key={idx} variant={cta.variant}>
<Link href={cta.href ?? ''}>{cta.label}</Link>
</Button>
))}
</UI.MediaHeroCTAs>
)}
</UI.MediaHeroContent>
)}
</UI.MediaHero>
);
}

Finally, In the cms-component-map.tsx file in the Next.js app, export your component so the CMS integration uses it to render media heros:

export { MediaHero as mediaHero } from '@/app/_components/content/media/media-hero';