Skip to content

ContentBlocks

Renders a list of CMS blocks.

import { ContentBlocks } from "@evolve-storefront/feature-content-blocks/content-blocks";

Renders inside ContentAudienceProvider.

Design notes

This is the whole public surface for putting content on a page: hand it a body and a registry. There is no renderItem and no slot prop, because the registry already is the per-item seam — it is keyed, so an override reaches one type everywhere it appears rather than only in the list the prop was passed to, and it reaches nested content that a top-level render prop could never see.

Server-renderable throughout: the registry travels as a prop, so no block needs "use client" to find its children.

Usage

// app/[locale]/[...slug]/content-page.tsx
import { ContentBlocks, defaultBlocks, PageHeading } from "@evolve-storefront/feature-content-blocks/content-blocks";
import { contentPageQueryOptions } from "@evolve-storefront/feature-content-blocks/queries";
import type { ClientFetcher } from "@labdigital/graphql-fetcher";
import { QueryClient } from "@tanstack/react-query";
export async function ContentPage({ gqlClient, path }: { gqlClient: ClientFetcher; path: string }) {
const page = await new QueryClient().fetchQuery(contentPageQueryOptions({ gqlClient, path }));
if (!page) return null;
return (
<main>
<PageHeading page={page} />
<ContentBlocks blocks={page.body} registry={defaultBlocks} headingLevel={2} />
</main>
);
}

Blocks

The blocks ContentBlocks renders out of the box, each shown from its default story in Storybook.

BlockPreview
ArticleList

Article cards, with a link to all of the section's articles.

Open in Storybook ↗
BrandList

Brand logos that link to their brand pages, under an optional title.

Open in Storybook ↗
CampaignBanner

A campaign: the image beside the text, never behind it, and one call to action.

Open in Storybook ↗
CategoryList

Category entrances: a grid of commerce categories, under an optional title.

Open in Storybook ↗
Columns

Two to four column entries side by side, under an optional title.

Open in Storybook ↗
ContentTeaser

An image beside rich text and up to two links, or the text alone.

Open in Storybook ↗
Faq

A titled FAQ: groups of questions, each group a single-open accordion.

Open in Storybook ↗
FigureList

Key figures: each a value with its label, and an optional source.

Open in Storybook ↗
ProductList

A titled rail of products the CMS picked.

Open in Storybook ↗
PromoList

Promo cards in a grid, under an optional title.

Open in Storybook ↗
RichText

Rich text. The default fragments select the vendor-neutral tree, which renders in full. Without a tree, as from a query of the site's own that selects only the CMS's document, the fallback: paragraphs, with the formatting dropped.

Open in Storybook ↗
ShoppableImage

An image with products pinned on it, listed beside or below it through the registered ProductVariant. These stories register a compact row for it, so the whole block fits in one view.

Open in Storybook ↗
StepList

Numbered steps that explain a process, each with an optional text and link.

Open in Storybook ↗
TableOfContents

Links to the anchors on the page. The CMS block carries only its title; ContentBlocks fills entries from the blocks around it: every block with an anchor and a title, and every anchored top-level heading of a rich text. See InPage. Without a title from the editor it reads "On this page".

Open in Storybook ↗
Tip

A highlighted tip: in a page body, or placed inside rich text, where it keeps its own look. Styled in styles.css from --color-brand and --color-surface-subtle.

Open in Storybook ↗
UspList

The selling-point band. Sized by container queries, so the same block is a row at page width and a stack in a narrow column, such as a cart summary.

Open in Storybook ↗
YoutubeVideo

An embedded YouTube video, from youtube-nocookie.com, at 16:9 and the full width of its column. Renders the content model's Video block and the YoutubeVideo placed in rich text.

Open in Storybook ↗

Props

PropTypeDescription
blocksreadonly (RenderableBlock | null | undefined)[] | null | undefined

A page body, a snippet's content — nulls and gaps are skipped.

registryContentBlockRegistry

Which component renders which type. Start from defaultBlocks.

headingLevel?HeadingLevel

The level for each block's own heading. Defaults to 2 — a page body sits under the page's h1. Pass 3 for content inside a card or a column.

framed?boolean

Give each block the page furniture it needs at the top level of a page body — a Section with a Container inside it. On by default; a block may still opt out for itself (fullBleed). Turn it off for content rendered inside an existing layout, such as a snippet in a cart column: the gutters belong to a full-width band, and a section's rhythm in a narrow column reads as broken.

Any other props go to the root element, like on a plain HTML element.

Registry components

ContentBlocks renders every block, and every piece of content inside one, with the component its registry holds under the GraphQL type name of that data. Start from defaultBlocks and register your own component under a key to replace it everywhere that type appears, nested blocks included; drop a key to turn that type off. Every component below is exported from @evolve-storefront/feature-content-blocks/content-blocks.

import { createRegistry } from "@evolve-storefront/feature-content-blocks";
import { defaultBlocks } from "@evolve-storefront/feature-content-blocks/content-blocks";
const registry = createRegistry({
...defaultBlocks,
ContentAsset: NextImage,
});
<ContentBlocks blocks={page.body} registry={registry} />;
ArticleBylineWho wrote an article and when: the author's photo, name and role, and the publication date.
PropTypeDescription
article{ author?: ArticleAuthor | null | undefined; publishedAt?: string | null | undefined; }

The article as queried: its author and publishedAt, an ISO date.

registry?ContentBlockRegistry

Renders the author's photo through its ContentAsset; the built-ins by default.

Any other props go to the root element, like on a plain HTML element.

ArticleTopicsWhat an article is about, as links to the topics' pages.
PropTypeDescription
topicsArticleTopic[]

The article's topics, in the editor's order.

resolveTopicHref?ArticleTopicHrefResolver

Routes a topic to its page. Without it, the topics are tags without links.

renderLink?ArticleTopicLinkRenderer

Any other props go to the root element, like on a plain HTML element.

BlockHeaderThe title row above a row of cards, ported from evolve-reference-b2b.

The title carries the reference's typography("h4") classes inline — see "Ported markup" in the README — but takes its level from the caller, since a card row can sit at any depth.

The link goes through the registered ResourceLink wearing buttonProps(), so the anchor carries the button classes and data-component="button". The CMS's own name wins over the default label when an editor wrote one.

PropTypeDescription
registryContentBlockRegistry
headingLevelHeadingLevel

The level for the title. The calling block passes the one it was given.

viewMoreLink?ResourceLink | null
titleId?string

Names the title element so the block's landmark can point aria-labelledby at it.

Any other props go to the root element, like on a plain HTML element.

CampaignBannerA campaign: the image beside the text from md, above it on a phone, never behind it.
PropTypeDescription
dataCampaignBanner
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

CategoryCardOne card in a CategoryList: a commerce ProductCategory, linked through the registered ResourceLink.
PropTypeDescription
dataProductCategory
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

CategoryListCategory entrances: a grid of the picked categories, else parentCategory's children, each through the registered ProductCategory.
PropTypeDescription
dataCategoryList
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

ColumnOne column of a layout block: its entries, stacked.

Not a registry key — a column is a [ColumnEntry!]! field rather than a type of its own, so there is nothing for a key to name. Its entries are keys, and that is where an override goes.

PropTypeDescription
entriesColumnEntry[]
registryContentBlockRegistry
headingLevelHeadingLevel

Any other props go to the root element, like on a plain HTML element.

ContentAssetFallback image: a plain <img> at the CMS's own URL.

Register your own to get next/image — its sizing, format negotiation and loader are app configuration (images.remotePatterns has to name the CMS host), and Next is only a peer dependency here.

alt="" when the CMS left it empty is deliberate: an unlabelled decorative image should be skipped by a screen reader, and a filename read aloud is worse than silence.

A caller passing aria-hidden gets alt="" too, and that is the contract a replacement has to honour. It is how a card says "my visible title already names this link": a catalog card whose alt text repeats its own caption makes a screen reader announce the category twice, which axe reports as image-redundant-alt.

PropTypeDescription
dataContentAsset
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

ContentBlockRenders one piece of CMS content, its component looked up in the registry by the data's GraphQL type name.

No wrapper of its own, because a div between a grid and its child breaks layout; the one exception is ContentAudience around a block for one audience.

PropTypeDescription
dataunknown

The entry to render. Ignored when null/undefined, so a nullable field needs no guard.

registryContentBlockRegistry
headingLevelHeadingLevel
name?keyof ContentBlockData | (string & {})

The registry key to look up. Omit for a union field, where the key is the value's own __typename; pass it where the GraphQL type is fixed and the selection therefore need not carry one (a campaign banner's image is a ContentAsset).

More

string & {} keeps the known names in autocomplete while still accepting a project's own block type, which is a plain string.

children?ReactNode
attributes?ContentBlockAttributes
frame?boolean

Wrap the result in ContentRow — the page furniture a top-level block sits in. Only ContentBlocks passes it; nested content is never framed.

More

It lives here rather than in the registry because framing is a property of the position, not of the type: one entry is reached from a page body, from a column and from inside another block, and only the caller knows which. A component may still opt out for itself with fullBleed.

ContentImageA standalone image, or a pair side by side from sm and stacked on a phone.

One image adds no element and no name of its own: the asset keeps data-component="content-asset" and takes the caller's attributes. A pair is wrapped in content-image, which takes them instead.

PropTypeDescription
dataContentImage
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

ContentMessageA piece of the store's fixed system copy: the editor's text where the CMS has it, else children.
PropTypeDescription
messageRichText | null | undefined

One field of Messages, such as cartEmpty; null where the editor left it empty.

registryContentBlockRegistry
headingLevel?HeadingLevel
children?ReactNode

The storefront's own copy, shown when the CMS has none.

ContentRowThe page furniture a top-level block sits in: a vertical rhythm band with the brand's content width and gutters inside it.

A div. It was a section, which bought nothing: a section is only exposed as a landmark once it has an accessible name, and this has none — the block inside it is the thing worth naming, and each one now does. Two nested regions where the outer is unnamed is noise in the landmark list.

The width comes from Container, so it follows --container-max and --container-padding rather than a hardcoded max-w-*.

Applied by ContentBlocks around each entry of a page body, not by the blocks themselves and not by the registry. Framing is a property of the position: one registry entry is reached from a page body, from a column and from inside another block, and only the caller knows which. A registry that carried the frame would be right in one position and wrong in the rest.

No props of its own.

ContentSearchHitsThe pages and articles a search found, at most three, as links to place above the products.
PropTypeDescription
hitsContentSearchHit[]

readContentSearch's hits; at most three are shown.

headingLevel?HeadingLevel

The title's level in the page. Defaults to 2.

resolvePageHref?ContentSearchPageHrefResolver

Routes a hit's page by its type; the default roots its path, see contentSearchHitHref.

renderLink?ContentSearchLinkRenderer

Any other props go to the root element, like on a plain HTML element.

FaqA titled FAQ, made of one or more groups of questions.

The groups sit under this block's own title, so they are handed subLevel() rather than the level this block was given.

PropTypeDescription
dataFaq
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

FaqGroupA titled set of FAQ items, as a single-open accordion.

ui's Accordion, which is single-open and collapsible by default — the behaviour the reference got from radix. The primitive holds the client boundary, so this block and its answers stay on the server.

A group without a title renders no heading, and its items keep the group's level; under a title they get subLevel().

PropTypeDescription
dataFaqGroup
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

FaqItemOne question and its answer, as a section of ui's Accordion.

A Server Component, which is what moving the accordion into ui buys: the client boundary is inside the primitive, so the answer — rendered through the registry, and reaching RichText and whatever renders its inline links — stays on the server and crosses as children.

PropTypeDescription
dataFaqItem
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

PageHeadingThe top of a page: its h1, then the intro (an article's lead) and the lead image.
PropTypeDescription
pagePageHeadingSource

The page as queried: its heading, intro and lead image.

registry?ContentBlockRegistry

Renders the lead image through its ContentAsset; the built-ins by default.

Any other props go to the root element, like on a plain HTML element.

ProductListA titled rail of products the CMS picked.

This block owns the frame — the heading, the view-more link, the rail — and none of the card. The rail is ui's Carousel, so it arrives with arrows that hide at each end, a keyboard-scrollable viewport, and the list semantics WebKit drops from an unstyled list.

It emits productListViewed once the rail has been seen, under data.title. The block is the only place that knows both the list name and its items, the same reason useAddToCart is where cartItemAdded comes from. productClicked is not emitted here: the click belongs to whatever the consumer rendered, so renderItem is handed the two facts a card cannot work out for itself.

The list part owns the key.

PropTypeDescription
dataProductListData<ProductVariant>
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

renderItem?ProductVariantRenderer<ProductVariant>

Renders each product; omit it and the registry's ProductVariant does. The seam for a card that needs more than this package selects: it is generic over the item, so a widened page query reaches it typed. See "Extending the query" in the README.

ProductVariantFallback product card: image, name, SKU.

Register your own. A product card is a commerce surface with a route, a price shape and a cart action behind it, all of which belong to the storefront. This exists so a ProductList shows its products rather than an empty row before that is wired, and to make the seam obvious in Storybook.

PropTypeDescription
dataProductVariant
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

PromoCardOne promo on ui's Card: its image, title and text, the whole card the link where it has one.
PropTypeDescription
dataPromo
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

PromoListPromo cards in a grid, one to a row on a phone and up to four from lg, each through the registered Promo.
PropTypeDescription
dataPromoList
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

RichTextRich text from any CMS: the vendor-neutral tree that ContentBlocksRichTextFields selects, rendered in full through RichTextTree.
PropTypeDescription
dataRichText
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

RichTextTreeRich text from the vendor-neutral tree, the same for every CMS.
PropTypeDescription
dataRichTextTreeData
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

TableOfContentsLinks to the anchors on the page.
PropTypeDescription
dataTableOfContents
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

TipA highlighted tip, in a page body or inside rich text: its label, or "Tip" where the editor left none, above the text.
PropTypeDescription
dataTip
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

UspOne selling point, optionally linked.
PropTypeDescription
dataUsp
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

UspListThe CMS's unique selling points: a band at page width, a stacked list in a narrow column.
PropTypeDescription
dataUspList
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.

YoutubeVideoAn embedded YouTube video: a plain <iframe> from youtube-nocookie.com, which defers YouTube's cookies until playback.

Renders YoutubeVideo and the content model's Video, which plays with controls and no autoplay or loop, and names itself by its title.

PropTypeDescription
dataVideo | YoutubeVideo
registryContentBlockRegistry

The registry to render nested content with — pass it down unchanged.

headingLevelHeadingLevel

The level for the topmost heading this component renders. Derive a subtree's with subLevel(); a block cannot know its own document depth.

children?ReactNode

Content this entry wraps. Only wrapping entries read it — ResourceLink is the one built-in that does, because a link is the label its caller gives it.

attributes?ContentBlockAttributes

Props for this entry's root element. Spread them last and merge className with cn().

More

This is how a caller styles an entry it does not own: a block rendering a link as a button passes buttonProps({ variant }) straight through, so the anchor carries the button classes and data-component="button" — the two halves of a button's identity, which must not be separable.