@bettercms-ai/sdk
Typed JavaScript/TypeScript client for the BetterCMS Delivery API.
Installation
npm install @bettercms-ai/sdk
# or
pnpm add @bettercms-ai/sdk
# or
yarn add @bettercms-ai/sdk
Quick start
import { BetterCMS } from "@bettercms-ai/sdk";
const client = BetterCMS.site({ workspace: "my-workspace" });
// Fetch a single page
const page = await client.getContent("home");
console.log(page.entry.title);
// List pages (paginated)
const result = await client.listContent({ page: 1, perPage: 20 });
console.log(result.items);
console.log(`Next page? ${result.hasNextPage}`);
// Iterate all pages automatically
for await (const item of client.listContentAll()) {
console.log(item.entry.title);
}
// Collect with a limit
const top = await client.listContentAll().all({ limit: 500 });
With an API key
const client = BetterCMS.site({
workspace: "my-workspace",
apiKey: process.env.BETTERCMS_API_KEY,
});
TypeScript
This package is written in TypeScript and ships its own type declarations. No separate @types/ package needed. All types from @bettercms-ai/types are re-exported.
Error handling
The SDK throws BetterCMSError on failures. Each error has a semantic code:
import { BetterCMSError, ErrorCodes } from "@bettercms-ai/sdk";
try {
const page = await client.getContent("does-not-exist");
} catch (err) {
if (err instanceof BetterCMSError) {
switch (err.code) {
case ErrorCodes.CONTENT_NOT_FOUND:
// 404
break;
case ErrorCodes.UNAUTHORIZED:
// 401
break;
case ErrorCodes.FORBIDDEN:
// 403
break;
case ErrorCodes.RATE_LIMITED:
// 429
break;
case ErrorCodes.INTERNAL_ERROR:
// 5xx
break;
case ErrorCodes.NETWORK_ERROR:
// Network failure
break;
}
}
}
Draft previews and visual editing
A client created with perspective: "drafts" reads unpublished content — through a
content:read:draft API key, or through a previewToken minted for one document.
Draft reads also carry stega provenance: invisible zero-width characters appended to every
prose value, naming the entry and field it came from. That is what lets the dashboard's canvas
(and the <VisualEditing> overlay) map a rendered string back to its editor without guessing at
its text. It is on by default for perspective: "drafts" from 1.15.0 — before that it had to
be asked for, and a preview build that did not know to ask rendered anonymous strings.
const draft = createClient({
apiUrl,
workspace: "my-workspace",
apiKey: process.env.BETTERCMS_DRAFT_KEY,
perspective: "drafts", // stega is on
});
// Opt out — e.g. when diffing or checksumming draft content, where the invisible
// characters would show up as differences.
const plain = createClient({
apiUrl,
workspace: "my-workspace",
apiKey: process.env.BETTERCMS_DRAFT_KEY,
perspective: "drafts",
stega: false,
});
Published reads never carry provenance, whatever stega is set to: the payload must not reach a
production build. Strip it yourself with stripStega(value) if you need the clean string, and
decode it with decodeStega(value).
Rich text
A rich-text field arrives as { format, value, html } (or as a bare string, if the field was
switched from text). Render html with rich(), or render the structure (value) yourself:
import { rich, plain, portableText, embeds, resolved } from "@bettercms-ai/sdk";
const entry = await client.getEntry<{ body: RichTextValue }>("launch");
rich(entry.data.body); // HTML for set:html / dangerouslySetInnerHTML: everything drawn
plain(entry.data.body); // text for <title>, meta description, alt
// Rendering the structure with @bettercms-ai/ui (or your own components):
<PortableText
value={portableText(entry.data.body) ?? entry.data.body}
embeds={embeds(entry.data.body) ?? undefined}
resolved={resolved(entry.data.body, entry) ?? undefined}
/>;
Related posts and author bios are resolved by the server for each response, from the published
entries, and never stored. An envelope value carries what it resolved to (value.resolved); a
Body stored as a bare block array cannot, so the entry carries the merged map beside data
(entry.resolved). resolved(value, entry) returns the value's own map, else the entry's.
API
BetterCMS.site(options)
| Option | Type | Required | Default |
|---|---|---|---|
workspace | string | Yes | — |
apiKey | string | No | — |
baseUrl | string | No | https://api.bettercms.ai/api/v1/delivery |
Returns a BetterCMSDeliveryClient instance.
client.getContent(slug)
Fetches a single published page by slug.
const content = await client.getContent("about-us");
client.listContent(options?)
Returns a paginated result object.
const result = await client.listContent({ page: 1, perPage: 20 });
// result.items, result.hasNextPage, result.totalPages, ...
client.listContentAll(options?)
Returns an async iterator that transparently follows pagination.
for await (const item of client.listContentAll()) { ... }
// With a limit:
const items = await client.listContentAll().all({ limit: 100 });