API for document version history: automatic and manual snapshots, diff view, and restore. The plugin does not ship a history UI — you build the panel (or modal) around the editor and call app.versions.*. Optional review data on snapshots when Review is loaded.
Disabled automatically when the Schema or Email product is active. Use save() for manual snapshots; auto-capture runs on editor changes when auto is enabled.
<script src="/assets/redactor/redactor.js"></script>
<script src="/assets/redactor/plugins/versions/versions.js"></script>
Versions is a store and API inside the editor. It captures snapshots, keeps an original baseline, opens a diff against a version, and restores content. It does not add a history sidebar, toolbar button, or modal of its own.
You build that host UI in your app (sidebar, drawer, CMS panel) and wire buttons to app.versions.*. The demo on this page is one such shell — use the pattern below as a starting point.
Typical flow:
getVersions() / getOriginal().showVersion(id) (diff in the editor).closeVersion().save({ label }); Restore → restore(id).versions:change (and after manual save/restore).Minimal layout: editor on the left, your panel on the right.
<div class="versions-layout">
<div id="entry"></div>
<aside class="versions-sidebar">
<h3>Versions</h3>
<p>Original</p>
<div id="versions-original"></div>
<button type="button" id="versions-show-original">Show original</button>
<button type="button" id="versions-restore-original">Restore original</button>
<p>History</p>
<button type="button" id="versions-save">Save</button>
<button type="button" id="versions-close">Back to editor</button>
<button type="button" id="versions-restore">Restore version</button>
<div id="versions-list"></div>
<div id="versions-status"></div>
</aside>
</div>
const app = Redactor('#entry', {
plugins: ['versions'],
versions: {
documentId: 'doc-1',
user: { id: '1', name: 'Alex' },
auto: true,
original: {
content: '<p>Original baseline content.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z'
},
// items: [ ... ] // optional history from your CMS
},
content: '<p>Current editor content.</p>'
});
const listEl = document.getElementById('versions-list');
const originalEl = document.getElementById('versions-original');
const statusEl = document.getElementById('versions-status');
let selectedVersionId = null;
function renderOriginal() {
const original = app.versions.getOriginal();
if (!original) {
originalEl.textContent = 'Original not set.';
return;
}
const delta = Math.round((app.versions.getDeltaFromOriginal() || 0) * 100);
const date = original.createdAt ? new Date(original.createdAt).toLocaleString() : '';
originalEl.innerHTML = `<strong>Original</strong> · ${delta}% changed<br><small>${date}</small>`;
}
function renderList() {
const { items } = app.versions.getVersions();
listEl.innerHTML = items.map((v) => {
const date = v.createdAt ? new Date(v.createdAt).toLocaleString() : '';
return `<button type="button" data-id="${v.id}">
<strong>${v.label || v.id}</strong><br>
<small>${date} · ${v.reason || ''}</small>
</button>`;
}).join('');
}
listEl.addEventListener('click', (e) => {
const btn = e.target.closest('[data-id]');
if (!btn) return;
selectedVersionId = btn.dataset.id;
const ok = app.versions.showVersion(selectedVersionId);
statusEl.textContent = ok ? `Diff: ${selectedVersionId}` : 'Could not open diff';
});
document.getElementById('versions-close').addEventListener('click', () => {
app.versions.closeVersion();
statusEl.textContent = 'Back to editor';
});
document.getElementById('versions-show-original').addEventListener('click', () => {
const ok = app.versions.showOriginal();
statusEl.textContent = ok ? 'Diff: original' : 'Could not open original';
});
document.getElementById('versions-restore-original').addEventListener('click', async () => {
if (!confirm('Restore original? Current edits will be replaced.')) return;
const ok = await app.versions.restoreOriginal();
statusEl.textContent = ok ? 'Restored: original' : 'Restore original failed';
renderOriginal();
renderList();
});
document.getElementById('versions-save').addEventListener('click', async () => {
const saved = await app.versions.save({ label: 'Manual save' });
statusEl.textContent = saved ? `Saved: ${saved.id}` : 'No changes to save';
renderList();
});
document.getElementById('versions-restore').addEventListener('click', async () => {
if (!selectedVersionId) {
statusEl.textContent = 'Select a version first';
return;
}
if (!confirm('Restore this version? Current edits will be replaced.')) return;
const ok = await app.versions.restore(selectedVersionId);
statusEl.textContent = ok ? `Restored: ${selectedVersionId}` : 'Restore failed';
renderList();
});
app.on('versions:change', () => {
renderList();
renderOriginal();
});
app.on('editor:ready', () => {
renderList();
renderOriginal();
});
Style the sidebar however you like. Persist snapshots with handlers.saveVersion (see Options). Full method list is under API below.
const app = Redactor('#entry', {
plugins: ['versions'],
versions: {
documentId: 'doc-1',
user: { id: '1', name: 'Alex' },
original: {
content: '<p>Original baseline content.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z'
}
}
});
boolean)
truefalse to disable the plugin.'html' | 'json')
'html'original when format is omitted.boolean)
trueversions:capture hook.The plugin is disabled when Schema or Email plugins are active.
original (object | string | null) — the document baseline used for diff (showOriginal, getDeltaFromOriginal) and change level. It is stored separately from items and is never shown in the versions history list.
Pass an object:
original: {
content: '<p>First published draft.</p><p>Second paragraph.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z',
meta: { source: 'cms-import' }
}
Or a content string (format and createdAt are filled automatically):
original: '<p>First published draft.</p>'
When original is omitted, the plugin captures the current editor content on first load and uses that as the baseline.
Editor content on init should match (or intentionally differ from) the baseline you want to diff against. See demo.html for a realistic setup with original plus edited content.
items (array) — version snapshots loaded into the store on init. Use this to restore history from your CMS or API when opening a document.
Each item:
| Field | Type | Description |
|---|---|---|
id |
string |
Version id (generated if omitted) |
content |
string |
Snapshot HTML or JSON string |
format |
'html' \| 'json' |
Defaults to plugin format |
createdAt |
string |
ISO timestamp |
reason |
string |
'auto', 'manual', 'restore', 'ai', 'paste' |
label |
string \| null |
UI label (for example Auto saved, Before publish) |
author |
object \| null |
{ id, name } |
delta |
number \| null |
Change ratio 0–1 from previous baseline |
meta |
object |
Custom metadata (reviews, etc.) |
versions: {
documentId: 'news-night-bus',
user: { id: '1', name: 'Alex' },
original: {
content: '<h2>Night Bus Pilot Approved</h2><p>Original lead paragraph.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z'
},
items: [
{
id: 'v-auto-june-12',
content: '<h2>Night Bus Pilot Approved</h2><p>Updated lead with more detail.</p>',
format: 'html',
createdAt: '2026-06-12T09:08:49.225Z',
reason: 'auto',
label: 'Auto saved',
author: { id: '1', name: 'Alex' },
delta: 0.12,
meta: {}
},
{
id: 'v-imported',
content: '<p>Imported history item from an older CMS export.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z',
reason: 'manual',
label: 'Imported',
author: null,
delta: null,
meta: {}
}
]
}
Items with identical content are deduplicated on load (latest wins). After init, use app.versions.getVersions() — items are returned newest first.
user (object | null) — attached to every new version as author:
user: {
id: '42',
name: 'Alex Morgan'
}
Result on save:
{
id: 'abc123',
reason: 'manual',
label: 'Manual save',
author: { id: '42', name: 'Alex Morgan' },
// ...
}
handlers (object | null) — override persistence. Currently supports saveVersion:
handlers: {
saveVersion: async ({ app, documentId, version }) => {
const response = await fetch('/api/documents/' + documentId + '/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(version)
});
const saved = await response.json();
// Optional: merge server fields (for example canonical id)
return { ...version, id: saved.id };
}
}
The handler runs on every auto/manual/restore snapshot. If it throws, versions:error is emitted and the version is still kept in the local store.
boolean) — default true. Capture versions on meaningful edits.number) — default 0.08. Minimum change ratio (0–1) vs last baseline.number) — default 60000. Minimum ms between auto snapshots.number) — default 80. Auto-save also requires this many characters changed (unless minWordsChanged is met).number) — default 10. Alternative word-count threshold.versions: {
auto: true,
minDelta: 0.05,
minInterval: 120000,
minCharsChanged: 40,
minWordsChanged: 5
}
When the store grows, the pruner removes old auto snapshots. Protected versions are always kept:
reason: 'manual' when keepManual is true (default)reason: 'restore' (always kept)true (default)maxItems (number) — default 100. Hard cap after retention; protected versions are kept first, then auto versions with the highest delta.
retention (object) — bucket size in seconds for grouping auto versions by age. Within each bucket only the latest auto version is kept. Versions younger than 1 hour are always kept.
Default:
retention: {
lastDay: 15 * 60, // 15 min buckets for versions 1h–24h old
lastWeek: 60 * 60, // 1 h buckets for versions 1–7 days old
lastMonth: 24 * 60 * 60, // 1 day buckets for versions 7–30 days old
older: 7 * 24 * 60 * 60 // 7 day buckets for versions older than 30 days
}
Example — keep more recent auto saves, prune aggressively after a week:
versions: {
maxItems: 50,
retention: {
lastDay: 5 * 60,
lastWeek: 30 * 60,
lastMonth: 12 * 60 * 60,
older: 3 * 24 * 60 * 60
},
keepManual: true,
keepLabeled: true
}
boolean) — default true. Never prune manual saves.boolean) — default true. Never prune versions with a label.boolean) — default true. Reserved; the original baseline lives outside items.documentId (string | null) — passed to handlers.saveVersion so your backend knows which document the snapshot belongs to.
const app = Redactor('#entry', {
plugins: ['versions'],
versions: {
documentId: 'post-42',
user: { id: '1', name: 'Alex' },
auto: true,
minInterval: 120000,
maxItems: 50,
original: {
content: '<p>Baseline draft.</p>',
format: 'html',
createdAt: '2026-06-01T10:00:00.000Z'
},
items: [
{
id: 'v1',
content: '<p>Baseline draft with edits.</p>',
createdAt: '2026-06-10T12:00:00.000Z',
reason: 'auto',
label: 'Auto saved'
}
],
handlers: {
saveVersion: async ({ documentId, version }) => {
await fetch(`/api/posts/${documentId}/versions`, {
method: 'POST',
body: JSON.stringify(version)
});
return version;
}
}
}
});
Fired after a version snapshot is saved.
Payload: id, version, reason ('auto', 'manual', 'restore', etc.).
app.on('versions:saved', ({ id, version, reason }) => {
console.log('version saved', id, reason, version.label, version.delta);
});
Fired after version content is restored.
Payload: id, version.
app.on('versions:restored', ({ id, version }) => {
console.log('restored from', id, version.createdAt);
});
Fired when the versions store changes (save, restore, setStore).
Payload: store snapshot — { original, items }.
app.on('versions:change', ({ original, items }) => {
console.log('versions updated', items.length, original?.createdAt);
});
Fired when a version diff panel opens.
Payload: id, version. When showing the original baseline, id is null and original is true.
app.on('versions:show', ({ id, version, original }) => {
if (original) {
console.log('showing diff vs original baseline');
return;
}
console.log('showing diff vs version', id, version.label);
});
Fired when the versions panel closes.
Payload: id — id of the version that was open, or null.
app.on('versions:close', ({ id }) => {
console.log('diff panel closed', id);
});
Hook while capturing a snapshot. Return { meta, content, format? } to extend the snapshot (for example attach review data).
Payload: content, format, meta.
app.on('versions:capture', (snapshot) => {
return {
meta: {
...snapshot.meta,
wordCount: snapshot.content?.split(/\s+/).length || 0
}
};
});
Hook before a version is restored. Used by the Review plugin to restore review state from item.meta.reviews.
Payload: item, restoreReviews.
app.on('versions:restore', ({ item, restoreReviews }) => {
console.log('restoring version', item.id, 'reviews:', restoreReviews);
});
Returns whether the plugin initialized successfully.
app.versions.isEnabled();
Captures and saves a manual version snapshot.
await app.versions.save({ reason: 'manual', label: 'Before publish' });
Alias for creating a version with options.
await app.versions.createVersion({ reason: 'manual' });
Returns { items, … } from the version store.
const { items } = app.versions.getVersions();
Returns a single version by id.
const v = app.versions.getVersion('abc123');
Restores document content from a version id.
await app.versions.restore('abc123');
Opens diff view for a version in the editor area.
app.versions.showVersion('abc123');
Closes the version diff view and returns to the editor.
app.versions.closeVersion();
Get or set the baseline original snapshot.
const original = app.versions.getOriginal();
app.versions.setOriginal({ content: '<p>Baseline</p>', format: 'html' });
Restores content from the original baseline.
await app.versions.restoreOriginal();
Returns HTML diff between two content strings.
const html = app.versions.getDiffHtml(baseHtml, versionHtml);
Returns change ratio (0–1) from the original baseline.
const delta = app.versions.getDeltaFromOriginal();
Captures a snapshot without saving to the store.
const snapshot = app.versions.captureSnapshot();
Get or replace the full internal store.
const store = app.versions.getStore();