Skip to main content

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', ...]