Разработка CMS-системы для управления контентом сайта
Заказная CMS пишется тогда, когда ни одна из существующих платформ не вписывается в бизнес-логику — или вписывается, но требует столько кастомизации, что проще построить своё. Типичные случаи: специфическая модель данных, нестандартные рабочие процессы согласования, интеграция с внутренними корпоративными системами, требования к производительности на уровне десятков тысяч запросов в минуту.
Из чего состоит CMS
Минимальный набор модулей для функциональной CMS:
- Content Types — определение структур данных: поля, типы, связи
- Content Editor — интерфейс создания и редактирования записей
- Media Manager — загрузка, хранение, обработка изображений и файлов
- User & Roles — аутентификация, авторизация, разграничение прав
- Publishing Workflow — черновики, согласование, планировщик публикаций
- API — REST или GraphQL для фронта и мобильных приложений
- Audit Log — кто что изменил и когда
Выбор стека
Для административной панели CMS чаще всего используется React или Vue с компонентной архитектурой. Backend — Laravel, Node.js, Go или Django в зависимости от команды и требований.
Пример стека, который хорошо себя показывает:
Backend API: Laravel 11 + PostgreSQL
Admin SPA: React 18 + TypeScript + TanStack Query
Media CDN: S3-совместимое хранилище + imgproxy для обработки
Auth: JWT + refresh tokens / или session-based
Модель данных: гибкость vs строгость
Два подхода к хранению контента:
Structured (жёсткая схема) — отдельная таблица на каждый тип контента:
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(255) UNIQUE NOT NULL,
title JSONB NOT NULL, -- {ru: "...", en: "..."}
body JSONB NOT NULL,
author_id BIGINT REFERENCES users(id),
status VARCHAR(32) DEFAULT 'draft',
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
Преимущество: индексы, типизация, JOIN-запросы. Недостаток: каждое изменение структуры — миграция.
EAV / JSONB (гибкая схема) — поля хранятся в JSON:
CREATE TABLE content_items (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(64) NOT NULL, -- 'article', 'product', 'event'
slug VARCHAR(255),
data JSONB NOT NULL, -- все поля внутри
meta JSONB DEFAULT '{}',
status VARCHAR(32) DEFAULT 'draft',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_content_data ON content_items USING gin(data);
Преимущество: новые поля без миграций. Недостаток: сложнее запрашивать, нет строгой типизации.
Гибридный подход — общая таблица с ключевыми полями (slug, status, author_id, published_at) и JSONB для произвольных полей конкретного типа — даёт лучшее из обоих миров.
Content Editor
Выбор редактора влияет на весь UX системы:
- Lexical (Meta) — гибкий, расширяемый, активно развивается
- TipTap (на базе ProseMirror) — хорошая экосистема расширений
- Slate.js — максимальная гибкость, но больше кода
- TinyMCE / CKEditor — для клиентов, привыкших к Word-подобному интерфейсу
Для контента со сложной структурой (блочный редактор как в Notion) строится своя реализация на основе одного из них.
API для фронта
// GET /api/v1/articles/:slug
// Response
{
"id": 42,
"slug": "how-to-start",
"title": { "ru": "С чего начать", "en": "How to start" },
"body": { "ru": "<p>...</p>", "en": "<p>...</p>" },
"author": { "id": 1, "name": "Иван Петров" },
"status": "published",
"published_at": "2025-06-01T10:00:00Z",
"meta": {
"seo_title": "...",
"seo_description": "...",
"og_image": "https://cdn.example.com/images/42.jpg"
}
}
GraphQL как альтернатива REST удобен, когда фронтов несколько с разными требованиями к полям.
Права доступа
RBAC-модель на основе разрешений:
// Матрица прав: роль → действие → тип контента
$permissions = [
'editor' => ['create:article', 'edit:own:article', 'publish:article'],
'moderator' => ['edit:any:article', 'delete:article'],
'admin' => ['*'],
];
Для строчного-уровневого контроля (пользователь видит только свои записи) — добавить условие WHERE author_id = :current_user_id к запросам через Policy/Gate.
Версионирование контента
CREATE TABLE content_revisions (
id BIGSERIAL PRIMARY KEY,
content_id BIGINT REFERENCES content_items(id) ON DELETE CASCADE,
data JSONB NOT NULL,
author_id BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
Последние N ревизий хранятся для каждой записи. Восстановление — копирование нужной ревизии в основную запись.
Медиа-менеджер
Загрузка файлов через presigned S3 URL — файл идёт напрямую в хранилище, минуя сервер приложения:
Client → POST /api/media/presign → получить presigned URL
Client → PUT presigned_url (S3) → загрузить файл
Client → POST /api/media/confirm → сообщить серверу об успешной загрузке
Server → imgproxy или CloudFront → ресайз по требованию
Сроки реализации
Базовая CMS: один тип контента, редактор, медиа, ролевая модель, REST API — 3–4 недели. Полноценная мультиязычная CMS с несколькими типами контента, версионированием, workflow согласования, GraphQL, CDN — 2–3 месяца.







