Cookbook: Custom Plugin
Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.
Все редакцииОбновлено 2026-09-30
Build a Notty CMS plugin that adds a page view counter with a custom route, content hooks, and a dashboard widget.
Recommended starting point: if you are inside a Notty workspace, use
notty scaffold plugin view-counterorcreate-notty my-plugin --plugin. Directnpm install @notty/plugin-apirequires access to the restricted@nottynpm scope.
What You'll Build
- A
view-counterplugin that tracks content views - Custom API route:
GET /api/x/view-counter/track/:contentType/:id - Content hook: logs every create/update event
- Dashboard widget: shows total tracked views
- Settings section: enable/disable tracking per content type
Step 1: Project Setup
mkdir notty-plugin-views
cd notty-plugin-views
npm init -y
Install the plugin API:
npm install @notty/plugin-api
npm install -D typescript
Update package.json:
{
"name": "notty-plugin-views",
"version": "1.0.0",
"main": "src/index.ts",
"notty": {
"kind": "plugin",
"displayName": "View Counter"
},
"peerDependencies": {
"@notty/plugin-api": ">=0.14.0"
}
}
Add tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"outDir": "dist"
},
"include": ["src"]
}
Step 2: Define the Plugin
// src/index.ts
import { definePlugin } from '@notty/plugin-api';
// In-memory store (replace with database table in production)
const viewCounts = new Map<string, number>();
function viewKey(contentType: string, id: string): string {
return `${contentType}:${id}`;
}
export default definePlugin({
manifest: {
name: 'notty-plugin-views',
displayName: 'View Counter',
description: 'Track and display content view counts',
version: '1.0.0',
kind: 'plugin',
engines: { notty: '>=0.14.0' },
configSchema: {
enabledTypes: {
type: 'string',
label: 'Tracked content types',
description: 'Comma-separated list of content types to track (or * for all)',
default: '*',
},
},
},
register(ctx) {
// ── Track a view ────────────────────────────────────────────────
// GET /api/x/notty-plugin-views/track/:contentType/:id
ctx.routes.get('/track/:contentType/:id', (req) => {
const { contentType, id } = req.params;
// Check if tracking is enabled for this content type
const enabled = ctx.config.enabledTypes as string;
if (enabled !== '*' && !enabled.split(',').includes(contentType)) {
return { tracked: false, reason: 'Content type not tracked' };
}
const key = viewKey(contentType, id);
const count = (viewCounts.get(key) || 0) + 1;
viewCounts.set(key, count);
return { tracked: true, contentType, id, views: count };
});
// ── Get view count ──────────────────────────────────────────────
// GET /api/x/notty-plugin-views/count/:contentType/:id
ctx.routes.get('/count/:contentType/:id', (req) => {
const key = viewKey(req.params.contentType, req.params.id);
return { views: viewCounts.get(key) || 0 };
});
// ── Get top viewed content ──────────────────────────────────────
// GET /api/x/notty-plugin-views/top?limit=10
ctx.routes.get(
'/top',
(req) => {
const limit = Number(req.query.limit) || 10;
const sorted = [...viewCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([key, views]) => {
const [contentType, id] = key.split(':');
return { contentType, id, views };
});
return { data: sorted };
},
{
policies: ['isAuthenticated'],
description: 'Top viewed content entries',
}
);
// ── Content lifecycle hooks ─────────────────────────────────────
ctx.hooks.on('content:afterCreate', (payload) => {
ctx.log.info(`[Views] New entry: ${payload.contentType} #${payload.entry}`);
});
// ── Dashboard widget ────────────────────────────────────────────
const totalViews = [...viewCounts.values()].reduce((a, b) => a + b, 0);
ctx.admin.addDashboardWidget({
id: 'views:total',
title: 'Content Views',
icon: 'Eye',
size: 'small',
type: 'stat',
statValue: String(totalViews),
statLabel: 'Total views tracked',
statColor: '#8b5cf6',
});
// ── Sidebar navigation ──────────────────────────────────────────
ctx.admin.addNavigationItem({
id: 'views:analytics',
label: 'View Analytics',
icon: 'Eye',
path: '/plugins/views',
section: 'plugins',
});
// ── Settings ────────────────────────────────────────────────────
ctx.admin.addSettingsSection({
id: 'views:settings',
label: 'View Counter',
description: 'Configure which content types are tracked',
icon: 'Eye',
path: 'views',
type: 'config-schema',
});
ctx.log.info('View Counter plugin registered');
},
async init(ctx) {
// In production: create a database table for persistent storage
ctx.log.info('View Counter initialized');
},
async destroy(ctx) {
ctx.log.info('View Counter shutting down');
},
});
Step 3: Install in Your Project
// notty.config.ts
import { defineConfig } from '@notty/core/config';
import viewsPlugin from 'notty-plugin-views';
export default defineConfig({
plugins: [
{
source: viewsPlugin,
config: {
enabledTypes: 'article,page',
},
},
],
});
Step 4: Use the Plugin
Track a View (Call from Frontend)
// In your frontend JavaScript
await fetch('http://localhost:2102/api/x/notty-plugin-views/track/article/1');
Get View Count
curl http://localhost:2102/api/x/notty-plugin-views/count/article/1
# { "views": 42 }
Get Top Content
curl http://localhost:2102/api/x/notty-plugin-views/top?limit=5 \
-H "Authorization: Bearer $TOKEN"
Step 5: Next Steps
For a production-ready version:
- Persistent storage — use
ctx.databasein theinit()phase to create a views table - Rate limiting — prevent view inflation by tracking unique visitors (IP or session)
- Batch writes — buffer views in memory and flush to database periodically
- Admin page — build a custom admin page at
/plugins/viewswith charts
Plugin Extension Points Summary
| Extension | Method | What It Does |
|---|---|---|
| Routes | ctx.routes.get/post() |
Add API endpoints at /api/x/<name>/ |
| Hooks | ctx.hooks.on() |
React to content events |
| Policies | ctx.policies.register() |
Reusable auth checks |
| Dashboard widgets | ctx.admin.addDashboardWidget() |
Cards on the dashboard |
| Navigation | ctx.admin.addNavigationItem() |
Sidebar menu items |
| Settings | ctx.admin.addSettingsSection() |
Auto-generated settings form |
Источник: docs/cookbook/custom-plugin.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.