blökkli’s adapter pattern lets you connect it to any backend. Whether you’re using a REST API, GraphQL, a headless CMS, or even browser storage, you implement the same interface and blökkli handles the rest — routing user editing actions to your adapter methods and keeping the UI in sync.
The Adapter Pattern
The adapter is a factory function you pass to defineBlokkliEditAdapter(). It receives a context object (ctx) and returns a plain object of async methods. blökkli calls those methods in response to user actions:
| User action | Adapter method called |
|---|
| Editor opens | loadState, mapState |
| Block list requested | getAllBundles, getFieldConfig |
| Add a block | addNewBlock |
| Move a block | moveBlock |
| Move multiple blocks | moveMultipleBlocks |
| Delete blocks | deleteBlocks |
| Change block options | updateOptions |
| Publish | publish |
Your adapter translates each of these calls into whatever API requests or storage operations your backend requires. blökkli doesn’t care how your data is stored — only that your adapter returns data in the expected shapes.
Data Shapes
blökkli expects block data in the following shape. Your mapState method is responsible for transforming your backend’s response into this format:
interface BlockData {
uuid: string // unique identifier for this block instance
bundle: string // block type — matches the component's bundle
hostField: string // the field name this block lives in
options?: Record<string, unknown> // current resolved option values
// any additional props passed through to the component
[key: string]: unknown
}
The mapState method receives the raw value returned by loadState and must return an object with a blocks array of BlockData items:
async mapState(state): Promise<{ blocks: BlockData[] }> {
return {
blocks: state.items.map((item) => ({
uuid: item.id,
bundle: item.type,
hostField: item.field,
options: item.options ?? {},
// pass any extra fields your block components need
title: item.title,
})),
}
}
Example: REST API Adapter
The following adapter connects blökkli to a JSON REST API. It covers all seven required methods plus the three most useful optional ones:
// app/blokkli.editAdapter.ts
import { computed } from 'vue'
import { defineBlokkliEditAdapter } from '#blokkli/adapter'
export default defineBlokkliEditAdapter((ctx) => {
const baseUrl = '/api/blokkli'
const entityUuid = computed(() => ctx.state.value.currentEntityUuid)
return {
// --- Required ---
async loadState() {
return $fetch(`${baseUrl}/state/${entityUuid.value}`)
},
async mapState(state) {
return {
blocks: (state as any).blocks.map((b: any) => ({
uuid: b.id,
bundle: b.type,
hostField: b.field,
options: b.options ?? {},
})),
}
},
async getAllBundles() {
return $fetch(`${baseUrl}/bundles`)
},
async getFieldConfig() {
return $fetch(`${baseUrl}/fields/${entityUuid.value}`)
},
async addNewBlock({ bundle, hostField, preceedingUuid, options }) {
await $fetch(`${baseUrl}/blocks`, {
method: 'POST',
body: { bundle, hostField, preceedingUuid, options },
})
},
async moveBlock({ uuid, hostField, preceedingUuid }) {
await $fetch(`${baseUrl}/blocks/${uuid}/move`, {
method: 'PATCH',
body: { hostField, preceedingUuid },
})
},
async moveMultipleBlocks({ uuids, hostField, preceedingUuid }) {
await $fetch(`${baseUrl}/blocks/move-multiple`, {
method: 'PATCH',
body: { uuids, hostField, preceedingUuid },
})
},
// --- Optional but recommended ---
async deleteBlocks({ uuids }) {
await $fetch(`${baseUrl}/blocks`, {
method: 'DELETE',
body: { uuids },
})
},
async updateOptions({ uuid, options }) {
await $fetch(`${baseUrl}/blocks/${uuid}/options`, {
method: 'PATCH',
body: { options },
})
},
async publish() {
await $fetch(`${baseUrl}/publish/${entityUuid.value}`, {
method: 'POST',
})
},
}
})
Example: localStorage Adapter (Development)
For prototyping without a backend, you can persist block state entirely in localStorage. This lets you build and test block components without any server infrastructure:
// app/blokkli.editAdapter.ts
import { defineBlokkliEditAdapter } from '#blokkli/adapter'
export default defineBlokkliEditAdapter((_ctx) => {
const STORAGE_KEY = 'blokkli_dev_state'
function getBlocks(): any[] {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
}
function saveBlocks(blocks: any[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(blocks))
}
return {
async loadState() {
return { blocks: getBlocks() }
},
async mapState(state) {
// State is already in the correct shape — pass it through.
return state as { blocks: any[] }
},
async getAllBundles() {
return [
{ id: 'text', label: 'Text' },
{ id: 'image', label: 'Image' },
]
},
async getFieldConfig() {
return [
{
name: 'blocks',
label: 'Content',
allowedBundles: ['text', 'image'],
entityType: 'page',
entityBundle: 'default',
},
]
},
async addNewBlock({ bundle, hostField, preceedingUuid }) {
const blocks = getBlocks()
const newBlock = {
uuid: crypto.randomUUID(),
bundle,
hostField,
options: {},
}
const insertAt = preceedingUuid
? blocks.findIndex((b) => b.uuid === preceedingUuid) + 1
: blocks.length
blocks.splice(insertAt, 0, newBlock)
saveBlocks(blocks)
},
async moveBlock({ uuid, hostField, preceedingUuid }) {
const blocks = getBlocks()
const fromIndex = blocks.findIndex((b) => b.uuid === uuid)
if (fromIndex === -1) return
const [block] = blocks.splice(fromIndex, 1)
block.hostField = hostField
const toIndex = preceedingUuid
? blocks.findIndex((b) => b.uuid === preceedingUuid) + 1
: blocks.length
blocks.splice(toIndex, 0, block)
saveBlocks(blocks)
},
async moveMultipleBlocks({ uuids, hostField, preceedingUuid }) {
const blocks = getBlocks()
// Extract all blocks to move, preserving their relative order.
const moving = blocks.filter((b) => uuids.includes(b.uuid))
const remaining = blocks.filter((b) => !uuids.includes(b.uuid))
moving.forEach((b) => { b.hostField = hostField })
const toIndex = preceedingUuid
? remaining.findIndex((b) => b.uuid === preceedingUuid) + 1
: remaining.length
remaining.splice(toIndex, 0, ...moving)
saveBlocks(remaining)
},
async deleteBlocks({ uuids }) {
saveBlocks(getBlocks().filter((b) => !uuids.includes(b.uuid)))
},
async updateOptions({ uuid, options }) {
const blocks = getBlocks()
const block = blocks.find((b) => b.uuid === uuid)
if (block) {
block.options = { ...block.options, ...options }
saveBlocks(blocks)
}
},
}
})
The localStorage adapter is a great way to build and test all your block components — including their options and layouts — before you wire up a real backend. You get the full editing UI without needing any server-side infrastructure in place.