Overview

Collections

Basic collection#

// 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({
  database: { dialect: "sqlite" },
  collections: [posts],
});

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
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
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 a users collection marked with auth: true. Auth collections get stricter default access rules, automatic password hashing, and credential-field filtering — see Access Control → Auth collections.

Admin sidebar#

Collection-level admin options 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#

By default, the admin uses a collection's title field as the document display name (relation selects, list view, breadcrumbs). If your collection has no title field, or you want a different one, 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#

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.

Internationalization#

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.

Next steps#

Updated