Skip to content

Admin UI

The admin is runtime-rendered from your schema. No generated page files, add a field and it appears immediately.

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: { ... },
});

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)

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" } } }),
}

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 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.

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/render and 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.

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 -->
))}

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.

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" (taxonomy term picker), "repeater" (JSON array editor), "color" (palette picker, via fields.color()), "link" (structured link, via fields.link()), "menu-items", "taxonomy-terms".

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.

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: [...],
});
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.

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.

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: [...] }).

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.