@b10cks/vue

SDK to integrate b10cks into your vue applications
6,343 2
Install
npm install @b10cks/vue
Latest Version:3.1.0
Vue:3
License:MIT
Last Updated:Oct 22, 2026
Links: GitHub  ·  npm
Maintainer: badmike

@b10cks/vue

Vue 3 SDK for integrating b10cks, an open-source headless CMS with a composable block-based content API, into Vue applications.

Installation

npm install @b10cks/vue @b10cks/client @b10cks/richtext

Or let the CLI install the packages, register the plugin, and write .env for you:

npx @b10cks/cli init

Usage

Plugin Setup

Keep the access token in .env rather than in source. On Vite:

# .env
VITE_B10CKS_TOKEN=your-access-token
import { createApp } from 'vue'
import { B10cksVue } from '@b10cks/vue'
import App from './App.vue'

const app = createApp(App)

app.use(B10cksVue, {
  apiClientOptions: {
    token: import.meta.env.VITE_B10CKS_TOKEN ?? '',
    baseUrl: 'https://api.b10cks.com/api',
  },
})

app.mount('#app')

Data Composables

import { useB10cksApi } from '@b10cks/vue'

const { useContent, useContents, useBlocks } = useB10cksApi()

// Single content entry by slug
const content = useContent('home', { vid: 'published' }, { immediate: true })

// List of content entries — params accepts a typed filter object
const { data: items } = useContents(
  {
    language_iso: 'en',
    vid: 'published',
    filter: {
      canonical_id: { in: [someId] },
    },
  },
  { immediate: Boolean(someId) }
)

const blocks = useBlocks({}, { immediate: true })

The immediate option controls whether the request fires on composable setup (true) or must be triggered manually (false). Pass immediate: false to defer requests that depend on reactive values not yet available.

Slugs, names and params accept refs and getters. Once a query has run, a change to them fetches again, and the request it replaces is aborted. Unmounting the component aborts a request still in flight.

const route = useRoute()
const { data } = useContent(
  () => route.params.slug as string,
  () => ({ vid: vid.value })
)

Components

Use B10cksComponent to render block-specific Vue components dynamically:

<B10cksComponent :block="block" v-editable="block" />

Each block is rendered inside an error boundary: a block component that throws while rendering doesn't take down the page. In the visual editor it shows a placeholder naming the block type and the error, which editors can select to fix or delete the block. In production it renders nothing. The error goes to your app.config.errorHandler once, or to the console without one. Errors from event handlers leave the block alone.

Block components resolve by name (hero_section renders HeroSection), so each one types its own props with the generated block type from b10cks generate types:

<script setup lang="ts">
import type { B10cksHeroSection } from '~/b10cks/types'

defineProps<{ block: B10cksHeroSection }>()
</script>

Link anchors

An internal link can point at a block on the target page: its anchor is that block's id. resolveB10cksLink and the rich text renderer append it to the href (/about?ref=nav#01kh6h981yh1s5z7s3f80wmrw2), so the page needs an element with that id.

v-editable renders id="<block.id>" on its element, in production too and during SSR. An id you set on the same element wins. Opt out with .noanchor:

<section v-editable="block">…</section>
<!-- id="01kh6h…" -->
<section v-editable.noanchor="block">…</section>
<!-- no id -->

A server-rendered template can't see an id on the same element, so the HTML carries the block id until hydration restores yours. Use .noanchor there if the id must be right before hydration.

For blocks rendered without the directive, bind blockAnchorAttrs:

<script setup lang="ts">
import { blockAnchorAttrs, resolveB10cksLink } from '@b10cks/vue'
</script>

<template>
  <section v-bind="blockAnchorAttrs(block)">
    <a :href="https://github.com/b10cks/sdk/blob/HEAD/resolveB10cksLink(block.link)?.href">…</a>
  </section>
</template>

Block ids are ULIDs and can start with a digit, which is not a valid CSS id selector. Look them up with document.getElementById(id), or querySelector('#' + CSS.escape(id)).

Live preview & visual editing

When your app is rendered inside the b10cks visual editor, these directives and the usePreviewContent composable make blocks selectable and keep the preview in sync while editing. They are all no-ops outside the editor, so they are safe to leave in production output.

Directives

<!-- Mark a block as selectable: click selects it; the editor highlights/scrolls to it.
     It also live-updates in place when the editor pushes changes for this block. -->
<section v-editable="block">…</section>

<!-- Inline-edit a simple string field (contenteditable, streams plain-text edits) -->
<h1 v-editable-field="{ id: block.id, field: 'headline' }">{{ block.headline }}</h1>

<!-- Accordions, tabs, carousels: a click selects the block and still reaches the
     component. Links and submit buttons inside don't navigate. -->
<section v-editable.interactive="block">…</section>

<!-- Rich text / complex fields: deep-select so the editor opens its own editor.
     Use `mode: 'select'` with a `path` instead of inline editing. -->
<B10cksRichText
  :document="block.body"
  v-editable-field="{ id: block.id, path: ['body'], mode: 'select' }"
/>

By default a click on a block only selects it, so links, buttons and router links inside don't fire while editing. A click into an inline field selects its block too, and keeps the caret. .interactive is for components that need their own clicks in the editor; Alt-click (Option on macOS) reaches the page for any block. v-editable-field follows its element: when a keyed list reorders, edits go to the item's new index.

usePreviewContent (recommended)

v-editable patches a single block in place, which is enough for simple cases. For whole-tree reactive updates — including nested and rich text fields — wrap your fetched content in usePreviewContent. It returns a reactive ref that live-updates from the editor and is unchanged outside preview mode:

<script setup lang="ts">
import { toRootBlock, usePreviewContent } from '@b10cks/vue'

const { data } = useContent('home')
// Pass a getter (or ref) so the preview resets when the content is refetched
// on a route/locale change instead of keeping the first tree.
// `toRootBlock(entry)` yields `{ ...entry.content, id, block }`: an entry's
// `content` object carries no `id` of its own, and without one the root block
// is not selectable in the editor and root-level edits cannot be matched to it.
const content = usePreviewContent(() => toRootBlock(data.value))
</script>

<template>
  <B10cksComponent :block="content" />
</template>

usePreviewContent accepts a plain value, a ref, or a getter. A plain value is captured once; a reactive source resets the preview store whenever it changes.

The editor sends CONTENT_UPDATE scoped to the block being edited, addressed by its id. The store merges such an update into the tree in place; only an update whose id is the root's (or a payload without an id) replaces the whole tree. Updates for ids that are not in the rendered tree are ignored.

Plugin options

app.use(B10cksVue, {
  apiClientOptions: { token, baseUrl },
  // Offset applied when a selected block is scrolled into view, so selection
  // clears a fixed app header. A number is px; strings are used verbatim.
  scrollOffset: 80,
  // Restrict the preview bridge handshake to known editor origins.
  allowedOrigins: ['https://app.b10cks.com'],
})

scrollOffset can also be set purely in CSS — :root { --b10cks-scroll-offset: 80px } — with no plugin option.

Rich Text

Rich text exports live in the @b10cks/vue/rich-text sub-path, so pages that do not render rich text keep it out of their entry bundle.

Use B10cksRichText to render a b10cks RichTextDocument (a TipTap/ProseMirror-style JSON document) as HTML with a dependency-free, SSR-friendly renderer.

<script setup lang="ts">
import type { RichTextDocument } from '@b10cks/vue/rich-text'
import { B10cksRichText } from '@b10cks/vue/rich-text'

const document: RichTextDocument = {
  type: 'doc',
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'Hello from b10cks rich text in Vue.',
        },
      ],
    },
  ],
}
</script>

<template>
  <B10cksRichText
    :document="document"
    class="prose"
  />
</template>

Editing rich text in the preview

Pass editable with the block id and the field's path, and editors can edit the text in place in the visual editor:

<B10cksRichText :document="block.body" :editable="{ id: block.id, path: ['body'] }" class="prose" />

A click turns the rendered text into an editor with the field's formatting options, in a toolbar above it. Edits reach the CMS form live, changes made there show up without moving the cursor, and Escape returns to block selection. The page keeps its styles: the editor renders the same HTML as B10cksRichText. The editor code is loaded only then, with a dynamic import, never during SSR or outside the visual editor. Users without edit permission, and older CMS versions, get the field opened in the CMS form instead. Render the page from usePreviewContent so the rest of the page sees the edits too.

You can also render rich text to an HTML string manually:

import type { RichTextDocument } from '@b10cks/vue/rich-text'
import { renderRichText } from '@b10cks/vue/rich-text'

const document: RichTextDocument = {
  type: 'doc',
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'Render me to HTML',
        },
      ],
    },
  ],
}

const html = renderRichText(document)

To extract plain text (useful for search indexing or meta descriptions), use renderRichTextAsText:

import type { RichTextDocument } from '@b10cks/vue/rich-text'
import { renderRichTextAsText } from '@b10cks/vue/rich-text'

const text = renderRichTextAsText(document)
// or with a custom block separator:
const inline = renderRichTextAsText(document, { blockSeparator: ' ' })

isRichTextEmpty(document) reports whether a document renders anything, so you can skip the wrapper markup for a field an editor cleared (which usually leaves an empty paragraph behind):

import { isRichTextEmpty } from '@b10cks/vue/rich-text'

if (!isRichTextEmpty(document)) {
  // render the section
}

For repeated rendering, use the factory:

import { createRichTextTextRenderer } from '@b10cks/vue/rich-text'

const renderer = createRichTextTextRenderer()
const text = renderer.render(document)

Internal link handler

b10cks internal links carry a url, title, anchor, and content (block ID) in their attrs. Pass an internalLinkHandler to customise how the href is generated — for example to prepend a route prefix or resolve content IDs via Vue Router:

<script setup lang="ts">
import { B10cksRichText } from '@b10cks/vue/rich-text'
import type { RichTextInternalLinkAttrs } from '@b10cks/vue/rich-text'
import { useRouter } from 'vue-router'

const router = useRouter()
</script>

<template>
  <B10cksRichText
    :document="document"
    :internal-link-handler="
      (attrs: RichTextInternalLinkAttrs) => router.resolve(attrs.url ?? '/').href
    "
  />
</template>

Or when rendering to a string:

import { renderRichText } from '@b10cks/vue/rich-text'

const html = renderRichText(document, {
  internalLinkHandler: (attrs) => `/app${attrs.url}`,
})

Return null or undefined from the handler to fall back to the default url/href attribute value.

Placeholder tokens

Pass a placeholderHandler to resolve {token} placeholders embedded in rich text to real values. Return null/undefined to leave a token unresolved.

import { renderRichText } from '@b10cks/vue/rich-text'

const html = renderRichText(document, {
  placeholderHandler: (key) => ({ companyName: 'b10cks' })[key],
})

B10cksRichText also exposes placeholderHandler as a prop, alongside internalLinkHandler.

URL safety

Link and image URLs from CMS content are validated against a scheme allowlist (default http/https/mailto/tel, plus relative URLs), so stored javascript: URLs cannot execute. Override the allowlist with the allowedSchemes prop / render option — see the @b10cks/richtext URL safety docs.

<B10cksRichText :document="doc" :allowed-schemes="['https', 'mailto']" />

Custom node and mark rendering

Pass nodes and marks to change the HTML of single node or mark types, for responsive images, heading anchors or embeds. Return null to keep the built-in output for that node. Node types the renderer doesn't know render their children unless you give them a renderer.

<script setup lang="ts">
import { B10cksRichText, escapeHtml, sanitizeUrl } from '@b10cks/vue/rich-text'
import type { RichTextNodeRenderer } from '@b10cks/vue/rich-text'

const nodes: Record<string, RichTextNodeRenderer> = {
  heading: ({ node, attrs, children }) => {
    const level = Number(attrs.level) || 2
    // `slugify` stands for your own slug helper.
    const id = slugify(node.content?.map((child) => child.text ?? '').join('') ?? '')
    return `<h${level} id="${escapeHtml(id)}">${children}</h${level}>`
  },
  image: ({ attrs }) =>
    `<img src="https://raw.githubusercontent.com/b10cks/sdk/HEAD/${escapeHtml(sanitizeUrl(attrs.src))}" alt="${escapeHtml(String(attrs.alt ?? ''))}" loading="lazy">`,
}
</script>

<template>
  <B10cksRichText
    :document="block.body"
    :nodes="nodes"
  />
</template>

Renderers are trusted code: their return value goes into the page as HTML. children and renderDefault() are already escaped; escape everything else you take from the document with escapeHtml, and pass URLs through sanitizeUrl. While a field is edited in place, the editor shows the built-in markup; your renderers apply again when editing ends.

Custom extensions are no longer used. The renderer is a custom, dependency-free implementation — it does not run TipTap. The extensions prop and createB10cksRichTextExtensions() remain as no-op stubs for backwards compatibility only.

Migrating from v1: renderRichText and B10cksRichText were previously exported from @b10cks/vue. Update all imports to @b10cks/vue/rich-text.

License

MIT

Related Packages

@b10cks/nuxt

b10cks Nuxt client SDK

6,778 2
@inneropen/marvin-sdk

Official TypeScript SDK for Marvin CMS

19,254 0
@easy-cms/nuxt

Nuxt module for Easy CMS

13,929 2
nuxt-directus-sdk

A Nuxt module for Directus with built-in authentication, realtime, file manageme...

9,595 13
@easy-cms/core

Embedded, code-first headless CMS for Nuxt and Next.js

25,029 2

Version History

Version Released Vue License
3.1.0 ^3.5.21 MIT
3.0.0 ^3.5.21 MIT
2.9.0 ^3.5.21 MIT
2.8.0 ^3.5.21 MIT
2.7.0 ^3.5.21 MIT