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.
Basic collection
Section titled “Basic collection”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.
Collection options
Section titled “Collection options”| 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.
Admin sidebar
Section titled “Admin sidebar”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.
Label field
Section titled “Label field”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.
Singletons
Section titled “Singletons”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.
Multi-site helper
Section titled “Multi-site helper”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.
Internationalization
Section titled “Internationalization”Enable locales in cms.config.ts and mark fields as translatable:
export default defineConfig({ locales: { default: "en", supported: ["en", "fi"], }, collections: [posts],});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.
Content language
Section titled “Content language”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.