Content Engine

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:

plugins/blog/src/admin/content.tsx
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 ContentFrontendPluginSource

The 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:

plugins/blog/src/content/post.ts
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:

plugins/blog/src/content/post.ts
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:

plugins/blog/locales/en.json
{
  "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:

plugins/blog/src/views/admin/article/form-layout.tsx
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:

plugins/blog/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:

plugins/blog/src/admin/content.tsx
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:

plugins/blog/src/admin/content.tsx
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:

plugins/blog/src/views/admin/article/editor-field.tsx
import { ContentFormFieldSkeleton } from "@vitnode/core/content/admin-form"

;<React.Suspense fallback={<ContentFormFieldSkeleton control="editor" />}>
  <AutoFormEditor label={t("content.label")} {...props} />
</React.Suspense>

Learn More