Admin UI
The admin is runtime-rendered from your schema. No generated page files, add a field and it appears immediately.
View customization
Section titled “View customization”List columns
Section titled “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: { ... },});Field position
Section titled “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
Section titled “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}Combine groups with short labels to turn a long flat form (for example a fixed-slot landing
page with many sections) into labeled panels instead of a wall of prefixed field names.
The object form makes a panel collapsible (rendered as a native <details> element).
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. Fields in the same run may
mix the string and object forms; the first collapsible declaration wins:
fields: { statValue: fields.text({ admin: { group: { label: "Stats", collapsible: true } } }), statLabel: fields.text({ admin: { group: "Stats" } }), archiveNote: fields.text({ admin: { group: { label: "Archive", collapsible: "collapsed" } } }),}Preview
Section titled “Preview”Collections with pathPrefix get a Preview link automatically. For collections without a prefix, add preview: true. For singletons, set preview to the URL:
// Automatic: pathPrefix enables previewdefineCollection({ slug: "posts", pathPrefix: "blog", ... });
// Explicit: no pathPrefix, needs opt-indefineCollection({ slug: "pages", preview: true, ... });
// Singleton: set the URL directlydefineCollection({ 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
Section titled “Live preview”When the preview tab is open, changes in the admin form update the preview in real time — no saving required. This works via BroadcastChannel (same-origin messaging between tabs).
- Text fields (title, excerpt, etc.) update instantly via
textContent - Rich text and blocks are rendered server-side via
/api/cms/preview/renderand injected as HTML
To enable live preview on a field, add the data-cms attribute to the element that renders it:
<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>The attribute value matches the field name from your collection definition. Only fields with data-cms attributes are live-updated — the rest update on save (the preview tab auto-reloads after saving).
The client preview script (src/cms/client/preview.ts) is auto-injected on every page by the integration — no manual step is needed. It activates only when ?preview is in the URL, so there is zero overhead on normal public page views.
Block rendering for preview
Section titled “Block rendering for preview”The live preview endpoint renders your blocks through the virtual module virtual:kide/block-renderer, which resolves to src/components/BlockRenderer.astro. This file is in your app (not in core), so you control the markup and styling. It is a regular Astro component that receives the blocks as a blocks prop — the preview endpoint renders it via Astro’s Container API:
---const { blocks } = Astro.props;---{blocks.map((block) => ( <!-- Render each block -->))}Public page setup
Section titled “Public page setup”Check for the ?preview param and query with status: "any" in preview mode:
---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>See Public pages for the full pattern, including caching.
Custom field components
Section titled “Custom field components”Create a React component in src/cms/fields/ and reference it by name:
// In your collection definitioncolor: fields.text({ admin: { component: "ColorPicker" },});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" (taxonomy term picker), "repeater" (JSON array editor), "color" (palette picker, via fields.color()), "link" (structured link, via fields.link()), "menu-items", "taxonomy-terms".
Custom navigation
Section titled “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: [...],});| Option | Type | Description |
|---|---|---|
label |
string |
Display text in the sidebar |
href |
string |
Link URL |
icon |
string |
Lucide icon name (optional, defaults to grid) |
weight |
number |
Sort order within the Custom group (optional, default 50) |
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 — it cannot move an item into or ahead of a built-in group.
Collections control their own sidebar placement through the collection-level admin config: admin.group sets the sidebar group label (built-ins include Content, Library, and Team), admin.icon sets the Lucide icon, and admin.weight sets the sort order within the group. admin.sidebar: false hides a collection from the sidebar entirely.
The linked pages are regular Astro pages you create in your app. To use the admin layout, import it from @kidecms/core:
---import AdminLayout from "@kidecms/core/admin/layouts/AdminLayout.astro";---
<AdminLayout title="Analytics | Admin"> <h1>Analytics</h1> <!-- your content --></AdminLayout>Available icons: BarChart, Bell, Bookmark, Calendar, Clock, Database, FileText, FolderTree, Globe, Home, Image, Inbox, Key, Layers, LayoutGrid, Link, Link2, Lock, Mail, Menu, MessageSquare, Package, Palette, PencilRuler, Search, Settings, Shield, Star, Tag, Terminal, Users, Zap.
Admin config
Section titled “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
Section titled “Uploads”| Option | Type | Default | Description |
|---|---|---|---|
allowedTypes |
string[] |
Images, PDF, MP4, WebM | Allowed MIME types |
maxFileSize |
number |
52428800 (50 MB) |
Max file size in bytes |
Default allowed types: image/jpeg, image/png, image/gif, image/webp, image/avif, application/pdf, video/mp4, video/webm.
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
Section titled “Rate limiting”| Option | Type | Default | Description |
|---|---|---|---|
maxAttempts |
number |
5 |
Login attempts before blocking |
windowMs |
number |
900000 |
Time window in ms (default 15 min) |
List views page at a fixed 10 items; the page size is not configurable.
Colors
Section titled “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
Section titled “Date & time”Control how dates and times display throughout the admin.
| 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 (numeric date + 2-digit HH:mm) |
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, { second: "2-digit" } to show seconds) or dateTimePattern for an exact layout.
Pattern tokens: yyyy yy · MM M · dd d · HH H (24-hour) · hh h (12-hour) · mm m · ss s · a (AM/PM). Wrap literal text in single quotes:
| Pattern | Output |
|---|---|
d.M.yyyy HH:mm |
1.7.2026 14:30 |
dd.MM.yyyy 'klo' HH:mm |
01.07.2026 klo 14:30 |
h:mm a |
2:30 PM |
timeZone still applies to pattern output.
All settings are optional — defaults apply when omitted.