AdminCP Integration
Configure zero-code AdminCP management screens, custom form layouts, list columns, and screen overrides.
Content Engine automatically builds interactive management screens in the AdminCP without writing hand-rolled pages or form components.
Quick start
Register your content type in src/admin/content.tsx with contentTypeAdmin:
import type { ContentFrontendPluginSource } from '@vitnode/core/lib/plugin'
import { contentTypeAdmin } from '@vitnode/core/lib/plugin'
import { FileTextIcon } from 'lucide-react'
import { postContentType } from '@/content/post'
export const adminContent = {
pluginId: 'blog',
contentTypes: [
contentTypeAdmin({
definition: postContentType,
icon: <FileTextIcon />,
}),
],
} satisfies ContentFrontendPluginSourceThe screen is immediately accessible at /admin/content/blog/post. The build
imports this module for you through the generated content registry; keep it out
of config.tsx, which is bundled with every public page. See
Plugin frontend modules for the
matching admin/nav.tsx sidebar entry.
Customizing the Data Table
Control which columns, search inputs, and sortable headers appear in the AdminCP table:
export const postContentType = defineContentType({
id: "blog.post",
tableName: "blog_posts",
fields: {/* ... */},
admin: {
titleField: "title", // Primary record identifier in dialogs and headings
list: {
columns: ["title", "status", "author", "createdAt"],
searchableFields: ["title"],
orderableFields: ["title"],
defaultOrderBy: "createdAt",
defaultOrder: "desc",
},
},
})System columns (createdAt, updatedAt, status, publishedAt) are always orderable and do not need to be listed in orderableFields.
Customizing Form Sections
Divide form fields into titled sections:
admin: {
form: {
sections: [
{
name: "main",
fields: ["title", "slug", "content"],
},
{
name: "meta",
fields: ["category", "author"],
},
],
},
}Section headings and descriptions are localized in your plugin's locale JSON at {pluginId}.content.{entityKey}.form.{sectionName}.title and .desc:
{
"content": {
"post": {
"form": {
"main": {
"title": "Article Content",
"desc": "Title, slug, and body text of the article."
},
"meta": {
"title": "Publishing Options",
"desc": "Category and author assignments."
}
}
}
}
}Custom Form Layouts
For complete control over form presentation (e.g. main content area with a publishing sidebar), supply a custom layout component using @vitnode/core/content/admin-form primitives:
import type { ContentFormLayoutProps } from "@vitnode/core/lib/plugin"
import {
ContentFormActions,
ContentFormField,
ContentFormHeader,
ContentFormLayoutGrid,
ContentFormMain,
ContentFormSection,
ContentFormSidebar,
ContentFormStatus,
} from "@vitnode/core/content/admin-form"
export const BlogArticleFormLayout = ({ mode }: ContentFormLayoutProps) => {
return (
<>
<ContentFormHeader>
<ContentFormActions />
</ContentFormHeader>
<ContentFormLayoutGrid>
<ContentFormMain>
<ContentFormSection>
<ContentFormField name="title" />
<ContentFormField name="slug" />
<ContentFormField name="content" />
</ContentFormSection>
</ContentFormMain>
<ContentFormSidebar>
{mode === "edit" ? <ContentFormStatus /> : null}
<ContentFormField name="category" />
<ContentFormField name="author" />
</ContentFormSidebar>
</ContentFormLayoutGrid>
</>
)
}Register the layout in src/admin/content.tsx:
contentTypeAdmin({
...postNav,
forms: {
layout: BlogArticleFormLayout,
},
})Custom Table Cells and Field Components
You can also customize individual table columns or form input controls:
contentTypeAdmin({
...categoryNav,
// Custom cell rendering in the DataTable
columns: {
color: {
cell: ({ row }) => (
<span className="flex items-center gap-2">
<span className="h-3 w-3 rounded-full" style={{ backgroundColor: row.color }} />
{row.color}
</span>
),
},
},
// Custom input component in AutoForm
fields: {
color: { component: CategoryColorFieldPicker },
},
})The Loading Placeholder
The create and edit screens never flash a spinner. While the form's code chunk and the record are still on their way, Content Engine draws a skeleton in the shape of the form you are about to get.
When an override is a different shape
A field override can render something much bigger than its field kind suggests — the blog's content is declared a textarea but renders a full rich-text editor. Tell the placeholder what to draw with skeleton:
contentTypeAdmin({
...postNav,
fields: {
content: {
component: BlogArticleEditorField,
skeleton: "editor",
},
},
})Prop
Type
The same block is available on its own, for a field that lazy-loads its control:
import { ContentFormFieldSkeleton } from "@vitnode/core/content/admin-form"
;<React.Suspense fallback={<ContentFormFieldSkeleton control="editor" />}>
<AutoFormEditor label={t("content.label")} {...props} />
</React.Suspense>