Editor system
The editor system (@nordcom/commerce-cms/editor) is the unified way to build
admin pages for CMS collections. Each editor page is driven by a
manifest — field descriptors plus route shape, route-level access
predicates, list columns, live preview URL, and revalidation paths.
Why
Before this system the admin app shipped two parallel patterns: per-collection
routes with hand-written form/action files, and a separate bridge plugin for the
database-backed entities. Adding a feature (tightening field allowlists, fixing
the autosave loop, improving error handling) meant touching both. The unified
editor system collapses them into one primitive set, and the Convex migration
finished the consolidation — every collection edits through the same
manifest-driven stack against the Convex cms/documents functions.
The mental model
Field descriptors ← single source of truth for fields, drafts,
(src/descriptors, used by versions, locales — the shapes `pnpm cms:gen`
each manifest) derives every artifact from.
CollectionEditorManifest ← route shape, route-level access, list
columns, live-preview URL, revalidation.
Declares the collection by slug.
EditorRuntime ← admin-app dependency bundle: auth helper,
form-state builder, shell-props helper,
shell components. Built once per app.
<EditorEditPage> ← server component; consumes manifest +
<EditorListPage> runtime + route params; renders the
<EditorNewPage> descriptor-driven form / list inside
<EditorVersionsPage> the admin's chrome.
pnpm cms:gen ← codegen step that emits `'use server'`
action wrappers per manifest into
`apps/admin/src/lib/cms-actions/_generated/`
(plus the content types and Convex table
validators).Route file shape
A per-collection page.tsx is ~25 lines:
import 'server-only';
import { businessDataEditor } from '@nordcom/commerce-cms/editor/manifests';
import { EditorEditPage } from '@nordcom/commerce-cms/editor/ui';
import * as actions from '@/lib/cms-actions/_generated/businessData';
import { editorRuntime } from '@/lib/editor-runtime';
export default async function Page({ params, searchParams }: {
params: Promise<{ domain: string }>;
searchParams: Promise<{ locale?: string }>;
}) {
const { domain } = await params;
const sp = await searchParams;
return (
<EditorEditPage
manifest={businessDataEditor}
runtime={editorRuntime}
params={{ domain, id: 'singleton' }}
searchParams={sp}
generatedActions={{
saveDraft: actions.businessDataSaveDraft,
publish: actions.businessDataPublish,
create: actions.businessDataCreate,
delete: actions.businessDataDelete,
bulkDelete: actions.businessDataBulkDelete,
bulkPublish: actions.businessDataBulkPublish,
restoreVersion: actions.businessDataRestoreVersion,
}}
/>
);
}When to use it
- A new CMS collection needs an admin edit / list / new / versions page.
- An existing collection's bespoke route is being refactored away.
When NOT to use it
- A route doesn't follow the "edit one collection" shape — e.g. multi-collection dashboards, custom workflows. Build those by hand and let them coexist with editor routes.
- One-off scripts or admin tools that don't need the editor's form chrome at all.
Continue reading
- Manifest reference — every field on
CollectionEditorManifestexplained, with worked examples. - Collections reference — the collections the manifests declare.