20 Plugins for the CMS interface
The CMS interface is the editors' application, served at /relatude.cms/
(chapter 17). A plugin adds a menu item, a screen, a dashboard card, search
results or a property editor to it. The interface's own Dashboard, Content and Settings are plugins
built on the same API.
A plugin is an ES module, built on its own and loaded by the interface at run time. One build works with every release of the interface whose SDK it fits.
- How a plugin is loaded — The interface's page has an import map for the six modules it shares with plugins: react, react/jsx-runtime, react-dom, react-dom/client, @relatude/sdk and @relatude/ui. A plugin leaves these as bare imports, so it runs…
- Setting up to develop plugins — You need: Node.js 20 or newer, which includes npm. An editor that knows TypeScript, such as Visual Studio Code, WebStorm or Rider. A browser with developer tools: Chrome, Edge or Firefox. A site to develop against: an…
- A first plugin — Create a plugin with the starter:
- TypeScript — The starter's tsconfig.json sets:
- The development loop — Start the dev server and leave it running:
- Debugging — The browser's developer tools (F12). The source map puts src/index.tsx under Sources (Firefox: Debugger) → localhost:5180. Set breakpoints there, or write debugger; in the code. &dev opens the interface's development…
- Tests — Keep logic that does not need the interface in plain modules, and test it with Vitest. Vitest reads the project's Vite config and needs no setup of its own:
- When something is wrong
- Building and deploying — This writes dist/index.js and its source map; relatudePlugin({ sourcemap: false }) leaves the map out. Raise version in definePlugin with each release, and deploy the plugin as Shipping a plugin describes: under the…
- definePlugin — A plugin's default export must be what definePlugin returns.
- The CMS services — useCms() gives the CMS services to any component the interface renders. activate, search providers and search hits get the same object as cms.
- Extension points — contributes takes five kinds of contribution, each with a short example below. A plugin on the store puts a blade, a widget and a search together in one plugin.
- The built-in editors — What the interface's own editors do, in short, and what the model controls in them: Dates. A datetime is stored in UTC and shown in the editor's own time zone. Clearing it stores the unset DateTime (0001-01-01), which…
- The content as the screens see it — cms.data is the DataClient the content screens run on: the site's model as collections of nodes with plain values. A plugin that lists or edits content through it gets the same lists, display names and saving as those…
- Form layouts — A FormLayout says how the node editor lays out a type's properties. It is plain data that the type carries as layout: tabs of items, each item taking a share of the row.
- Skins — A skin is the interface's look: a value for each design token, the CSS variables (--r-*) that every component is drawn with, for the light theme and for the dark. Shared tokens (corners, fonts, text size, motion) have…
- A plugin on the store — The stock plugin from A first plugin, grown into a real one: a blade listing the products with a switch for InStock, a dashboard widget counting those out of stock, and a search for SKUs. All three stay current as…
- Opening the interface's own blades — Open one beside your blade with props.openChild(…), or a whole stack with cms.blades.openPath(…), as the search above does.
- The component library — @relatude/ui is the visual API for plugins. Its components follow the interface's theme and skin, and they come from the interface through the import map, so they add nothing to a plugin's size.
- Shipping a plugin — dist/index.js is the whole plugin. Serve it from the site, under wwwroot (such as wwwroot/cms-plugins/stock/index.js, served by app.UseStaticFiles()), or from another server that answers with Access-Control-Allow-Origin…
- Rules for plugins — Never bundle the shared modules. relatudePlugin() leaves them out. A second React breaks every hook, and a second SDK has its own useCms() context, which no shell provides. Begin every id with the plugin's name. Blade…