Skip to content

Add a content block

A content block is one content type from end to end: the fields an editor fills in, the GraphQL type the API returns, and the mapper between the two. You define all of it once with defineContentBlock and add it to the CMS module’s blocks. The module merges the block’s GraphQL type into the schema and registers its mapper.

This page adds a quote block to page bodies. It assumes a CMS module is already in your service. See Add a CMS.

import { defineContentBlock } from "@evolve-framework/contentful";
import type { Block } from "@evolve-framework/contentful/graphql/types";
import type { EntryFieldTypes, EntrySkeletonType } from "contentful";
type QuoteSkeleton = EntrySkeletonType<
{ quote: EntryFieldTypes.Text; author?: EntryFieldTypes.Symbol },
"quote"
>;
export const quote = defineContentBlock<QuoteSkeleton>({
id: "quote",
typename: "QuoteBlock",
groups: ["block"],
typeDefs: /* GraphQL */ `
type QuoteBlock {
id: ID!
quote: String!
author: String
}
union Block = QuoteBlock
`,
terraform: /* hcl */ `
resource "contentful_contenttype" "quote" {
space_id = local.contentful_space_id
environment = local.contentful_environment
id = "quote"
name = "Quote"
display_field = "author"
fields = [
{ id = "quote", name = "Quote", type = "Text", required = true, localized = true },
{ id = "author", name = "Author", type = "Symbol", required = false },
]
}
`,
// The package's Block type lists only the built-in members, so a new member needs the cast.
mapper: (_context, entry) =>
({
__typename: "QuoteBlock",
id: entry.sys.id,
quote: entry.fields.quote,
author: entry.fields.author,
}) as unknown as Block,
});

The type parameter is the entry skeleton, so entry.fields is checked against your content type. The skeleton types come from the contentful package.

  • id is what the CMS calls the content type: the Contentful content type id, the Storyblok component name, or the Payload block slug. The mapper registry dispatches on it.
  • typeDefs declares the GraphQL type and the union it joins. Join Block for a page body, ColumnEntry for a column. Merging only adds: a block cannot change or remove a built-in type, and a field that conflicts with the shared definition throws.
  • groups says where an editor may place the block: block for a page body, columnEntry for a column. The Terraform whitelists and the Payload slots are built from it.
  • The mapper turns one CMS entry into the GraphQL type. Its __typename must match typename.

groups and the union are separate statements. Usually they match, as here.

If your block holds other blocks, declare them in slots, by group or by id, and map each child through the registry rather than by importing its block. In Storyblok, call getMapperRegistry().mapValue<T>(storeContext, child). In Payload, call context.mapValue<T>(child) with the context the mapper receives.

blocks is the complete set the module serves, so spread the built-ins and add yours:

import { ContentfulModule, contentBlocks } from "@evolve-framework/contentful";
import { quote } from "./blocks/quote.ts";
new ContentfulModule({
// ...config and messagePublisher
blocks: [...contentBlocks, quote],
});

A block with the same id as a built-in replaces the built-in.

How the module registers the mapper depends on the CMS:

  • Contentful registers it by groups only: block with registerBlock, columnEntry with registerColumnEntry. A block in neither group is not mapped.
  • Storyblok and Payload register every mapper with registerValue, so a container can map it as a child, and also by groups. They wrap a column entry placed directly in a page body in a OneColumnBlock.

An entry with no registered mapper is logged and left out of the response.

evolve tf-export writes the built-in blocks only. Write your set with writeTerraform, then apply it with Terraform:

import { contentBlocks, writeTerraform } from "@evolve-framework/contentful";
import { quote } from "./blocks/quote.ts";
writeTerraform([...contentBlocks, quote], "terraform/contentful");

Because quote is in the block group, it is added to the block_content_types whitelist, so editors can add it to a page body.

The storefront renders blocks with @evolve-storefront/feature-content-blocks. It looks up each block’s component by its GraphQL type name.

Select your type in the page query, next to the package’s fragment:

query GetContentPage($path: String!) {
contentPage(path: $path) {
body {
...ContentBlocksBlockFields
... on QuoteBlock {
id
quote
author
}
}
}
}

Then register a component under the type name:

import { createRegistry } from "@evolve-storefront/feature-content-blocks";
import { defaultBlocks } from "@evolve-storefront/feature-content-blocks/content-blocks";
function Quote({ data }: { data: { quote: string; author?: string | null } }) {
return (
<blockquote>
<p>{data.quote}</p>
{data.author ? <cite>{data.author}</cite> : null}
</blockquote>
);
}
export const registry = createRegistry({ ...defaultBlocks, QuoteBlock: Quote });

To replace only the mapper of a built-in block, pass mappers. The callback receives the registry with every block already registered. A registration for a known id replaces the existing mapper.

new StoryblokModule({
// ...config, siteConfigs and messagePublisher
mappers: (registry) => {
registry.registerBlock("content_teaser", myTeaserMapper);
},
});

Register on the channel the module reads the block from:

Method Contentful Storyblok Payload
registerBlock(id, mapper) Page body entries, Block Page body bloks, Block Page body blocks, Block
registerColumnEntry(id, mapper) Column entries, ColumnEntry Column entries, ColumnEntry Column entries, ColumnEntry
registerValue(id, mapper) None Any child blok, by component name Any child block, by slug
registerLink(id, mapper) Entries in link fields, ResourceLink None None

In Storyblok and Payload a container, and a page’s hero field, map their children through registerValue. To change a block everywhere it appears, register its mapper there as well. Contentful maps a content page’s hero field with the built-in mapper, whatever you register.

To change more than the mapper, such as the fields or the Terraform, eject the block. See Take ownership of a built-in block.

To add a field to a built-in GraphQL type without replacing the block, add a module of your own with extend type and a resolver, and compose it next to the CMS module. See Add your own module.