Разделы документации
ОбзорБыстрый стартРедакции и возможностиМодели и поляРедактор контентаМедиатекаЛокализацияПубликация и работа командыAPI, SDK и генерация типовРасширения и инструментыРабочие проектыЗадания, вебхуки и наблюдаемостьАудит и управление даннымиСоветники и доверие к плагинамКорпоративный входПространства, квоты и масштабированиеCommerce и PortalПрава и безопасностьРазвёртывание и обновленияЛицензии и установка пакетовТекущие ограниченияПомощь и диагностикаДанные в кабинетеCore CMSDeveloper PlatformProduction UseWorkflowOperationsComplianceAI AssistantsPlugin TrustEnterprise IdentityEnterprise ScaleEnterprise DeploymentCommerce BundlePortal BundleNotty CMS DocumentationAuth & SecurityContent ModelingDeploymentEcosystem & Packaging ConventionsEditions and First-party ModulesExtensibilityGetting StartedMedia ManagementModule Extraction PathDraft & PublishUpgrade GuideWebhooks & IntegrationsCookbook: Blog with Next.jsCookbook: Custom PluginCookbook: Multilingual SiteOperations DocsBackup AutomationDeployment BlueprintsRunbook — Восстановление БД из бэкапаRunbook — Плановый деплойRunbook — Реакция на инцидентRunbook — Откат релизаRunbook — Горизонтальное масштабированиеRunbook — Ротация секретовRunbook — Major upgradeSecrets ManagementNotty CMS — Capability MapNotty Configuration ModelGenerated App ContractComponents and Dynamic ZonesMiddleware SystemPerformance & Scaling ToolkitDisaster Recovery PlaybookDistribution Model
Документация / Технический справочник

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-counter or create-notty my-plugin --plugin. Direct npm install @notty/plugin-api requires access to the restricted @notty npm scope.

What You'll Build

  • A view-counter plugin 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:

  1. Persistent storage — use ctx.database in the init() phase to create a views table
  2. Rate limiting — prevent view inflation by tracking unique visitors (IP or session)
  3. Batch writes — buffer views in memory and flush to database periodically
  4. Admin page — build a custom admin page at /plugins/views with 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. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.