Skip to content

Collections

Collections are defined in src/cms/collections/ and registered in src/cms/cms.config.ts. Each collection becomes a database table, a set of TypeScript types, Zod validators, and an admin UI.

src/cms/collections/posts.ts
import { defineCollection, fields, hasRole } from "@kidecms/core";
export default defineCollection({
slug: "posts",
labels: { singular: "Post", plural: "Posts" },
timestamps: true,
drafts: true,
versions: { max: 20 },
access: {
publish: hasRole("admin"),
},
fields: {
title: fields.text({ required: true }),
slug: fields.slug({
from: "title",
admin: { position: "sidebar" },
}),
body: fields.richText(),
author: fields.relation({
collection: "authors",
admin: { position: "sidebar" },
}),
},
});

Register it in src/cms/cms.config.ts:

import { defineConfig } from "@kidecms/core";
import posts from "./collections/posts";
export default defineConfig({
collections: [posts],
});

collections is the only required key. The other top-level keys (locales, admin, images, integrations, collaboration) are listed in the Configuration reference.

Option Type Default Description
slug string URL-safe identifier, used as table name prefix
labels { singular, plural } Display names in admin
singleton boolean false Single document (e.g., front page)
timestamps boolean true Auto _createdAt / _updatedAt, added unless timestamps: false is set
drafts boolean false Enable draft/published status
versions { max: number } Keep version snapshots
pathPrefix string URL prefix for public pages (e.g., "blog")
preview boolean | string false Enable preview link (string for static URL)
labelField string title Field used as document display name
access object Role-based access rules (see Access Control)
hooks object Lifecycle hooks (see Hooks)
views object List column config (see Admin UI)
admin object Sidebar grouping, icon, order, and visibility
auth boolean false Mark an authentication-sensitive collection
searchable boolean | { fields: string[] } Controls full-text search inclusion

Seed content is not a collection option. Define it in src/cms/seed.ts (keyed by collection slug) and load it with pnpm cms:seed.

The built-in login flow uses the users collection marked with auth: true. Auth collections receive stricter default access rules, automatic password hashing, and credential-field filtering. See Authentication before changing this boundary.

Use collection-level admin options to keep large projects organized:

defineCollection({
slug: "case-studies",
labels: { singular: "Case Study", plural: "Case Studies" },
admin: {
group: "Marketing",
icon: "Star",
weight: 20,
},
fields: { ... },
});
Option Type Description
group string Sidebar group label. Built-ins include Content, Library, Team
icon string Lucide icon name used in the sidebar
weight number Sort order inside the group. Lower appears first
sidebar boolean Set false to hide from the sidebar

Custom groups are collapsible and remember their open/closed state in the browser. Hidden collections still work in relations, search, and the local API.

By default, the admin uses the title field as the document display name (in relation selects, DataTable, breadcrumbs). If your collection doesn’t have a title field, or you want a different field, set labelField:

defineCollection({
slug: "authors",
labels: { singular: "Author", plural: "Authors" },
labelField: "name",
fields: {
name: fields.text({ required: true }),
title: fields.text(), // work title, not the display name
},
});

Fallback chain: labelField → field named title → field named name → first text field → first field of any type.

defineCollection({
slug: "front-page",
labels: { singular: "Front Page", plural: "Front Page" },
singleton: true,
fields: { ... },
})

Singletons show under “Singles” in the sidebar. One document per collection.

withSite() wraps a collection definition and prepends a required site relation, for projects that serve several sites from one CMS:

import { defineCollection, fields, withSite } from "@kidecms/core";
export default withSite(
defineCollection({
slug: "posts",
labels: { singular: "Post", plural: "Posts" },
fields: { title: fields.text({ required: true }) },
}),
);
Option Type Default Description
field string "site" Name of the injected relation field
collection string "sites" Slug of the site collection it points at
required boolean true Whether every document must choose a site
admin AdminFieldComponent sidebar Admin options for the injected field

It throws if the collection already has a field with that name. The sites collection itself is an ordinary collection you define.

Enable locales in cms.config.ts and mark fields as translatable:

src/cms/cms.config.ts
export default defineConfig({
locales: {
default: "en",
supported: ["en", "fi"],
},
collections: [posts],
});
src/cms/collections/posts.ts
fields: {
title: fields.text({ translatable: true }),
body: fields.richText({ translatable: true }),
category: fields.select({ options: ["tech", "design"] }), // not translated
}

Translatable fields get a separate _translations table. Non-translatable fields stay on the main table. The admin shows a language switcher on edit pages.

Every document in a collection with translatable fields carries _sourceLocale: the language its base row is written in. It defaults to locales.default, and editors can change it in the sidebar (“Content language”). A document exists in its source locale plus every locale it has a translation for — _availableLocales on every read result lists them, source locale first.

Content that only exists in Finnish is stored with _sourceLocale: "fi" and no English translation:

await cms.posts.create({ title: "Vain suomeksi", slug: "vain-suomeksi", _sourceLocale: "fi" });

In the admin, the FI tab edits the document itself and the EN tab offers the translation form (“Translate from FI”); tabs for locales the document does not exist in are drawn dashed. Adding a translation for the source locale is refused, and switching the content language to a locale that already has a translation is refused until that translation is deleted.

When listing for a locale, pass availability: "exact" to get only documents that exist in it — see Local API.