Admin UI
List columns#
Configure which columns appear in the list view via views in the collection definition:
defineCollection({
slug: "posts",
views: {
list: {
columns: ["title", "category", "_status", "_updatedAt"],
defaultSort: { field: "_updatedAt", direction: "desc" },
},
},
fields: { /* ... */ },
});List views page at a fixed 10 items — the page size is not configurable.
Field position#
Fields go to the content area by default. Set admin.position: "sidebar" to place a field in the sidebar:
fields: {
title: fields.text({ required: true }), // → content
body: fields.richText(), // → content
slug: fields.slug({ admin: { position: "sidebar" } }), // → sidebar
category: fields.text({ admin: { position: "sidebar" } }), // → sidebar
}| Position | Description |
|---|---|
"content" |
Main area (left column on desktop), default |
"sidebar" |
Side panel (right column on desktop) |
Field groups#
Set admin.group to render fields inside a titled panel in the edit form. Consecutive fields sharing the same group become one panel; fields without a group render loose, as before. Grouping is purely presentational — field order and storage are unchanged.
fields: {
heroHeading: fields.text({ label: "Heading", admin: { group: "Hero" } }),
statValue: fields.text({ label: "Value", admin: { group: "Stats" } }),
statLabel: fields.text({ label: "Label", admin: { group: "Stats" } }),
notes: fields.text(), // ungrouped
}The object form makes a panel collapsible: collapsible: true starts open, "collapsed" starts closed. Once an editor toggles a panel, the browser remembers that state per collection and group (localStorage), overriding the schema default on later visits.
Preview#
Collections with pathPrefix get a Preview link automatically. Collections without a prefix need preview: true. Singletons set preview to the URL directly:
// Automatic: pathPrefix enables preview
defineCollection({ slug: "posts", pathPrefix: "blog", /* ... */ });
// Explicit: no pathPrefix, needs opt-in
defineCollection({ slug: "pages", preview: true, /* ... */ });
// Singleton: set the URL directly
defineCollection({ slug: "front-page", singleton: true, preview: "/", /* ... */ });The Preview link opens the public page in a new tab with ?preview=true, which shows draft content.
Live preview#
When the preview tab is open, changes in the admin form update the preview in real time, no saving required — via BroadcastChannel same-origin messaging between tabs. Text fields (title, excerpt, etc.) update instantly via textContent; rich text and blocks render server-side via /api/cms/preview/render and get injected as HTML.
Enable live preview on a field by adding a data-cms attribute to the element that renders it, matching the field name from your collection definition:
<h1 data-cms="title">{doc.title}</h1>
<p data-cms="excerpt">{doc.excerpt}</p>
<div data-cms="body"><RichTextContent content={doc.body} /></div>
<div data-cms="blocks"><BlockRenderer blocks={blocks} /></div>Only fields with data-cms attributes are live-updated; everything else updates on save, and the preview tab auto-reloads after saving. The client preview script is auto-injected on every page by the integration and only activates when ?preview is in the URL, so there is zero overhead on normal page views.
Public page setup#
Check for the ?preview param and query with status: "any" in preview mode — see Local API for the full querying pattern:
---
import { cms } from "@/cms/.generated/api";
const isPreview = Astro.url.searchParams.has("preview");
const doc = await cms.posts.findOne({ slug: Astro.params.slug!, status: isPreview ? "any" : "published" });
if (!doc) return Astro.redirect("/404");
---
<h1 data-cms="title">{doc.title}</h1>Custom field components#
Create a React component in src/cms/fields/ and reference it by name:
// In your collection definition
color: fields.text({
admin: { component: "ColorPicker" },
});// src/cms/fields/ColorPicker.tsx
import type { CustomFieldProps } from "@kidecms/core";
export default function ColorPicker({ name, value, readOnly }: CustomFieldProps) {
return (
<input type="color" name={name} defaultValue={value || "#000000"} disabled={readOnly} />
);
}The component receives name (form field name), field (field config), value (serialized value), and readOnly. It renders with client:load and must include an input with the name prop so the form can read its value.
Built-in component variants: "radio" (select as radio buttons), "taxonomy-select", "repeater" (JSON array editor), "color" (palette picker), "link" (structured link), "menu-items", "taxonomy-terms".
Custom navigation#
Add custom pages to the admin sidebar via admin.nav in your CMS config:
export default defineConfig({
admin: {
nav: [
{ label: "Dashboard", href: "/dashboard", icon: "Home", weight: 10 },
{ label: "Analytics", href: "/analytics", icon: "BarChart", weight: 20 },
{ label: "Settings", href: "/settings", icon: "Settings" },
],
},
collections: [ /* ... */ ],
});The sidebar sorts by group first, then by weight within each group. Groups render in a fixed order:
| Order | Group | Contents |
|---|---|---|
| 0 | Content | Content collections |
| 10 | Library | Assets and related library items |
| 20 | Team | Users and team management |
| 100 | Custom | Your admin.nav items |
Custom nav items always land in the Custom group, which renders after every built-in group regardless of weight — weight only orders your items relative to each other within that group.
Collections control their own sidebar placement through collection-level admin config — see Collections → Admin sidebar.
Admin config#
Configure admin behavior in your CMS config:
export default defineConfig({
admin: {
uploads: {
allowedTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf", "application/zip"],
maxFileSize: 100 * 1024 * 1024, // 100 MB
},
rateLimit: {
maxAttempts: 10,
windowMs: 5 * 60 * 1000, // 5 minutes
},
},
collections: [ /* ... */ ],
});Uploads#
| Option | Type | Default | Description |
|---|---|---|---|
allowedTypes |
string[] |
Images, PDF, MP4, WebM | Allowed MIME types |
maxFileSize |
number |
52428800 (50 MB) |
Max file size in bytes |
SVG is deliberately not allowed by default — it executes script when served inline from the admin's origin. Re-enable it via allowedTypes only behind a CSP or Content-Disposition.
Rate limiting#
| Option | Type | Default | Description |
|---|---|---|---|
maxAttempts |
number |
5 |
Login attempts before blocking |
windowMs |
number |
900000 |
Time window in ms (default 15 min) |
Colors#
Define the palette offered by every fields.color(...) picker — editors choose from these named colors, there is no free-form hex entry:
admin: {
colors: [
{ label: "Blue", value: "#4000FF" },
{ label: "Pink", value: "#FFDBEB" },
{ label: "Black", value: "#000000" },
],
}A color field can override this list per-field via fields.color({ colors: [...] }).
Date & time#
| Option | Type | Default | Description |
|---|---|---|---|
dateFormat |
string |
"en-US" |
BCP-47 locale for date/time display (e.g. "en-GB", "fi-FI") |
timeZone |
string |
browser | IANA time zone (e.g. "Europe/Helsinki"). Overrides each viewer's zone |
dateTimeFormat |
Intl.DateTimeFormatOptions |
— | Overrides merged over the defaults |
dateTimePattern |
string |
— | Explicit token pattern; wins over dateFormat/dateTimeFormat |
admin: {
dateFormat: "fi-FI",
timeZone: "Europe/Helsinki",
dateTimePattern: "d.M.yyyy HH:mm", // → 1.7.2026 14:30
}dateFormat is a locale, not a pattern — it controls ordering and 12/24-hour conventions. For finer control, use dateTimeFormat (e.g. { hour12: false } for 24-hour time) or dateTimePattern for an exact layout. All settings are optional; defaults apply when omitted.