Blocks
Blocks are the core building unit. Each block pairs a Zod schema (defining editable fields) with a Svelte component (defining how it renders).
Block definition
import type { Block } from '@repo/cms'
interface Block {
label: string
schema: AllowedObjects // Zod object schema
component: FlexRender.FlexRenderComponentConfig<any>
}
A block map is a Record<string, Block> where keys are block identifiers:
import { RenderBlocks, RichTextEditor } from '@repo/cms'
import { zaf } from '@repo/form'
import { z } from 'zod'
import TextSection from './TextSection.svelte'
import ImageBanner from './ImageBanner.svelte'
export const pageBlocks = {
text: {
label: 'Text Section',
schema: z.object({
body: zaf(RichTextEditor.schema, { component: RichTextEditor.Root }),
}),
component: RenderBlocks.renderComponent(TextSection, {}),
},
banner: {
label: 'Image Banner',
schema: z.object({
title: z.string(),
image: zaf(z.string(), { component: AssetAutocompleteField, group: 'assets' }),
}),
component: RenderBlocks.renderComponent(ImageBanner, {}),
},
}
renderComponent
RenderBlocks.renderComponent(Component, staticProps) wraps a Svelte component so the block system can render it. The component receives data as a prop containing the block's field values.
// Component signature
type Props = { data: { body: JSONContent } }
const { data }: Props = $props()
BlockValue
Each saved block instance has this shape:
interface BlockValue<T = Record<string, any>> {
block: string // key from the block map
values: T // data matching the block's schema
}
A page stores an array of BlockValue[].
BlockField -- editing blocks
BlockField.Root provides the editing UI: add blocks, reorder, remove, and edit each block's fields through auto-generated forms.
<script lang="ts">
import { BlockField } from '@repo/cms'
type Props = { value: BlockValue[]; blocks: Record<string, Block> }
let { value = $bindable(), blocks }: Props = $props()
</script>
<BlockField.Root bind:value {blocks} />
Typically used via zaf inside a form schema rather than standalone:
const schema = z.object({
blocks: zaf(z.array(z.any()), {
component: FormField.renderComponent(BlockField.Root, { blocks: pageBlocks }),
}),
})
RenderBlocks -- displaying blocks
RenderBlocks.Root iterates over a BlockValue[] array, looks up each block's component from the block map, and renders it with the stored values.
<script lang="ts">
import { RenderBlocks } from '@repo/cms'
import { pageBlocks } from './blocks'
type Props = { value: BlockValue[] }
const { value }: Props = $props()
</script>
<RenderBlocks.Root blocks={pageBlocks} {value} />
Asset grouping
When blocks reference assets via zaf with a group option, getGroupsFromBlocks extracts all grouped values. This is useful for batch-loading asset URLs before rendering:
import { getGroupsFromBlocks } from '@repo/cms'
const groups = getGroupsFromBlocks({ value: blockValues, blocks: pageBlocks })
// groups.assets => ['asset-id-1', 'asset-id-2', ...]