Extensibility
Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.
Notty CMS supports three extension mechanisms: plugins, middleware, and themes.
Status note:
@notty/plugin-apiand@notty/theme-apiare already usable, but they are still early-stage and distributed through the restricted@nottynpm scope. For the least-friction path, start fromcreate-notty --plugin/--themeorpnpm dlx @notty/cli scaffold plugin|theme.
Plugins
Plugins are the primary way to extend Notty. A plugin can add routes, hooks, dashboard widgets, navigation items, settings, and custom policies.
Plugin Structure
my-plugin/
src/index.ts # Plugin definition
package.json # Dependencies + notty manifest
tsconfig.json
Define a Plugin
// src/index.ts
import { definePlugin } from '@notty/plugin-api';
export default definePlugin({
manifest: {
name: 'my-analytics-plugin',
displayName: 'Analytics',
description: 'Track content views and engagement',
version: '1.0.0',
kind: 'plugin',
engines: { notty: '>=0.14.0' },
// Auto-generate a settings form in admin
configSchema: {
trackingId: {
type: 'string',
label: 'Tracking ID',
description: 'Your analytics provider tracking ID',
default: '',
},
enabledContentTypes: {
type: 'string',
label: 'Content types to track',
description: 'Comma-separated list',
default: '*',
},
},
},
register(ctx) {
// Everything below runs once at startup
},
async init(ctx) {
// Async init: database setup, external connections
},
async boot(ctx) {
// Final setup after all plugins initialized
},
async destroy(ctx) {
// Cleanup on shutdown
},
});
Plugin Lifecycle
| Phase | Sync/Async | Purpose |
|---|---|---|
register |
sync | Register routes, hooks, policies, admin extensions |
init |
async | Database migrations, external service connections |
boot |
async | Final setup, safe to use other plugins' services |
destroy |
async | Cleanup on server shutdown |
Register Custom Routes
Routes are mounted at /api/x/<plugin-name>/:
register(ctx) {
// GET /api/x/my-analytics-plugin/stats
ctx.routes.get('/stats', async (req) => {
const contentType = req.query.contentType as string;
return { views: 1234, contentType };
});
// POST /api/x/my-analytics-plugin/track
ctx.routes.post('/track', async (req) => {
const { contentType, entryId } = req.body;
// Record the view...
return { tracked: true };
}, {
policies: ['isAuthenticated'],
description: 'Track a content view',
});
}
Register Content Hooks
React to content lifecycle events:
register(ctx) {
ctx.hooks.on('content:afterCreate', (payload) => {
ctx.log.info(`Created: ${payload.contentType} #${payload.entry}`);
});
ctx.hooks.on('content:afterUpdate', (payload) => {
ctx.log.info(`Updated: ${payload.contentType} #${payload.entry}`);
});
ctx.hooks.on('content:afterDelete', (payload) => {
ctx.log.info(`Deleted: ${payload.contentType} #${payload.entry}`);
});
}
Add Dashboard Widgets
Widgets appear on the admin dashboard:
register(ctx) {
// Stat card
ctx.admin.addDashboardWidget({
id: 'analytics:total-views',
title: 'Total Views',
icon: 'BarChart',
size: 'small',
type: 'stat',
statValue: '12,345',
statLabel: 'Last 30 days',
statColor: '#6366f1',
});
// Link list
ctx.admin.addDashboardWidget({
id: 'analytics:links',
title: 'Quick Links',
icon: 'ExternalLink',
size: 'medium',
type: 'link-list',
links: [
{
label: 'View Dashboard',
href: '/plugins/analytics',
icon: 'BarChart',
description: 'Full analytics dashboard',
},
],
});
}
Add Navigation Items
Add entries to the admin sidebar:
register(ctx) {
ctx.admin.addNavigationItem({
id: 'analytics:main',
label: 'Analytics',
icon: 'BarChart',
path: '/plugins/analytics',
section: 'plugins',
});
}
Add Settings Section
Use configSchema from the manifest to auto-generate settings:
register(ctx) {
ctx.admin.addSettingsSection({
id: 'analytics:settings',
label: 'Analytics',
description: 'Configure analytics tracking',
icon: 'BarChart',
path: 'analytics',
type: 'config-schema',
});
}
Register Custom Policies
Policies are reusable authorization checks:
register(ctx) {
ctx.policies.register(
'analytics:hasAccess',
(policyCtx) => {
return policyCtx.adminUser?.role === 'super-admin';
},
'Only super-admins can access analytics'
);
}
Install a Plugin
Register plugins in notty.config.ts:
import { defineConfig } from '@notty/core/config';
import analyticsPlugin from 'my-analytics-plugin';
export default defineConfig({
plugins: [{ source: analyticsPlugin, config: { trackingId: 'UA-123456' } }],
});
Middleware
Middleware intercepts requests and events at three levels: admin API, content API, and system events.
Admin Middleware
Intercept admin API requests (user management, schema operations, settings):
// src/middlewares/index.ts
import { defineAdminMiddleware } from '@notty/core';
export default defineAdminMiddleware({
name: 'audit-admin-actions',
routes: 'admin:*', // Glob pattern: all admin routes
methods: ['POST', 'PUT', 'DELETE'],
order: 10, // Lower = runs earlier
async handler(ctx, input, next) {
console.log(`[Audit] ${ctx.adminUser?.username} → ${ctx.routeName}`);
const result = await next();
console.log(`[Audit] ${ctx.routeName} completed`);
return result;
},
});
Route patterns:
| Pattern | Matches |
|---|---|
admin:* |
All admin routes |
admin:users:* |
User management routes |
admin:roles:* |
Role management routes |
admin:schemas:* |
Schema management routes |
Content Middleware
Intercept content CRUD operations:
import { defineContentMiddleware } from '@notty/core';
export default defineContentMiddleware({
name: 'auto-slug',
contentTypes: '*', // All content types, or ['article', 'page']
operations: ['create', 'update'],
order: 20,
async handler(ctx, input, next) {
const data = input as Record<string, unknown>;
// Auto-generate slug from title
if (data.title && !data.slug) {
data.slug = String(data.title)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
return next();
},
});
System Middleware
React to system events (app lifecycle, content events):
import { defineSystemMiddleware } from '@notty/core';
export default defineSystemMiddleware({
name: 'log-system-events',
events: ['app:start', 'content:afterCreate'],
order: 10,
async handler(ctx) {
console.log(`[System] ${ctx.event}`, ctx.payload);
},
});
Register Middleware
In a generated app, middleware is registered via src/plugins/app.ts:
// src/plugins/app.ts
import { registerAppMiddlewares } from '../middlewares';
export default {
register(ctx) {
registerAppMiddlewares(ctx);
},
};
// src/middlewares/index.ts
import auditMiddleware from './admin/audit';
import autoSlugMiddleware from './content/auto-slug';
import logEvents from './system/log-events';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addAdmin(auditMiddleware);
ctx.middleware.addContent(autoSlugMiddleware);
ctx.middleware.addSystem(logEvents);
}
Themes
Themes customize the admin panel appearance with design tokens, branding, and layout.
Define a Theme
import { defineTheme } from '@notty/theme-api';
export default defineTheme({
manifest: {
name: 'my-brand-theme',
displayName: 'My Brand',
version: '1.0.0',
kind: 'theme',
engines: { notty: '>=0.14.0' },
tokens: {
colors: {
brand: {
primary: { value: '#10b981', description: 'Green primary' },
secondary: { value: '#34d399' },
accent: { value: '#a78bfa' },
},
surface: {
background: { value: '#ffffff' },
foreground: { value: '#1f2937' },
card: { value: '#f9fafb' },
sidebar: { value: '#f3f4f6' },
},
},
typography: {
heading: { fontFamily: '"Poppins", sans-serif', fontWeight: 600 },
body: { fontFamily: '"Inter", sans-serif', fontSize: '14px' },
},
radii: {
sm: { value: '4px' },
md: { value: '8px' },
lg: { value: '16px' },
},
},
branding: {
title: 'My Company CMS',
footerText: 'Powered by Notty',
hidePoweredBy: false,
},
layout: {
sidebarPosition: 'left',
sidebarCollapsed: false,
headerVisible: true,
},
},
register(ctx) {
ctx.log.info('Brand theme loaded');
},
});
Install a Theme
import { defineConfig } from '@notty/core/config';
import brandTheme from 'my-brand-theme';
export default defineConfig({
plugins: [{ source: brandTheme }],
});
Override Theme Tokens
Override specific tokens without forking the theme:
{
source: brandTheme,
config: {
overrides: {
colors: {
brand: {
primary: { value: '#3b82f6' }, // Switch to blue
},
},
},
},
}
Custom Routes
Add custom API routes to your project without a full plugin:
// src/routes/index.ts
export function registerAppRoutes(ctx) {
// GET /api/x/app/health
ctx.routes.get('/health', () => {
return { status: 'ok', uptime: process.uptime() };
});
// POST /api/x/app/contact
ctx.routes.post('/contact', async (req) => {
const { name, email, message } = req.body;
// Send email, store in database, etc.
return { sent: true };
});
}
Extension Point Reference
| What | How | Where |
|---|---|---|
| HTTP routes | ctx.routes.get/post/put/delete() |
Plugins, app routes |
| Content hooks | ctx.hooks.on('content:*') |
Plugins |
| Dashboard widgets | ctx.admin.addDashboardWidget() |
Plugins |
| Sidebar navigation | ctx.admin.addNavigationItem() |
Plugins |
| Settings section | ctx.admin.addSettingsSection() |
Plugins |
| Custom policies | ctx.policies.register() |
Plugins |
| Admin middleware | ctx.middleware.addAdmin() |
Middleware module |
| Content middleware | ctx.middleware.addContent() |
Middleware module |
| System events | ctx.middleware.addSystem() |
Middleware module |
| Design tokens | manifest.tokens |
Themes |
| Branding | manifest.branding |
Themes |
| Layout | manifest.layout |
Themes |
Further Reading
- Ecosystem & Packaging Conventions — naming, versioning, publishing guidelines
- Upgrade Guide — how to upgrade between Notty versions safely
Источник: docs/guide/extensibility.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.