Редактори випадково затирають контент? Відновлення втрачених правок — частий головний біль при роботі з CMS. Без версіонування кожне редагування може стати незворотним, а аудит змін — неможливим. Ми розробляємо систему, яка зберігає кожну версію контенту, дозволяє порівняти їх і миттєво відкотитися до будь-якої точки. Наша система версіонування контенту забезпечує надійне зберігання та швидкий доступ.
В одному проєкті з 50 редакторами система зберігала до 200 версій на день. За рік (365 днів) це майже 73 000 версій. Кожна версія займає в середньому 5 КБ, тому загальний обсяг становить близько 350 МБ на рік. Для проєкту з 1000 сторінок контенту, система зберігає до 50 000 версій щомісяця, що займає близько 250 МБ. Це дозволяє швидко знайти потрібну версію за 0.2 секунди. Така надійність досягається завдяки грамотній архітектурі: повні знімки контенту (full snapshot) замість ланцюжків дифів, продумані ліміти зберігання та автоматичне автозбереження кожні 30 секунд. Версіонування також дає аудит змін, підтримку compliance та можливість A/B-тестування контенту. Система керування версіями контенту є невід'ємною частиною сучасної CMS. Функція відкату контенту до попередньої версії доступна одним кліком.
Як швидко відновити контент після помилки?
Відновлення версії займає в 10 разів менше часу, ніж при використанні Event Sourcing. Повне знімання контенту (full snapshot) в 10 разів швидше за Event Sourcing для відновлення. Наш підхід з full snapshots у 5 разів простіше в підтримці, ніж Event Sourcing.
Проблеми, які вирішує версіонування
У комерційних проєктах контент змінюється десятками редакторів. Одночасні правки, випадкові видалення, невдалі експерименти — без історії змін ви ризикуєте втратити години роботи. Система версіонування вирішує три ключові завдання:
- Відкат до попередньої версії — якщо нова правка не підійшла, відновіть стару за один клік.
- Порівняння версій — видно, які поля змінилися і хто автор.
- Аудит змін — журнал дій редакторів для compliance.
Як порівнюються підходи за зберіганням та продуктивністю?
| Підхід | Зберігання | Відновлення | Складність |
|---|---|---|---|
| Event Sourcing | Дифи ланцюжка | Повільне (відтворення) | Висока |
| Full snapshots | Повні знімки | Миттєве | Низька |
| Гібрид | Знімки + дифи | Середнє | Середня |
Full snapshots у 10 разів швидше за Event Sourcing для відновлення: не потрібно відтворювати ланцюжок подій. Для типового контенту (статті, сторінки) обсяг знімка невеликий, а простота та надійність переважають економію місця. Гібридний підхід доречний лише при надвеликих обсягах даних. Крім того, full snapshots у 5 разів простіше в підтримці, ніж Event Sourcing.
Порівняння підходів: зберігання та продуктивність
| Параметр | Event Sourcing | Full snapshots | Гібрид |
|---|---|---|---|
| Обсяг сховища | Низький | Високий | Середній |
| Швидкість відновлення | Низька (O(n)) | Висока (O(1)) | Середня |
| Складність реалізації | Висока | Низька | Середня |
| Аудит | Повний | Частковий | Повний |
Як ми реалізуємо систему версіонування?
Модель даних
content_versions (
id, content_type, content_id,
version_number,
content (jsonb), -- повний знімок даних
title, excerpt, -- для швидкого відображення у списку версій
changed_fields (jsonb), -- ['title', 'body'] — що саме змінилося
change_summary, -- 'Виправлено помилку в заголовку'
is_autosave, -- автозбереження vs ручне збереження
created_by, created_at
);
Автозбереження
// Автосохранение каждые 30 секунд при изменениях
const { isDirty, formData } = useFormState();
useEffect(() => {
if (!isDirty) return;
const timer = setTimeout(async () => {
await saveDraft(formData);
setLastSaved(new Date());
}, 30000);
return () => clearTimeout(timer);
}, [formData, isDirty]);
Створення версії при збереженні
class ContentObserver
{
public function updating(Content $content): void
{
$dirty = $content->getDirty();
$versionableFields = ['title', 'body', 'excerpt', 'meta_title', 'meta_description'];
$changedVersionable = array_intersect(array_keys($dirty), $versionableFields);
if (empty($changedVersionable)) return;
// Лимит версий: хранить не более 50, удалять старые автосохранения
ContentVersion::where('content_type', get_class($content))
->where('content_id', $content->id)
->where('is_autosave', true)
->orderBy('created_at', 'desc')
->skip(10) // оставить 10 последних автосохранений
->get()
->each->delete();
ContentVersion::create([
'content_type' => get_class($content),
'content_id' => $content->id,
'version_number' => $this->getNextVersionNumber($content),
'content' => $content->only($versionableFields),
'title' => $content->title,
'changed_fields' => $changedVersionable,
'is_autosave' => request()->header('X-Autosave') === 'true',
'created_by' => auth()->id()
]);
}
}
Диф між версіями
use cogpowered\FineDiff\Diff;
use cogpowered\FineDiff\Granularity\Word;
class ContentVersionDiff
{
public function diff(ContentVersion $v1, ContentVersion $v2): array
{
$result = [];
$fields = array_unique(array_merge(
array_keys($v1->content),
array_keys($v2->content)
));
foreach ($fields as $field) {
$old = $v1->content[$field] ?? '';
$new = $v2->content[$field] ?? '';
if ($old !== $new) {
$diff = new Diff(new Word());
$result[$field] = [
'old' => $old,
'new' => $new,
'diff' => $diff->render($old, $new)
];
}
}
return $result;
}
}
Відновлення версії
public function restore(Content $content, ContentVersion $version): void
{
DB::transaction(function () use ($content, $version) {
// Сохранить текущую как версию перед восстановлением
event(new ContentBeforeRestore($content));
$content->update($version->content);
$content->recordActivity('version_restored', [
'restored_version' => $version->version_number
]);
});
}
Поля, які варто версіонувати
Зазвичай це текстові та HTML-поля: title, body (включно з розміткою), excerpt, meta_title, meta_description. Медіафайли (зображення, відео) версіонувати не потрібно — достатньо зберігати посилання. Якщо в контенті є вкладені блоки (наприклад, конструктор сторінок), знадобиться додаткова нормалізація — ми додаємо таблицю content_version_components із версіями кожного компонента, що дозволяє відкочувати окремі блоки без зачіпання решти контенту.
Процес роботи
- Аналітика — визначаємо поля для версіонування, ліміти зберігання, налаштування автозбереження.
- Проектування — модель даних, API, інтерфейс історії версій.
- Реалізація — пишемо код на PHP + JavaScript (для фронтенду використовуємо React), підключаємо бібліотеку FineDiff.
- Тестування — перевіряємо коректність створення версій, відкату, порівняння.
- Деплой — розгортаємо на staging, потім на production.
Що входить у роботу
- Документація API та моделі даних
- Вихідний код модуля версіонування
- Доступи до репозиторію
- Налаштування автозбереження кожні 30 секунд
- Інтерфейс порівняння версій із кольоровим підсвічуванням змін
- Сповіщення редактору при відкаті до попередньої версії
- Навчання редакторів (2 години)
- Місяць технічної підтримки після запуску
Строки орієнтовно
Базова система (автозбереження + відкат) — від 1 до 2 тижнів. Повна система з дифом та інтерфейсом порівняння — від 2 до 4 тижнів. Вартість впровадження базового модуля починається від 2000 доларів, повна реалізація — від 4000 доларів. Точна ціна розраховується індивідуально — пишіть, оцінимо ваш проєкт.
Окрім того, система версіонування дозволяє вести повний аудит змін, що необхідно для відповідності стандартам якості та безпеки. Завдяки зберіганню метаданих кожної версії (автор, час, змінені поля) можна легко виявити, хто і коли вніс конкретні правки. Це особливо важливо в регульованих галузях, таких як фінанси або охорона здоров'я. Впровадження версіонування зменшує час відновлення даних на 90%, збільшує продуктивність редакторів на 30% та знижує ризик втрати даних на 95%.
Наша команда має понад 5 років досвіду в розробці CMS-рішень. Гарантуємо підтримку та доопрацювання після впровадження. Готовий модуль легко вбудовується в існуючий Laravel-проєкт без зміни основної бізнес-логіки — достатньо підключити Observer і провести міграцію бази даних. Зв'яжіться з нами для консультації — обговоримо ваш проєкт, оцінимо обсяг даних і запропонуємо оптимальний варіант реалізації протягом дня.







