Content Versioning System Development
A versioning system saves change history for each page or post. Editor can compare versions, restore previous, or revert to specific point. Protects against accidental content loss and allows auditing changes.
Implementation Options
Event Sourcing — store only diff between versions. Saves space, but version restoration requires replaying change chain.
Full snapshots — store complete snapshot of each version. Simpler to implement, takes more space, instant restoration.
Hybrid — full snapshot every N versions, diff between them.
For most CMS — full snapshots: text volume is small, simplicity is more important than storage savings.
Data Model
content_versions (
id, content_type, content_id,
version_number,
content (jsonb), -- full data snapshot
title, excerpt, -- for quick display in version list
changed_fields (jsonb), -- ['title', 'body'] — what changed
change_summary, -- 'Fixed typo in title'
is_autosave, -- autosave vs manual save
created_by, created_at
)
Autosave
// Autosave every 30 seconds on changes
const { isDirty, formData } = useFormState();
useEffect(() => {
if (!isDirty) return;
const timer = setTimeout(async () => {
await saveDraft(formData);
setLastSaved(new Date());
}, 30000);
return () => clearTimeout(timer);
}, [formData, isDirty]);
Creating Version on Save
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;
// Version limit: keep max 50, delete old autosaves
ContentVersion::where('content_type', get_class($content))
->where('content_id', $content->id)
->where('is_autosave', true)
->orderBy('created_at', 'desc')
->skip(10) // keep last 10 autosaves
->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 Between Versions
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;
}
}
Version Restoration
public function restore(Content $content, ContentVersion $version): void
{
DB::transaction(function () use ($content, $version) {
// Save current as version before restoration
event(new ContentBeforeRestore($content));
$content->update($version->content);
$content->recordActivity('version_restored', [
'restored_version' => $version->version_number
]);
});
}
Version History Interface
Side panel or separate page:
- Version list: number, date, author, changed fields, type (manual/autosave)
- Click version → preview content
- Compare two versions: select "base" and "compare", view diff
- "Restore" button with confirmation
Development timeline: 2–4 weeks for complete system with diff, autosave, and version comparison interface.







