Data model
The Blok document is a plain JSON object. You can store it wherever you want — a Postgres column, a file in object storage, a Markdown frontmatter block — and round-trip it through JSON.stringify / JSON.parse without loss.
Top-level shape
interface Data<RootProps = {}, BlockProps = {}> {
root: RootData<RootProps>;
content: BlockInstance<BlockProps>[];
zones?: Record<string, BlockInstance<BlockProps>[]>;
}
interface RootData<Props> {
props?: Props;
readOnly?: Partial<Record<keyof Props, boolean>>;
}
interface BlockInstance<Props> {
type: string;
props: Props & { id: string };
readOnly?: Partial<Record<keyof Props, boolean>>;
version?: number;
}root
Document-level settings — whatever the root component declares as fields.
{ "root": { "props": { "title": "Welcome" } } }content
The top-level blocks, in order.
{
"content": [
{ "type": "Hero", "props": { "id": "b1", "title": "Welcome" } },
{ "type": "Features", "props": { "id": "b2", "items": [...] } }
]
}zones
Nested blocks — anything inside a slot. The key is
`${blockId}:${slotFieldName}`.
{
"content": [
{ "type": "Container", "props": { "id": "b1" } }
],
"zones": {
"b1:children": [
{ "type": "Hero", "props": { "id": "b2", "title": "Nested" } }
]
}
}Use slotZoneId(blockId, fieldName) to compute the key:
import { slotZoneId } from "@useblok/core";
const key = slotZoneId("b1", "children"); // "b1:children"BlockInstance
{
type: "Hero",
props: {
id: "b1", // unique within the document
title: "Welcome",
subtitle: "…",
},
version: 2, // optional schema version (for migrations)
}type— must match a key inconfig.components.props.id— unique block id. Blok generates these viauid().version— optional. Used by the migration runner. Blocks without it are treated as v0.readOnly— optional. Partial record marking specific props as read-only in the UI.
uid() helper
Blok exports a small unique-ID generator:
import { uid } from "@useblok/core";
const id = uid(); // "k3p9a-x"Use it when you construct block instances programmatically.
Serialisation
Blok stores nothing in the data that isn’t plain JSON:
- No Date objects — use ISO strings.
- No functions —
render,getSummary, etc. live in the Config. - No class instances — stick to plain objects and primitives.
This means:
const fromDB = JSON.parse(await db.pages.find(id));
<Blok config={config} data={fromDB} onSave={...} />just works.
Compatibility
The shape is intentionally close to Puck ’s data
model. If you have a Puck document, you can pass it directly as data
(with minor component-name mapping). See Migrating from Puck
— on our roadmap as a dedicated guide.