@b10cks/nuxt
@b10cks/nuxt
Nuxt 4 module for integrating b10cks, the open-source headless CMS with a composable block-based content API.
Installation
npm install @b10cks/nuxt @b10cks/vue @b10cks/client @b10cks/richtext
Or let the CLI install the packages, wire up nuxt.config.ts, and write .env for you:
npx @b10cks/cli init
Setup
Put the access token in .env:
# .env
NUXT_PUBLIC_B10CKS_ACCESS_TOKEN=your-access-token
Nuxt maps NUXT_PUBLIC_* onto runtimeConfig.public automatically, so the token
never has to appear in your config file — and never lands in version control:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@b10cks/nuxt'],
b10cks: {
apiUrl: 'https://api.b10cks.com/api',
componentsDir: '~/b10cks',
// Optional: offset applied when a selected block is scrolled into view, so
// selection clears a fixed app header (number → px, or a string like '5rem').
scrollOffset: 80,
// Optional: restrict the preview bridge handshake to known editor origins.
allowedOrigins: ['https://app.b10cks.com'],
// Optional transport limits, for the app and `useB10cksServerApi()`:
timeoutMs: 5000, // per attempt, including reading the body
retries: 2, // network errors, timeouts, 429 and 5xx on GET requests
maxConcurrency: 6, // pages fetched at once by `allPages` requests
},
})
accessToken can also be set directly in the b10cks block, but prefer the env
var so the token stays out of the repository.
scrollOffset can also be set purely in CSS — :root { --b10cks-scroll-offset: 80px }.
Usage
Each composable returns the same object as Nuxt's useAsyncData() — destructure data, pending, error, and refresh as needed.
Every query waits for the space revision, which is fetched once per server request, so the first page load already reads the latest published content. Slugs, names and params accept refs and getters: the async-data key follows them, so a change fetches the new query. When Nuxt cancels a query, its request is aborted too. A custom key replaces the derived one, so make it a getter as well when the inputs change.
const route = useRoute()
const vid = useB10cksVersion()
const { data: page } = await useContent(
() => route.params.slug as string,
() => ({ vid: vid.value })
)
If your app runs inside the visual editor, wrap the fetched content in
usePreviewContent so edits stream into the page. It is a
no-op in production, so there is no reason not to.
// Single content entry by slug
const { useContent } = useB10cksApi()
const { data: page, error } = await useContent('home')
if (error.value) throw error.value
// With query params (language, vid, etc.)
const { data: page } = await useContent('home', {
language_iso: 'de',
vid: 'published', // or 'draft'
})
// List of content entries — params accepts the same filter object as dataApi.getContents()
const { useContents } = useB10cksApi()
// Plain params
const { data: items } = await useContents({ language_iso: 'en', vid: 'published' })
// Typed filter object (no wire-format string hacks needed)
const { data: people } = await useContents({
language_iso: 'en',
vid: 'published',
filter: {
canonical_id: { in: ['id-1', 'id-2'] },
},
})
// Breadcrumb trail of an entry, root first — one request, no tree walking
const { useBreadcrumb } = useB10cksApi()
const { data: trail } = await useBreadcrumb('products/shoes', { language: 'de' })
// Each level carries `name`, `path`, `depth`, `is_root`, `is_current` and
// `is_fallback` (true when the level had no translation and fell back).
// Redirects and config
const { useRedirects, useB10cksConfig } = useB10cksApi()
const redirects = await useRedirects()
const { data: config, pending, error, refresh } = await useB10cksConfig()
// Pass a getter so the config refetches when the locale changes
const { config } = await useB10cksConfig(() => ({ language_iso: locale.value }))
useB10cksConfig takes language_iso, like every other content param.
language still works as a deprecated alias.
useB10cksVersion()
The visual editor appends ?b10cks_vid to preview a draft. The composable
normalizes that query param (which Vue Router types as string | string[] | null) to a version string defaulting to published:
const vid = useB10cksVersion()
const { data: page } = await useContent('home', () => ({ vid: vid.value }))
Server routes and middleware
Nitro has no access to the Vue injection the module's plugin sets up, so server
code gets its own entry point. useB10cksServerApi() is auto-imported in the
server bundle and returns the same B10cksDataApi — including its paginated
getRedirects, getSitemap and getNamedSitemap, and their built-in caching:
// server/middleware/redirects.ts
export default defineEventHandler(async (event) => {
const url = getRequestURL(event)
if (url.searchParams.has('b10cks_vid')) return
const redirects = await useB10cksServerApi().getRedirects({}, { allPages: true })
const hit = redirects[url.pathname]
if (hit) return sendRedirect(event, hit.target, hit.status_code || 301)
})
The instance is shared per space so its caches outlive a single request. That
also means the revision is shared — call syncRevision() when a route must
read the newest published state.
The helpers use Nuxt's useAsyncData() under the hood, so requests participate in SSR payload serialization and are not refetched during hydration. Each helper derives a stable async-data key from its inputs — no manual key needed.
B10cksComponent and directives
B10cksComponent, v-editable, and v-editable-field are available globally after registering the module. componentsDir in the config tells the module where your block components live; it auto-registers them by block name. A block that throws while rendering doesn't take down the page: see @b10cks/vue.
<script setup lang="ts">
const { useContent } = useB10cksApi()
const { data: entry } = await useContent('home')
// An entry's `content` object has no `id` — the id lives on the entry — so pass
// it through `toRootBlock` (auto-imported). Without it the root block cannot be
// selected in the visual editor and `v-editable` no-ops on it.
const block = computed(() => toRootBlock(entry.value))
</script>
<template>
<!-- Renders the component matching block.block from componentsDir -->
<B10cksComponent
v-if="block"
:block="block"
:content="entry"
/>
</template>
B10cksRichText is not auto-registered. Import it in components that render rich text:
import { B10cksRichText } from '@b10cks/nuxt/rich-text'
<!-- Mark a block as selectable; it also live-updates in place while editing -->
<div v-editable="block">…</div>
<!-- Inline-edit a simple string field -->
<h1 v-editable-field="{ id: block.id, field: 'header' }">{{ block.header }}</h1>
<!-- Rich text: editors edit it in place, with the field's formatting options -->
<B10cksRichText :document="block.body" :editable="{ id: block.id, path: ['body'] }" />
<!-- Other complex fields: deep-select so the editor opens its own editor -->
<LinkCard :link="block.link" v-editable-field="{ id: block.id, path: ['link'], mode: 'select' }" />
In preview, clicking a block selects it and prevents its links and buttons from firing. Clicking an inline text field selects its block while keeping the caret. For accordions, tabs, and other components that need click handlers, use v-editable.interactive="block". It selects the block and allows its handlers, while links and form submit buttons still cannot navigate. Alt-click passes through for any block.
The rich text editor loads only when an editor clicks the field in the visual editor, never during SSR or for visitors. See @b10cks/vue for details.
Link anchors
An internal link's anchor is the id of a block on the target page. resolveB10cksLink (auto-imported) builds the href with params and anchor, /about?ref=nav#01kh6h981yh1s5z7s3f80wmrw2. Rich text internal links get the anchor appended too.
v-editable renders id="<block.id>" on its element, server-side and in production, so the fragment has a target. An id you set on the same element wins after hydration, but the server HTML can't see it; add .noanchor when you set your own id. For blocks without the directive, bind blockAnchorAttrs (auto-imported):
<section v-bind="blockAnchorAttrs(block)">…</section>
<NuxtLink :to="resolveB10cksLink(block.link)?.href">…</NuxtLink>
Block ids are ULIDs and can start with a digit. Use document.getElementById(id), or querySelector('#' + CSS.escape(id)).
Live preview
For whole-tree reactive updates while editing — including nested and rich text fields — wrap your content in usePreviewContent (auto-imported by the module):
<script setup lang="ts">
const { useContent } = useB10cksApi()
const { data } = await useContent('home')
// Pass a getter (or ref) so the preview resets when content is refetched
// on a route/locale change instead of keeping the first tree. `toRootBlock`
// carries the entry id into the root block so root-level edits are matched.
const content = usePreviewContent(() => toRootBlock(data.value))
</script>
<template>
<B10cksComponent :block="content" />
</template>
If you need to override Nuxt's cache identity behavior, pass a custom key as a third argument to any composable.
Page translations
usePageTranslations maintains a reactive locale → path map built from an IBContent entry and its translations array. Use it to drive language-switcher links without any extra API calls.
<script setup lang="ts">
const { useContent } = useB10cksApi()
const { translations, setFromContent, clear } = usePageTranslations()
const { data: content } = await useContent(slug, { language_iso: locale })
if (content.value) setFromContent(content.value)
onBeforeRouteLeave(() => clear())
</script>
<template>
<!-- translations.value: { en: '/en/about', de: '/de/ueber-uns' } -->
<NuxtLink
v-for="(path, lang) in translations"
:key="lang"
:to="path"
>{{ lang }}</NuxtLink
>
</template>
setFromContent uses buildLocalizedPath from @b10cks/client internally, so paths are always correctly normalized and locale-prefixed. setTranslations lets you set the map manually when you need full control.
Rich text usage
Use B10cksRichText to render a b10cks RichTextDocument (a TipTap/ProseMirror-style JSON document) on the server and client with a dependency-free renderer. Import it explicitly; the module does not auto-register this component.
<script setup lang="ts">
import { B10cksRichText, type RichTextDocument } from '@b10cks/nuxt/rich-text'
const { useContent } = useB10cksApi()
const { data: page, pending } = await useContent<{ body?: RichTextDocument }>('home')
</script>
<template>
<div v-if="pending">Loading…</div>
<B10cksRichText
v-else
:document="page?.content?.body"
class="prose"
/>
</template>
If you need to render HTML manually, you can use renderRichText:
import { renderRichText } from '@b10cks/nuxt/rich-text'
const html = renderRichText(document)
Use @b10cks/nuxt/rich-text in app code. It exports the Vue rich text component, rendering helpers, and their types without importing @nuxt/kit. The package root is the Nuxt module entry for nuxt.config.ts. Its existing rich text exports remain available for compatibility, but importing them also loads module tooling.
@nuxt/kit remains a package dependency because Nuxt loads the module in consuming projects. It is only needed by the module entry, not the rich text entry.
License
MIT
Related Packages
Read published Mapled content in a Nuxt site — a server-side client with a tag c...
A Nuxt module for Directus with built-in authentication, realtime, file manageme...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 4.2.0 | ^3.0 | MIT | |
| 4.1.0 | ^3.0 | MIT | |
| 4.0.0 | ^3.0 | MIT | |
| 3.6.1 | ^3.0 | MIT | |
| 3.6.0 | ^3.0 | MIT | |
| 3.5.0 | ^3.0 | MIT | |
| 3.4.1 | ^3.0 | MIT | |
| 3.4.0 | ^3.0 | MIT | |
| 3.3.0 | ^3.0 | MIT | |
| 3.2.5 | ^3.0 | MIT | |
| 3.2.4 | ^3.0 | MIT | |
| 3.2.3 | ^3.0 | MIT | |
| 3.2.2 | ^3.0 | MIT | |
| 3.2.1 | ^3.0 | MIT | |
| 3.2.0 | MIT | ||
| 3.1.0 | MIT | ||
| 3.0.6 | MIT | ||
| 3.0.5 | MIT | ||
| 3.0.4 | MIT | ||
| 3.0.3 | MIT | ||
| 3.0.2 | MIT | ||
| 3.0.1 | MIT | ||
| 3.0.0 | MIT | ||
| 2.2.5 | MIT | ||
| 2.2.4 | MIT | ||
| 2.2.3 | MIT | ||
| 2.2.2 | MIT | ||
| 2.2.1 | MIT | ||
| 2.2.0 | MIT | ||
| 2.1.0 | MIT |
Showing the latest 30 of 43. See every release on npm