The shape you pass to cms0<T>() becomes the shape of the client. The CLI
writes local descriptor types under .cms0/generated, so every accessor is
fully inferred from your schema.
import { cms0 } from "@cms0/cms0";
type RootSchema = {
homePage: {
headline: string;
seo: { title: string; description: string };
};
articles: Array<{ title: string; slug: string }>;
};
export const cms = cms0<RootSchema>({
apiConfig: {
baseUrl: process.env.CMS0_API_BASE_URL,
key: process.env.CMS0_API_KEY,
},
});Accessors are generated from the TypeScript shape that the CLI publishes. If an accessor is missing, run cms0 dev or cms0 build again.
Reading root content
Root content is a singleton. Call the accessor as a function to read it.
const homePage = await cms.homePage();
// ^? { headline: string; seo: { title: string; description: string } }Reading collections
Root array fields return all items when called as a function.
const articles = await cms.articles();
// ^? Array<{ title: string; slug: string }>Reading models
Reusable model declarations are available under cms.models.
const posts = await cms.models.Post();
// ^? Post[]
const post = await cms.models.Post.byId("post_id");
// ^? Post | nulllist() and get() are aliases for the callable accessor and byId():
const posts = await cms.models.Post.list();
const post = await cms.models.Post.get("post_id");Querying with .where()
Use .where() to filter models and root arrays by field values. The clause is
fully type-inferred: keys are constrained to actual fields, and operator values
are constrained to the field’s type.
Exact match
const drafts = await cms.models.Post.where({ status: "draft" });
const featured = await cms.articles.where({ slug: "featured" });String operators
await cms.models.Post.where({ title: { contains: "launch" } });
await cms.models.Post.where({ title: { startsWith: "How to" } });
await cms.models.Post.where({ title: { endsWith: "guide" } });
await cms.models.Post.where({ title: { in: ["Post A", "Post B"] } });
await cms.models.Post.where({ title: { notIn: ["Draft"] } });
await cms.models.Post.where({ title: { isNull: true } });Number operators
await cms.models.Post.where({ views: { gt: 100 } });
await cms.models.Post.where({ views: { gte: 50 } });
await cms.models.Post.where({ views: { lt: 1000 } });
await cms.models.Post.where({ views: { lte: 500 } });
await cms.models.Post.where({ views: { between: [10, 100] } });
await cms.models.Post.where({ views: { in: [0, 100, 1000] } });Boolean operators
await cms.models.Post.where({ published: true });
await cms.models.Post.where({ published: { eq: false } });
await cms.models.Post.where({ published: { isNull: true } });Dot-path fields
Filter on nested object fields using dot notation:
await cms.models.Post.where({ "seo.title": { contains: "guide" } });
await cms.homePage.where({ "author.name": { startsWith: "J" } });Combining conditions
Use AND, OR, and NOT to build complex queries:
await cms.models.Post.where({
AND: [
{ status: "published" },
{ views: { gt: 100 } },
],
});
await cms.models.Post.where({
OR: [
{ status: "draft" },
{ status: "review" },
],
});
await cms.models.Post.where({
NOT: { status: "archived" },
});Where with options
.where() accepts the same options as the callable accessor:
const results = await cms.models.Post.where(
{ status: "published" },
{
fields: ["title", "slug"],
graph: { pageSize: 10, orderBy: "views", orderDir: "desc" },
response: "envelope",
},
);
// results: { items: Post[], total: number }.whereFirst()
When you expect a single result, use .whereFirst() instead of .where(). It
returns the first matching item or null — no need to index into an array.
const post = await cms.models.Post.whereFirst({ slug: "launch-notes" });
// ^? Post | null
if (post) {
console.log(post.title);
}Works on root arrays too:
const article = await cms.articles.whereFirst({ slug: "featured" });Nested field access
Chain into nested objects and arrays to read specific fields:
const headline = await cms.homePage.headline();
// ^? string
const seoTitle = await cms.homePage.seo.title();
// ^? stringField projection
Control which fields are returned using fields (include) and exclude:
const partial = await cms.homePage({
fields: ["headline", "seo.title"],
});
// ^? { headline: string; seo: { title: string } }
const withoutSeo = await cms.homePage({
exclude: ["seo"],
});Pagination and ordering
Use graph options to control pagination and sorting for array fields:
const page = await cms.homePage({
graph: {
page: 2,
pageSize: 10,
orderBy: "title",
orderDir: "desc",
},
});Use pageSize: "full" to fetch all items without pagination:
const all = await cms.articles({
graph: { pageSize: "full" },
});Per-path overrides
Override pagination for specific nested arrays using graph.paths:
await cms.homePage({
graph: {
pageSize: "full",
paths: {
"seo.openGraph.images": { pageSize: 50 },
},
},
});Search
Use graph.search for full-text search across all text fields:
const results = await cms.articles({
graph: { search: "launch" },
});Localization
Read content for a specific locale:
const french = await cms.homePage({ locale: "fr" });Response modes
Envelope response
Use response: "envelope" to get pagination metadata:
const envelope = await cms.articles({
response: "envelope",
});
// ^? { items: Article[], total: number }Include IDs
Use includeId: true to include cms0 internal object IDs:
const withIds = await cms.homePage({ includeId: true });Raw response
Use response: "raw" to skip normalization:
const raw = await cms.homePage({ response: "raw" });Mutating roots
Update a root singleton with a partial value:
await cms.homePage.update({
headline: "A better headline",
});Mutating fields
Replace, update, or delete individual fields:
await cms.homePage.headline.set("New headline");
await cms.homePage.headline.update("Updated headline");
await cms.homePage.headline.delete();For nested objects, .update() patches the object:
await cms.homePage.seo.update({
title: "New SEO title",
});Mutating arrays
Append, insert, or update items in root arrays:
await cms.articles.append({
title: "Launch notes",
slug: "launch-notes",
});
await cms.articles.insert(0, {
title: "Pinned post",
slug: "pinned",
});
await cms.articles.at(0).update({
title: "Updated title",
});Mutating models
Create, update, and delete model instances:
const newPost = await cms.models.Post.create({
title: "Launch notes",
slug: "launch-notes",
});
await cms.models.Post.update("post_id", {
title: "Updated title",
});
await cms.models.Post.delete("post_id");Abort and signals
Pass an AbortSignal to cancel in-flight requests:
const controller = new AbortController();
const homePage = await cms.homePage({
signal: controller.signal,
});
controller.abort();Client metadata
Access SDK metadata through cms.meta:
const { locales, defaultLocale, includeIdDefault } = cms.meta;Error handling
Accessor calls throw on network errors, 404s, and permission failures:
try {
const post = await cms.models.Post.byId("missing_id");
if (!post) {
console.log("Post not found");
}
} catch (error) {
console.error("Request failed:", error);
}Success check
Accessor calls should match your TypeScript schema. If a root field or model
does not exist on the client, run cms0 dev or cms0 build again and confirm
the entry file in cms0.config.ts points at the file that calls cms0<T>().