Разделы документации
ОбзорБыстрый стартРедакции и возможностиМодели и поляРедактор контентаМедиатекаЛокализацияПубликация и работа команды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
Документация / Технический справочник

Content Modeling

Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.

Все редакцииОбновлено 2026-09-30

Notty CMS uses JSON schemas to define content types. Schemas live in the schemas/ directory of your project and are auto-synced to the database on server startup.

Schema Structure

Every schema has this shape:

{
  "kind": "collectionType",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article",
    "description": "Blog articles"
  },
  "options": {
    "draftAndPublish": true,
    "timestamps": true
  },
  "attributes": {
    "title": { "type": "string", "required": true }
  }
}

Kind

  • collectionType — standard collection with many entries (articles, products, users)
  • singleType — singleton with exactly one entry (homepage, site settings)

Options

Option Type Default Description
draftAndPublish boolean false Enable draft/publish workflow
timestamps boolean true Auto-add createdAt and updatedAt
softDelete boolean false Enable deletedAt instead of hard delete
i18n.enabled boolean false Enable content localization
i18n.locales string[] all system locales Restrict to specific locales
i18n.defaultLocale string system default Default locale for this type
workflow object — Enable approval workflow (see Publishing)

Composite Indexes

Define database indexes at the schema level:

{
  "indexes": [
    { "fields": ["slug", "locale"], "type": "unique" },
    { "fields": ["title"], "type": "fulltext" },
    { "fields": ["category_id", "published_at"], "type": "index" }
  ]
}

Field Types

String

Short text, stored as VARCHAR. Use for titles, names, slugs.

{
  "type": "string",
  "required": true,
  "unique": false,
  "minLength": 1,
  "maxLength": 255,
  "default": "Untitled"
}

Text

Longer text, stored as TEXT/LONGTEXT. Use for descriptions, excerpts.

{
  "type": "text",
  "required": false,
  "minLength": 0,
  "maxLength": 65535
}

Rich Text

HTML or structured text, stored as LONGTEXT. Rendered as a rich-text editor in admin.

{
  "type": "richtext"
}

Integer

Whole numbers, stored as INT.

{
  "type": "integer",
  "required": true,
  "min": 0,
  "max": 100,
  "default": 0
}

Big Integer

Large numbers, stored as BIGINT. Use for IDs from external systems.

{
  "type": "bigint"
}

Decimal

Precise floating-point numbers, stored as DECIMAL. Use for prices, coordinates.

{
  "type": "decimal",
  "min": 0,
  "max": 99999.99
}

Boolean

True/false, stored as BOOLEAN.

{
  "type": "boolean",
  "default": false,
  "required": true
}

Date

Date only (no time), stored as DATE.

{
  "type": "date"
}

DateTime

Date and time, stored as TIMESTAMP.

{
  "type": "datetime",
  "default": "now"
}

JSON

Arbitrary JSON data. Use when structure varies or for complex nested data.

{
  "type": "json",
  "default": {}
}

Enum

One value from a predefined list.

{
  "type": "enum",
  "enum": ["draft", "review", "approved", "rejected"],
  "default": "draft",
  "required": true
}

Media

Reference to a file in the media library. See Media Management.

{
  "type": "media",
  "allowedTypes": ["images"],
  "multiple": false
}

allowedTypes accepts: "images", "videos", "audios", "files". Omit for any type.

Set "multiple": true to allow multiple files (gallery).

Relations

Relations link content types together. Notty supports four relation types.

Many-to-One

An article belongs to one category. Many articles share the same category.

// schemas/article.json
{
  "attributes": {
    "category": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "category"
    }
  }
}

One-to-Many

A category has many articles. Define the inverse side with mappedBy:

// schemas/category.json
{
  "attributes": {
    "articles": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "article",
      "mappedBy": "category"
    }
  }
}

One-to-One

A user has one profile. The owning side defines the foreign key:

// schemas/user-profile.json
{
  "attributes": {
    "user": {
      "type": "relation",
      "relation": "oneToOne",
      "target": "user"
    }
  }
}

Many-to-Many

Articles have many tags, and tags belong to many articles:

// schemas/article.json
{
  "attributes": {
    "tags": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "tag",
      "inversedBy": "articles"
    }
  }
}

// schemas/tag.json
{
  "attributes": {
    "articles": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "article",
      "mappedBy": "tags"
    }
  }
}

Cascade Delete

Control what happens when a related entry is deleted:

{
  "type": "relation",
  "relation": "manyToOne",
  "target": "category",
  "onDelete": "SET NULL"
}

Options: CASCADE, SET NULL, RESTRICT.

Working with Relations via API

Create with relation (by ID):

curl -X POST http://localhost:2102/api/content/article \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "title": "My Article", "category": 1 }'

Link an existing entry:

curl -X POST http://localhost:2102/api/content/article/1/link/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "id": 5 }'

Unlink:

curl -X DELETE http://localhost:2102/api/content/article/1/unlink/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "id": 5 }'

Populate relations in queries:

curl "http://localhost:2102/api/content/article?populate=category,tags" \
  -H "Authorization: Bearer $TOKEN"

Components

Components are reusable field groups. Define them once, use in multiple content types.

Define a Component

Create components/shared/seo-meta.json:

{
  "uid": "shared.seo-meta",
  "category": "shared",
  "info": {
    "displayName": "SEO Meta",
    "description": "SEO metadata for any page",
    "icon": "mdi:search-web"
  },
  "attributes": {
    "metaTitle": { "type": "string", "maxLength": 60 },
    "metaDescription": { "type": "text", "maxLength": 160 },
    "metaKeywords": { "type": "string" },
    "noIndex": { "type": "boolean", "default": false }
  }
}

Component UIDs follow the pattern category.name (e.g., shared.seo-meta, blocks.hero).

Use a Component in a Schema

Single component (one instance):

{
  "seo": {
    "type": "component",
    "component": "shared.seo-meta"
  }
}

Repeatable component (array of instances):

{
  "gallery": {
    "type": "component",
    "component": "blocks.image-slide",
    "repeatable": true,
    "min": 1,
    "max": 20
  }
}

Component API Payloads

Create:

{
  "title": "About Us",
  "seo": {
    "metaTitle": "About Our Company",
    "metaDescription": "Learn about our team."
  }
}

Repeatable component:

{
  "gallery": [
    { "imageUrl": "/uploads/photo1.jpg", "caption": "Office" },
    { "imageUrl": "/uploads/photo2.jpg", "caption": "Team" }
  ]
}

Dynamic Zones

Dynamic zones allow mixing different component types in an ordered list. Use them for page builders, flexible content blocks.

Define Components for Dynamic Zone

// components/blocks/hero.json
{
  "uid": "blocks.hero",
  "category": "blocks",
  "info": { "displayName": "Hero Section", "icon": "mdi:view-dashboard" },
  "attributes": {
    "title": { "type": "string", "required": true },
    "subtitle": { "type": "text" },
    "backgroundImage": { "type": "media", "allowedTypes": ["images"] }
  }
}

// components/blocks/text-block.json
{
  "uid": "blocks.text-block",
  "category": "blocks",
  "info": { "displayName": "Text Block", "icon": "mdi:text-box" },
  "attributes": {
    "content": { "type": "richtext", "required": true }
  }
}

// components/blocks/cta.json
{
  "uid": "blocks.cta",
  "category": "blocks",
  "info": { "displayName": "Call to Action", "icon": "mdi:cursor-default-click" },
  "attributes": {
    "heading": { "type": "string", "required": true },
    "buttonText": { "type": "string", "required": true },
    "buttonUrl": { "type": "string", "required": true },
    "style": { "type": "enum", "enum": ["primary", "secondary", "outline"] }
  }
}

Use a Dynamic Zone

// schemas/page.json
{
  "kind": "collectionType",
  "info": {
    "singularName": "page",
    "pluralName": "pages",
    "displayName": "Page"
  },
  "attributes": {
    "title": { "type": "string", "required": true },
    "slug": { "type": "string", "unique": true },
    "blocks": {
      "type": "dynamicZone",
      "components": ["blocks.hero", "blocks.text-block", "blocks.cta"]
    }
  }
}

Dynamic Zone API Payloads

Every block must include __component to identify its type:

{
  "title": "Home",
  "slug": "home",
  "blocks": [
    {
      "__component": "blocks.hero",
      "title": "Welcome to Our Site",
      "subtitle": "Building the future"
    },
    {
      "__component": "blocks.text-block",
      "content": "<p>We are a team of passionate developers.</p>"
    },
    {
      "__component": "blocks.cta",
      "heading": "Ready to start?",
      "buttonText": "Contact Us",
      "buttonUrl": "/contact",
      "style": "primary"
    }
  ]
}

Block order in the request is preserved in the response.

Conditional Fields

Show or hide fields based on another field's value:

{
  "type": { "type": "enum", "enum": ["internal", "external"] },
  "internalPage": {
    "type": "relation",
    "relation": "manyToOne",
    "target": "page",
    "condition": {
      "field": "type",
      "operator": "eq",
      "value": "internal"
    }
  },
  "externalUrl": {
    "type": "string",
    "condition": {
      "field": "type",
      "operator": "eq",
      "value": "external"
    }
  }
}

Available operators: eq, neq, in, notIn, exists, empty.

System Schemas

Notty ships with built-in schemas: user, media, media-folder, role. Their core fields are protected — you cannot modify or remove them. But you can extend them with additional fields by creating extension files:

// schemas/user.extension.json
{
  "attributes": {
    "bio": { "type": "text" },
    "avatar": { "type": "media", "allowedTypes": ["images"] }
  }
}

Extension fields merge with the system schema at runtime.

Validation Rules Summary

Rule Applies to Example
required all fields "required": true
unique string, integer, email "unique": true
minLength / maxLength string, text "maxLength": 255
min / max integer, bigint, decimal "min": 0, "max": 100
enum enum "enum": ["a", "b", "c"]
min / max (repeatable) component (repeatable) "min": 1, "max": 10

Full Example: E-commerce Product

{
  "kind": "collectionType",
  "info": {
    "singularName": "product",
    "pluralName": "products",
    "displayName": "Product"
  },
  "options": {
    "draftAndPublish": true,
    "timestamps": true,
    "i18n": { "enabled": true }
  },
  "indexes": [
    { "fields": ["slug", "locale"], "type": "unique" },
    { "fields": ["price"], "type": "index" }
  ],
  "attributes": {
    "name": {
      "type": "string",
      "required": true,
      "maxLength": 200,
      "pluginOptions": { "i18n": { "localized": true } }
    },
    "slug": {
      "type": "string",
      "unique": true
    },
    "description": {
      "type": "richtext",
      "pluginOptions": { "i18n": { "localized": true } }
    },
    "price": {
      "type": "decimal",
      "required": true,
      "min": 0
    },
    "sku": {
      "type": "string",
      "unique": true,
      "required": true
    },
    "inStock": {
      "type": "boolean",
      "default": true
    },
    "images": {
      "type": "media",
      "allowedTypes": ["images"],
      "multiple": true
    },
    "category": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "product-category"
    },
    "tags": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "tag",
      "inversedBy": "products"
    },
    "seo": {
      "type": "component",
      "component": "shared.seo-meta"
    },
    "sections": {
      "type": "dynamicZone",
      "components": ["blocks.hero", "blocks.text-block", "blocks.cta"]
    }
  }
}

Источник: docs/guide/content-modeling.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.