# ContentBind documentation Generated from https://contentbind.com/docs. Each section below is one page. # Quickstart From an empty project to an editable page in about ten minutes. ## Before you start You need a workspace and its public key. ContentBind is invite only, so the workspace is created for you; the key is in **Settings → Environments**, one per environment, and it is safe to ship in client code. It grants read access to published content and nothing else. ## 1. Install ```sh npm install @contentbind/sdk ``` ## 2. Point the SDK at the API ```sh # .env.local NEXT_PUBLIC_CONTENTBIND_API_URL=https://api.contentbind.com NEXT_PUBLIC_CONTENTBIND_KEY=pk_live_... ``` Both come from your workspace. The API URL rarely changes; left unset, the SDK talks to `https://api.contentbind.com`. ## 3. Render a page Fetch on the server, render on the client. The fetch is a plain HTTP call with no runtime, so it works in any framework; this example is the Next.js App Router. ```tsx // app/[[...slug]]/page.tsx import { fetchOneEntry } from '@contentbind/sdk' import { Content } from '@contentbind/sdk/react' const KEY = process.env.NEXT_PUBLIC_CONTENTBIND_KEY! export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) { const { slug } = await params const url = '/' + (slug ?? []).join('/') const entry = await fetchOneEntry({ model: 'page', publicKey: KEY, url }) if (!entry) return null return } ``` That is a working integration. Every page an author creates at a URL your app routes to now renders, and the built-in component library is available to them immediately. ## 4. Open the editor In ContentBind, create a page whose URL matches a route in your app, then open it. The canvas is your site in an iframe, loaded from your own origin, with `?contentbind.editor=1` appended. Nothing is proxied and nothing is re-hosted: what an author sees is your application running your code. If the canvas stays blank, run the doctor: ```sh npx contentbind doctor ``` It checks the key, the API, the delivery call and the editor origin, and tells you which of the four is wrong. ## 5. Register your own components The built-ins get a page shipped. Your components make it yours. ```tsx import { Content } from '@contentbind/sdk/react' import { PricingTable } from '@/components/PricingTable' const COMPONENTS = [ { component: PricingTable, name: 'PricingTable', friendlyName: 'Pricing table', category: 'Marketing', inputs: [ { name: 'heading', type: 'string', defaultValue: 'Plans' }, { name: 'highlight', type: 'string', options: [ { label: 'Starter', value: 'starter' }, { label: 'Growth', value: 'growth' }, ] }, ], }, ] ``` The component stays in your repository. ContentBind never sees its source, only its name and the inputs you declared: it sends props, your code renders them. See [Custom components](/docs/components) for the full input vocabulary. --- # How it works The architecture, and why the canvas is your own site. ## Three pieces **The platform** is a single Cloudflare Worker. It owns the API, authentication, publishing and delivery. Content lives in R2, metadata in D1, the read path is cached in KV, and live presence runs on a Durable Object. **The editor** is a React SPA. It holds no content of its own: it reads through the API and drives the canvas over `postMessage`. **The canvas** is your application. The editor loads your site in an iframe from your origin with the editor flag set, and the SDK inside it answers: here are my components, here is where each block rendered, this text was edited. ## Why an iframe and not a renderer The alternative is for the builder to re-implement your components, which means a second version of your design system that drifts from the first. Loading your real site means an author edits the thing that ships. Your CSS, your fonts, your data fetching, your framework version. The cost is a boundary: the editor and the canvas are different origins, so they talk in messages rather than function calls. Every message is origin-checked against the workspace's registered editor origin, in both directions. ## The read path ``` Visitor → your app → fetchOneEntry() → Worker → KV cache → R2 ``` Published content is written once to R2 and cached in KV keyed by environment, URL, and the resolved variant and personalization combination. The cache key includes those dimensions, which is why the editor warns when a page's variant count multiplies past a soft cap: every combination is a separate cached object. ## The write path Drafts autosave to R2 under a draft key. Publishing copies the draft to the published key for the chosen environments, writes a version row, and purges the affected cache keys. A version is a full snapshot, so restoring is a copy rather than a replay of changes. --- # Custom components Registering your own components and declaring their inputs. ## Registration A registration is your component plus a description of what an author may change about it. ```tsx { component: Hero, name: 'Hero', // stable id, stored in content friendlyName: 'Hero', // shown in the palette category: 'Marketing', // groups it in the palette icon: 'megaphone', inputs: [ /* ... */ ], } ``` `name` is stored in every block that uses the component, so renaming it orphans existing content. `friendlyName` is free to change. ## Input types | Type | Editor control | Prop value | |---|---|---| | `string` | text field, or a select when `options` is set | `string` | | `longText` | textarea | `string` | | `richText` | formatting toolbar | document object | | `number` | numeric field | `number` | | `boolean` | switch | `boolean` | | `color` | colour picker with your brand palette | `string` | | `file` | media library | URL `string` | | `list` | repeatable rows of `subFields` | array of objects | | `blocks` | a drop target for other blocks | rendered children | ```tsx inputs: [ { name: 'heading', type: 'string', required: true, defaultValue: 'Ship faster' }, { name: 'body', type: 'richText' }, { name: 'image', type: 'file', allowedFileTypes: ['png', 'jpg', 'webp'] }, { name: 'features', type: 'list', subFields: [ { name: 'title', type: 'string' }, { name: 'detail', type: 'longText' }, ] }, { name: 'children', type: 'blocks' }, ] ``` ## Slots An input of type `blocks` becomes a slot: authors drop other components inside yours. Your component receives it as a prop holding rendered children. ```tsx function Split({ heading, left, right }: { heading: string left: ReactNode right: ReactNode }) { return (

{heading}

{left}{right}
) } ``` An empty slot renders a placeholder in the editor and nothing at all for visitors, so a container is visible and droppable before it holds anything. ## Brand colours Hand the SDK your palette and it appears first in every colour picker, so authors pick from the brand instead of typing a hex from memory. ```tsx ``` ## Inline editing Double-clicking text on the canvas edits it in place. This works with no integration at all: the clicked element's text is matched back against the block's own input values, including values inside `list` inputs. When the match is ambiguous, because two inputs hold the same string or the element holds a concatenation, editing is declined rather than guessed at, and the author uses the inspector. --- # Preview and drafts Seeing unpublished content, and sharing it with people outside. ## Draft mode ```tsx import { fetchOneEntry, isPreviewing, getPreviewToken } from '@contentbind/sdk' import { cookies } from 'next/headers' const jar = await cookies() const entry = await fetchOneEntry({ model: 'page', publicKey: KEY, url, preview: isPreviewing(jar), previewToken: getPreviewToken(jar), }) ``` With `preview` set, delivery returns the draft and skips the cache entirely. ## Share links A preview link lets someone read a draft with no account: a designer, a legal reviewer, a client. Links can carry a password and an expiry, and can be revoked. The token grants one entry, not a workspace: it cannot be pointed at another page. --- # CLI Scaffolding an integration and diagnosing a broken one. ```sh npx contentbind init # scaffold the route, env file and a first component npx contentbind doctor # check an integration end to end ``` ## doctor Six checks, in the order things actually break: 1. The key is present and shaped like a key. 2. The API answers. 3. A real delivery call for a real URL returns content. 4. The editor origin registered for the workspace matches where the editor runs. 5. The SDK version in the project is one the API still speaks to. 6. The canvas responds to the editor handshake. Each failure names the file or setting to change. A blank canvas is nearly always one of the first four, and doctor will say which. --- # Publishing Environments, versions, approvals and scheduling. ## Environments A workspace starts with Production only. Add Staging, or a per-team preview environment, in **Settings → Environments**. Each has its own public key, its own published content, and its own cache. Protected environments require an approval before anything reaches them. ## Publishing Publishing takes the current draft and copies it to the environments you pick. It is a copy, not a build: the same bytes that were reviewed are the bytes that go live, and the cache purge is part of the same operation. A page is live in under a minute, usually a few seconds. ## Versions Every publish and every manual save (⌘S) writes a version. The history drawer shows them with who and when. Clicking one *previews* it on the canvas rather than applying it, and restoring goes through the normal edit path, so ⌘Z takes it back. Looking at an old version can never cost you your work. ## Approvals Authors can edit but not publish. Their primary action is **Submit for review**, which notifies the workspace's approvers. An approver sees the change summary and either publishes it or sends it back with a comment. ## Scheduling A publish can be dated. The Worker's cron checks each minute for anything due and publishes it with the same code path as a manual publish, so a scheduled release behaves identically to one someone clicks. --- # Experiments Running A/B tests with the flag tool you already use. ## ContentBind does not decide who sees what Your experimentation tool owns the flag, the bucketing and the results. ContentBind owns the content each bucket sees. That split is deliberate: teams already have a tool for this and do not want a second source of truth for what is running. Supported flag providers: LaunchDarkly, Statsig, GrowthBook, Split, Optimizely, Eppo, and a built-in bucketer for teams with no external tool. ## Starting one In the editor, open **Experiments → Start an experiment**. It asks for two things: the flag key, exactly as it exists in your tool, and the list of variants. The first variant is the default, which is what anyone unassigned, or assigned to a variant this page has never heard of, falls back to. There is no hypothesis field and no sample-size calculator. Those live in the tool that owns the flag. ## Authoring per variant With an experiment running, the topbar gets a variant switcher. Switch to `variant_b`, edit the headline, switch back: the control is unchanged. Any input can differ per variant, and only the inputs you actually change are stored, so a variant is a diff rather than a copy of the page. Blocks can also be shown only for a variant, which is how you test a section existing at all. ## Serving Pass the visitor's assigned variants when you fetch: ```tsx const entry = await fetchOneEntry({ model: 'page', publicKey: KEY, url, variants: { hero_headline_test: assignedVariant }, }) ``` Delivery resolves the content server-side and caches per combination, so a visitor gets fully-formed HTML with no flash of the control. ## Ending one Ending an experiment promotes one variant's content into the page proper and removes the rest. The promotion is a normal edit, so it lands on the version history and can be undone. --- # Personalization Server-side targeting on attributes you declare. ## Attributes Personalization runs on attributes your app already knows: plan, region, account age, whatever you pass. Declare them once in workspace settings, with example values, and they become pickable in the editor's rule builder rather than free-form strings authors have to spell correctly. ## Rules A block can carry a rule: show when `plan` is `pro`, hide when `region` is `EU`. Rules are evaluated at delivery, server-side, before the HTML is cached. The visitor never receives content meant for someone else, and there is no client-side flicker. ## Previewing The editor's **Preview as a visitor** control sets attributes for the canvas. With none set, personalized blocks stay visible but dimmed and labelled, so authoring a page does not mean hunting for blocks that vanished. With attributes set, the canvas shows exactly what that visitor gets. ## Passing attributes ```tsx const entry = await fetchOneEntry({ model: 'page', publicKey: KEY, url, attributes: { plan: user.plan, region: geo.country }, }) ``` Every distinct combination is a distinct cache key. The editor tracks the total and warns before it multiplies past the point where the cache stops helping. --- # Delivery API The HTTP surface behind the SDK. The SDK is a thin wrapper. Anything it does can be done with `fetch`, which is what non-JavaScript stacks do. ## Fetch one entry ```http GET /api/delivery/:publicKey/entry?model=page&url=/pricing ``` Query parameters: | Name | Meaning | |---|---| | `model` | content model, usually `page` | | `url` | the page's URL path | | `variants` | JSON object of flag key to variant | | `attributes` | JSON object of personalization attributes | | `preview` | `1` to read the draft; requires a preview token | Returns the resolved entry: variant content merged, personalization applied, hidden blocks removed. ## List entries ```http GET /api/delivery/:publicKey/entries?model=page&limit=50 ``` For sitemaps and index pages. Returns metadata, not full content. ## Redirects ```http GET /api/delivery/:publicKey/redirect?url=/old-path ``` Returns the target and status code, or 404. Authors manage redirects alongside pages so a URL change does not need a deploy. ## Errors Errors come back as `{ error: { code, message } }` with a real status code. The SDK's fetch helpers return the error object rather than throwing, so a delivery hiccup renders your fallback instead of a 500. --- # Security model Keys, isolation, and what the bridge will and will not do. ## Two kinds of key **Public keys** ship in client code. They read published content for one environment. They cannot write, cannot read drafts without a preview token, and cannot see other environments. **Sessions** are httpOnly cookies signed with `AUTH_SECRET`. Every write goes through one, with a role check per workspace. ## Workspace isolation Content is sharded across pod databases. The shard opener takes the workspace id and asserts it inside the query, not just in the routing table, so a bug in routing produces an error rather than another workspace's rows. ## The editor bridge The editor and the canvas run on different origins and exchange `postMessage` events. Both sides check `event.origin` against the workspace's registered editor origin. A page that loads the SDK cannot be driven by an arbitrary opener. Content edited inline is sent as values, never as markup, and rich text is stored as a document rather than HTML: a `javascript:` link is refused at the editor and never reaches storage. ## Roles | Role | Can | |---|---| | Owner | everything, including billing and deleting the workspace | | Admin | manage members, environments and settings | | Editor | edit and publish | | Author | edit and submit for review | | Viewer | read | ---