Разработка системы версионирования контента сайта
Система версионирования сохраняет историю изменений каждой страницы или записи. Редактор может сравнить версии, восстановить предыдущую или откатиться к конкретной точке. Защищает от случайных потерь контента и позволяет аудитировать изменения.
Варианты реализации
Event Sourcing — хранить только diff между версиями. Экономия места, но восстановление версии требует воспроизведения цепочки изменений.
Full snapshots — хранить полный снимок каждой версии. Проще в реализации, больше занимает места, мгновенное восстановление.
Гибрид — полный снимок каждые N версий, diff между ними.
Для большинства CMS — full snapshots: объём текста невелик, простота важнее экономии.
Модель данных
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()
]);
}
}
Diff между версиями
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
]);
});
}
Интерфейс истории версий
Боковая панель или отдельная страница:
- Список версий: номер, дата, автор, изменённые поля, тип (ручное/автосохранение)
- Клик по версии → предпросмотр содержимого
- Сравнение двух версий: выбрать "базовую" и "сравниваемую", просмотр diff
- Кнопка "Восстановить" с подтверждением
Срок разработки: 2–4 недели для полной системы с diff, автосохранением и интерфейсом сравнения версий.







