Content Engine

Plugin Frontend Modules

How plugins export AdminCP navigation, Content Engine screens, and runtime configs with optimized code splitting.

To keep the AdminCP sidebar light while lazy-loading heavy rich text editors on demand, plugins separate frontend code into three clean modules.

The Three Frontend Modules

ModuleLocationPurpose
admin/navsrc/admin/nav.tsxLightweight sidebar navigation entries and Lucide icons
admin/contentsrc/admin/content.tsxContent Engine editing screens, custom fields, and form layouts
configsrc/config.tsxThe plugin's identity, locale barrel and route declarations - plain data the browser may hold

1. admin/nav.tsx (Sidebar Navigation)

Kept deliberately minimal so the AdminCP shell loads navigation without pulling in heavy editing components:

plugins/blog/src/admin/nav.tsx
import type { AdminNavPluginSource } from '@vitnode/core/lib/plugin'
import { FileTextIcon } from 'lucide-react'
import { postContentType } from '@/content/post'

export const postNav = {
  definition: postContentType,
  icon: <FileTextIcon />,
}

export const adminNav = {
  pluginId: 'blog',
  contentTypes: [postNav],
} satisfies AdminNavPluginSource

2. admin/content.tsx (Editing Screens & Overrides)

Loaded only when an administrator navigates into Content Engine screens:

plugins/blog/src/admin/content.tsx
import type { ContentFrontendPluginSource } from '@vitnode/core/lib/plugin'
import { contentTypeAdmin } from '@vitnode/core/lib/plugin'
import { postNav } from './nav'

export const adminContent = {
  pluginId: 'blog',
  contentTypes: [
    contentTypeAdmin({
      ...postNav,
      // Optional column, field, or form layout overrides
    }),
  ],
} satisfies ContentFrontendPluginSource

3. config.tsx (Root Plugin Config)

The factory a host registers in vitnode.config.ts. It is bundled for the browser with the document shell, so it carries only what a browser may hold: the plugin id, its locale barrel and its route declarations.

plugins/blog/src/config.tsx
import { buildPlugin } from '@vitnode/core/lib/plugin'
import messages from './locales'
import { routes } from './routes'

export const blogPlugin = () =>
  buildPlugin({
    pluginId: 'blog',
    messages,
    routes,
  })

Do not spread admin/nav or admin/content into it. The build generates one literal import of each per configured plugin - admin/nav with the AdminCP shell, admin/content behind a dynamic import() - so an editing screen arrives with the route that renders it. Spreading them into the factory puts the whole editing stack, rich text editor included, into every public page's bundle. See Configuration.


Package Exports Configuration

Declare the subpaths in your plugin's package.json:

plugins/blog/package.json
{
  "exports": {
    "./config": "./dist/src/config.js",
    "./admin/nav": "./dist/src/admin/nav.js",
    "./admin/content": "./dist/src/admin/content.js"
  }
}

Learn More