Migrations
When you change a block’s props shape in a backwards-incompatible way — renaming a field, merging two fields, restructuring nested data — existing saved documents still reference the old shape. Migrations let you upgrade old blocks on-the-fly when they load.
The idea
- Each component has a
version(default0). - Each saved block instance records its
version. - When the editor loads a document, the migration runner walks each block from its recorded version up to the component’s current version, running each migration in order.
Migrations only run in memory. The original saved JSON is untouched until the user saves again.
Declaring a migration
import { defineMigration, type ComponentConfig } from "@useblok/core";
// v0 → v1: rename `heading` to `title`.
const migrateV0ToV1 = defineMigration<
{ heading: string },
{ title: string }
>({
from: 0,
to: 1,
migrate: (props) => ({
title: props.heading,
}),
});
const Hero: ComponentConfig<{ title: string }> = {
label: "Hero",
version: 1,
migrations: [migrateV0ToV1],
fields: { title: { type: "text", label: "Title" } },
defaultProps: { title: "Hello" },
render: ({ title }) => <h1>{title}</h1>,
};Multi-step migrations
Chain migrations for each step of your schema evolution:
version: 3,
migrations: [
migrateV0ToV1, // rename heading → title
migrateV1ToV2, // split title into title + subtitle
migrateV2ToV3, // move subtitle into a `hero.meta` object
],The runner picks the right starting point per-block. If a block is
already at v2, only migrateV2ToV3 runs on it.
migrate function signature
migrate: (oldProps) => newPropsIt’s a pure function. Don’t trigger side effects — it may run many times per session (once per block).
When props depend on other data
Sometimes you need to look at the full document to migrate a block (e.g.
“if the root has darkMode: true, set this block’s theme to dark”).
The runner doesn’t pass the full doc, but you can migrate the document
yourself before passing it to <Blok>:
import { migrateData } from "@useblok/core";
const upgraded = migrateData(data, config);
// Now do whatever document-level fix-ups you need
upgraded.content = upgraded.content.map((block) => {
if (block.type === "Hero" && data.root.props?.darkMode) {
return { ...block, props: { ...block.props, theme: "dark" } };
}
return block;
});
<Blok config={config} data={upgraded} />Migration report
migrateData returns the upgraded document and a report:
const { data: upgraded, report } = migrateData(originalData, config);
report.migratedBlocks.forEach((entry) => {
console.log(`${entry.blockId}: v${entry.fromVersion} → v${entry.toVersion}`);
});Inspect the report in dev to catch missed migrations before deploying.
Removing a block type
If you completely remove a block type, existing saved documents will have
references to the missing type. Options:
- Leave a stub — keep a minimal
ComponentConfigthat just renders a placeholder. Users can then delete the blocks themselves. - Filter at load — strip removed types from
data.content/data.zonesbefore passing to Blok.
Don’t silently drop unknown block types without telling anyone — users may lose content without realising. Log it, warn, or show an “unknown block” placeholder.
Versioning field types (plugins)
Custom field types from plugins don’t have their own migration system — if the plugin changes its field shape, you need to migrate the block that uses it.