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
| Module | Location | Purpose |
|---|---|---|
admin/nav | src/admin/nav.tsx | Lightweight sidebar navigation entries and Lucide icons |
admin/content | src/admin/content.tsx | Content Engine editing screens, custom fields, and form layouts |
config | src/config.tsx | The 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:
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 AdminNavPluginSource2. admin/content.tsx (Editing Screens & Overrides)
Loaded only when an administrator navigates into Content Engine screens:
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 ContentFrontendPluginSource3. 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.
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:
{
"exports": {
"./config": "./dist/src/config.js",
"./admin/nav": "./dist/src/admin/nav.js",
"./admin/content": "./dist/src/admin/content.js"
}
}