Installation
To get started, you can follow the official guide or in summary:
pnpm add @nuxt/content
yarn add @nuxt/content
npm install @nuxt/content
bun add @nuxt/content
Then, add the @nuxt/content module in your nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'@bitrix24/b24ui-nuxt',
'@nuxt/content'
],
css: ['~/assets/css/main.css']
})
@nuxt/content after @bitrix24/b24ui-nuxt in the modules array, otherwise the prose components will not be available.Configuration
When using Tailwind CSS classes in your markdown content files, you need to ensure Tailwind can detect and generate the necessary utility classes. By default, Tailwind's automatic content detection might not pick up classes written in markdown files.
To fix this, use the @source directive in your CSS file to explicitly include your content directory:
@import "tailwindcss";
@import "@bitrix24/b24ui-nuxt";
@source "../../../content/**/*";
This ensures that:
- Tailwind scans all markdown files in your content directory
- Any utility classes used in your markdown are included in the final CSS
- Dynamic classes in MDC components or custom Vue components within your content work properly
@source "../../../content/docs/**/*.md"- Only scan markdown in the docs folder@source "../../../content/**/*.{md,yml}"- Include both markdown and YAML files
Client-only apps (SPA)
Prose components work in a client-only app (ssr: false) exactly as they do with server rendering — nothing about markdown rendering is disabled in this mode. Nuxt Content moves its database into the browser instead: on the first query the client downloads a dump from /__nuxt_content/<collection>/sql_dump.txt and answers every query locally through WASM SQLite.
localStorage. Anything you put in a collection that a client-rendered page queries is readable by every visitor, so keep drafts and non-public documents in a separate collection.The build still needs SQLite
The dump is produced at build time, so the build needs a working SQLite adapter even though the deployed app never runs one. This is where a client-only build usually breaks: the default adapter, better-sqlite3, is a native module that has to be compiled during install. Without it the build fails with Could not locate the bindings file and produces no dump, so no content reaches the page.
On Node.js 22.5 or later you can skip the native binding entirely and use Node's built-in node:sqlite. One line, no compiler, nothing package-manager specific:
export default defineNuxtConfig({
content: {
experimental: {
sqliteConnector: 'native'
}
}
})
This is what this documentation site is built with.
On older Node.js better-sqlite3 has to compile, and pnpm does not run a dependency's install scripts unless you say so:
allowBuilds:
better-sqlite3: true
Running pnpm approve-builds writes that entry for you, which is the version-proof way to do it — pnpm 11 removed the older onlyBuiltDependencies and ignoredBuiltDependencies settings in favour of this map. It also fails the install outright with ERR_PNPM_IGNORED_BUILDS; pnpm 10 lets the install succeed and only prints a note, so that is the version where this is easy to miss. Approve the one package you need rather than reaching for pnpm approve-builds --all or dangerouslyAllowAllBuilds: an install script runs arbitrary code on your machine and in CI, which is what that default protects you from.
npm and yarn run install scripts by default, so a better-sqlite3 failure there is a missing C++ toolchain rather than a blocked script — install one, or switch the connector as above.
When a page renders nothing
Work outwards from the build — each step has a different answer:
- Is the dump in the output? Look for
.output/public/__nuxt_content/<collection>/sql_dump.txtafter a build. If it is missing, the build never produced it, and the SQLite adapter above is where to look. - Does the browser get it? The Network tab should show that file being fetched on the first query. A 404 there usually means the site is served from a sub-path that
app.baseURLdoes not account for. - Does the query match anything? That fetch throws rather than returning nothing, so a missing dump surfaces in
error, not indata. An emptydatawith no error is a different problem, usually a path filter that matches no document.
<script setup lang="ts">
// `content` is the default collection name — use yours if you renamed it
const { data: all, error } = await useAsyncData('all', () => queryCollection('content').all())
// `content/index.md` has the path `/`, not `/index`
if (import.meta.dev) {
console.log(error.value, all.value?.length, all.value?.map(item => item.path))
}
</script>
private: true are deliberately left out of the browser dump and go through a server route instead, so they need a running server — a fully static export cannot answer them.Components
You might be using @nuxt/content to build a documentation. To help you with that, we've built some components that you can use in your pages:
- a built-in full-text search command palette with ContentSearch, replacing the need for Algolia DocSearch
- a sticky Table of Contents with the ContentToc component
- a prev / next navigation with the ContentSurround component
Typography
Bitrix24 UI provides its own custom implementations of all prose components for seamless integration with @nuxt/content. This approach ensures consistent styling, complete control over typography, and perfect alignment with the Bitrix24 UI design system so your content always looks and feels cohesive out of the box.
Utils
Bitrix24 UI ships helpers to reshape the data returned by @nuxt/content into the format its components expect.
mapContentNavigation
This util will map the navigation from queryCollectionNavigation and transform it recursively into an array of objects that can be used by various components.
mapContentNavigation(navigation, options?)
navigation: The navigation tree (array of ContentNavigationItem).options(optional):labelAttribute: (string) Which field to use as label (titleby default)deep: (number or undefined) Controls how many levels of navigation are included (undefinedby default, includes all levels)
Example: As shown in the breadcrumb example below, it's commonly used to transform the navigation data into the correct format.
<script setup lang="ts">
import { mapContentNavigation } from '@bitrix24/b24ui-nuxt/utils/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(navigation?.value, page.value?.path, { indexAsChild: true })).map(({ icon, ...link }) => link), { deep: 0 })
</script>
<template>
<B24Page>
<B24PageHeader v-bind="page">
<template #headline>
<B24Breadcrumb :items="breadcrumb" />
</template>
</B24PageHeader>
</B24Page>
</template>