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';