Logo VitNode

Plugin files

Reference for the files and exports a VitNode plugin needs for Content Engine content types, which part of the app or API reads each one, and why AdminCP code stays out of config.tsx.

VitNode reads a plugin's content types from several small files instead of one big config. A different part of the app loads each file, so a public page never downloads the AdminCP's forms and a browser never sees your database code. First content type writes each file step by step; this page is the lookup.

Files at a glance

FileExportRead byHolds
src/content/<type>.tsAny nameEvery file belowThe defineContentType definition. Safe in the browser
src/database/<type>.tsEvery tableDrizzle Kit, from dist/src/database/*.jscreateContentModel and the generated tables. Server only
src/api/modules/admin/admin.module.tsAny namesrc/config.api.tsbuildContentAdminModule inside a module named admin
src/config.api.tsThe API plugin, VitNodeApiPluginThe API, and the app's typed fetcherThe admin module, and buildContentPublicModule for public routes
src/admin/nav.tsxadminNavThe AdminCP layout, on every AdminCP page{ definition, icon } per content type
src/admin/content.tsxadminContentThe content screens, loaded on demandScreen registrations, custom cells, fields and form layouts
src/content.tscontentTypesThe web build and the API at startupEvery content type with delivery or search
src/routes.tsroutesThe web buildThe pages that serve delivery and search URLs
src/config.tsxThe pluginThe app's vitnode.config.tsPlugin id, messages, localeFiles and routes only
src/locales/en.jsonJSONThe app, through localeFilesLabels for content types, fields and permissions

A content type with an AdminCP screen needs an entry in both admin/nav and admin/content. The first adds the sidebar link, the second the screen behind it. They are separate files so the sidebar stays small on every AdminCP page, while form and editor code loads only with the screen.

Without custom cells, fields or layouts, admin/content can re-export the nav list:

src/admin/content.tsx
export { adminNav as adminContent } from "./nav";

Once you customize a screen, build adminContent on its own and reuse the nav entries, as the example plugin does:

plugins/example/src/admin/content.tsx
import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin";

import { CONFIG_PLUGIN } from "@/const";

import { exampleArticleNav, exampleCategoryNav, examplePageNav } from "./nav";

export const adminContent = {
  pluginId: CONFIG_PLUGIN.pluginId,
  contentTypes: [exampleArticleNav, exampleCategoryNav, examplePageNav],
} satisfies ContentFrontendPluginSource;

How the app finds them

The app never imports these files by hand. Its Vite plugin resolves a fixed subpath of every plugin listed in vitnode.config.ts and writes the result into a generated file:

SubpathGenerated fileUsed for
<pluginId>/admin/navsrc/admin-nav.gen.tsThe AdminCP sidebar
<pluginId>/admin/contentsrc/content-registry.gen.tsThe AdminCP content screens
<pluginId>/contentsrc/content-modules.gen.tsURL checks in the build and in the API
<pluginId>/config.apisrc/api-registry.gen.tsTypes for fetcher, imported with import type only
<pluginId>/routessrc/plugin-routes.gen.tsPlugin pages

This happens each time vite dev or vite build starts, and the dev server regenerates the files when a resolved module changes. A subpath that does not resolve is skipped without an error, because most plugins have no content types. That is also the usual reason a new screen does not show up: a wrong file path, a wrong export name, or a plugin that was not rebuilt.

Resolution only works when the plugin id equals the npm package name and the package exports its dist folder. A generated plugin already has the right exports:

plugins/example/package.json
{
  "name": "@vitnode/example",
  "exports": {
    "./locales/*.json": "./src/locales/*.json",
    "./*": {
      "import": "./dist/src/*.js",
      "types": "./dist/src/*.d.ts",
      "default": "./dist/src/*.js"
    }
  }
}

Tables are the exception. Drizzle Kit reads node_modules/<pluginId>/dist/src/database/*.js directly for every plugin in the API config. Export each table from those files, or it never reaches a migration. Database shows a model with several tables.

Keep config.tsx small

config.tsx is bundled with the document shell of every public page:

plugins/example/src/config.tsx
import { buildPlugin } from "@vitnode/core/lib/plugin";

import { CONFIG_PLUGIN } from "@/const";

import messages from "./locales";
import { routes } from "./routes";

export const examplePlugin = () =>
  buildPlugin({
    ...CONFIG_PLUGIN,
    localeFiles: {
      en: "@vitnode/example/locales/en.json",
    },
    messages,
    routes,
  });

Do not import admin/nav or admin/content here. The app loads admin/nav with the AdminCP layout and admin/content behind a dynamic import(), so form code arrives only with the screen that needs it. Pulling them into config.tsx would put the whole editing stack, rich text editor included, into every public page.

localeFiles maps each language to your locale JSON so the app can load it. Without it nothing fails, but every label from your locale file is missing.

content.ts and public URLs

A content type with delivery or search publishes URLs. List every such content type in src/content.ts. The example plugin lists all of its types, which is allowed:

plugins/example/src/content.ts
import { advancedArticleContentType } from "@/content/advanced-article";
import { articleContentType } from "@/content/article";
import { categoryContentType } from "@/content/category";
import { localizedArticleContentType } from "@/content/localized-article";
import { pageContentType } from "@/content/page";

export const contentTypes = [
  articleContentType,
  advancedArticleContentType,
  localizedArticleContentType,
  categoryContentType,
  pageContentType,
];

Keep the module browser-safe: it imports definitions only, never src/database. The web build checks that one of your pages serves each URL, and the API refuses to start when a plugin publishes URLs that this module does not declare. See Content URLs need a page.

Errors

Error or symptomCauseFix
A content type has no sidebar entry or screenadmin/nav or admin/content did not resolve, or the export name is wrongCheck the file path, the adminNav / adminContent export and rebuild the plugin
A table is missing from the migrationThe table is not exported from src/database/*.ts, or the plugin was not rebuiltExport it and run build:plugins before db:migrate
content-module-missing when the API startsA content type has delivery or search, but <pluginId>/content does not resolveAdd src/content.ts with export const contentTypes
incomplete-content-types when the API startssrc/content.ts leaves out a content type that publishes URLs, or declares it differentlyExport the same definition the API registers in contentTypes
content-url-without-page in dev or buildNo page route serves a delivery or search URLAdd the page to src/routes.ts, or change the URL
Labels show raw keys or field nameslocaleFiles is missing from config.tsx, or the key path is wrongAdd localeFiles and check the content.<entity> keys