Skip to content

Latest commit

 

History

History

react

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@b10cks/react

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

Installation

npm install @b10cks/react @b10cks/client @b10cks/richtext

Or let the CLI install the packages, add a provider, and write .env for you:

npx @b10cks/cli init

Usage

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

# .env
VITE_B10CKS_TOKEN=your-access-token
import { B10cksProvider, useB10cksApi } from '@b10cks/react'

function App() {
  return (
    <B10cksProvider
      apiClientOptions={{
        token: import.meta.env.VITE_B10CKS_TOKEN ?? '',
        baseUrl: 'https://api.b10cks.com/api',
      }}
    >
      <Page />
    </B10cksProvider>
  )
}

function Page() {
  const { useContent } = useB10cksApi()
  const content = useContent('home')

  if (content.pending) return <div>Loading...</div>
  return <pre>{JSON.stringify(content.data, null, 2)}</pre>
}

Live preview & visual editing

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

Provider options

<B10cksProvider
  apiClientOptions={{ token, baseUrl }}
  // 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}
  // Restrict the preview bridge handshake to known editor origins.
  allowedOrigins={['https://app.b10cks.com']}
>
  …
</B10cksProvider>

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

Hooks

import {
  useEditable,
  useEditableField,
  usePreviewContent,
  usePreviewSelection,
} from '@b10cks/react'

function Hero({ block }: { block: HeroBlock }) {
  // Make the element selectable; click selects it and the editor highlights/scrolls to it.
  const ref = useEditable<HTMLElement>(block.id, block.block)

  // Inline-edit a simple string field (contenteditable, streams edits back):
  const headlineRef = useEditableField<HTMLHeadingElement>({ id: block.id, field: 'headline' })

  // Rich text / complex fields: deep-select so the editor opens its own editor.
  const bodyRef = useEditableField<HTMLDivElement>({ id: block.id, path: ['body'], mode: 'select' })

  return (
    <section ref={ref}>
      <h1 ref={headlineRef}>{block.headline}</h1>
      <div ref={bodyRef}>
        <B10cksRichText document={block.body} />
      </div>
    </section>
  )
}

For whole-tree reactive updates — including nested and rich text fields — wrap your fetched content in usePreviewContent. It returns the content live-updated from the editor, and initial unchanged outside preview mode. When initial changes identity (a route change, revalidation, or locale switch under a persistent layout), the preview resets to the new tree instead of keeping the first one:

function Page({ initialContent }: { initialContent: PageContent }) {
  const content = usePreviewContent(initialContent)
  return <B10cksComponent block={content} />
}

Broken blocks

B10cksComponent wraps each block in an error boundary, so one block 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 the block renders nothing; React logs the error as usual. Like any React error boundary it catches rendering errors on the client, not in event handlers or during server rendering.

Typed components

b10cks generate types emits B10cksBlock, the union of your block types. Type the components map with it, and each component gets its own block's props:

import { B10cksComponent, type B10cksComponents } from '@b10cks/react'
import type { B10cksBlock } from '@/b10cks/types'

const components: B10cksComponents<B10cksBlock> = {
  hero_section: ({ block }) => <h1>{block.headline}</h1>, // block: B10cksHeroSection
  card: Card,
}

<B10cksComponent block={block} components={components} />

Typed maps are keyed by block slug. Untyped maps keep working as before, including PascalCase keys.

usePreviewSelection(blockId) returns { isSelected, isHovered } if you prefer to drive your own highlight styling.

Link anchors

An internal link's anchor is the id of a block on the target page. resolveB10cksLink builds the href with params and anchor, /about?ref=nav#01kh6h981yh1s5z7s3f80wmrw2. Rich text internal links get the anchor appended too.

B10cksComponent renders id={block.id} on its wrapper div. An id prop wins, and anchor={false} turns it off. For blocks rendered without it, spread blockAnchorAttrs:

import { blockAnchorAttrs, resolveB10cksLink } from '@b10cks/react'

function Teaser({ block }: { block: TeaserBlock }) {
  return (
    <section {...blockAnchorAttrs(block)}>
      <a href={resolveB10cksLink(block.link)?.href}>…</a>
    </section>
  )
}

Block ids are ULIDs and can start with a digit. Use document.getElementById(id), or querySelector('#' + CSS.escape(id)).

Rich Text

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

import { B10cksRichText } from '@b10cks/react'

type PageBlock = {
  body?: {
    type: 'doc'
    content?: unknown[]
  } | null
}

export function PageBody({ block }: { block: PageBlock }) {
  return (
    <B10cksRichText
      document={block.body}
      className="prose"
    />
  )
}

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'] }}
  className="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 on the server or outside the visual editor. Users without edit permission, and older CMS versions, get the field opened in the CMS form instead.

editable uses hooks, so it needs a client component, like the rest of live preview (usePreviewContent). Without it, B10cksRichText stays hook-free and renders in Server Components.

If you need the rendered HTML string on the server or inside your own component logic, use renderRichTextHtml:

import { renderRichTextHtml } from '@b10cks/react'

const html = renderRichTextHtml(block.body)

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

import { renderRichTextAsText } from '@b10cks/react'

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

options accepts an internalLinkHandler (to customise how internal-link hrefs are generated), a placeholderHandler (to resolve {token} placeholders to real values), and allowedSchemes (the URL scheme allowlist for link/image URLs):

import { B10cksRichText } from '@b10cks/react'

export function CustomBody({ document }: { document: any }) {
  return (
    <B10cksRichText
      document={document}
      options={{
        internalLinkHandler: (attrs) => `/app${attrs.url ?? ''}`,
        placeholderHandler: (key) => ({ companyName: 'b10cks' })[key],
      }}
    />
  )
}

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

The renderer is a custom, dependency-free implementation — it does not run TipTap, and there is no extensions option.

License

MIT