Versions

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.

Versions

Original (not in versions list)

Versions history

Load the bundle

<script src="/assets/redactor/redactor.js"></script>
<script src="/assets/redactor/plugins/versions/versions.js"></script>

No built-in UI

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:

  1. Render the list from getVersions() / getOriginal().
  2. On item click → showVersion(id) (diff in the editor).
  3. Back to editorcloseVersion().
  4. Savesave({ label }); Restorerestore(id).
  5. Refresh the list on versions:change (and after manual save/restore).

Host UI example

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.

Initialization

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'
        }
    }
});

Options

General

  • enabled (boolean)
    • default true
    • Set to false to disable the plugin.
  • format ('html' | 'json')
    • default 'html'
    • Snapshot content format for new versions and for original when format is omitted.
  • storeReviews (boolean)
    • default true
    • When the Review plugin is active, review data is attached to snapshots via the versions:capture hook.

The plugin is disabled when Schema or Email plugins are active.

Original baseline

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.

Preloaded version history

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.

Author metadata

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' },
    // ...
}

Custom save handler

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.

Auto capture

  • auto (boolean) — default true. Capture versions on meaningful edits.
  • minDelta (number) — default 0.08. Minimum change ratio (0–1) vs last baseline.
  • minInterval (number) — default 60000. Minimum ms between auto snapshots.
  • minCharsChanged (number) — default 80. Auto-save also requires this many characters changed (unless minWordsChanged is met).
  • minWordsChanged (number) — default 10. Alternative word-count threshold.
versions: {
    auto: true,
    minDelta: 0.05,
    minInterval: 120000,
    minCharsChanged: 40,
    minWordsChanged: 5
}

Pruning & retention

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)
  • any version with a label when keepLabeled is true (default)
  • the most recent version (always kept)

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
}
  • keepManual (boolean) — default true. Never prune manual saves.
  • keepLabeled (boolean) — default true. Never prune versions with a label.
  • keepOriginal (boolean) — default true. Reserved; the original baseline lives outside items.

Document id

documentId (string | null) — passed to handlers.saveVersion so your backend knows which document the snapshot belongs to.

Full example

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;
            }
        }
    }
});

Events

versions:saved

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);
});

versions:restored

Fired after version content is restored.

Payload: id, version.

app.on('versions:restored', ({ id, version }) => {
    console.log('restored from', id, version.createdAt);
});

versions:change

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);
});

versions:show

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);
});

versions:close

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);
});

versions:capture

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
        }
    };
});

versions:restore

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);
});

API

isEnabled

Returns whether the plugin initialized successfully.

app.versions.isEnabled();

save

Captures and saves a manual version snapshot.

await app.versions.save({ reason: 'manual', label: 'Before publish' });

createVersion

Alias for creating a version with options.

await app.versions.createVersion({ reason: 'manual' });

getVersions

Returns { items, … } from the version store.

const { items } = app.versions.getVersions();

getVersion

Returns a single version by id.

const v = app.versions.getVersion('abc123');

restore

Restores document content from a version id.

await app.versions.restore('abc123');

showVersion

Opens diff view for a version in the editor area.

app.versions.showVersion('abc123');

closeVersion

Closes the version diff view and returns to the editor.

app.versions.closeVersion();

getOriginal / setOriginal

Get or set the baseline original snapshot.

const original = app.versions.getOriginal();
app.versions.setOriginal({ content: '<p>Baseline</p>', format: 'html' });

restoreOriginal

Restores content from the original baseline.

await app.versions.restoreOriginal();

getDiffHtml

Returns HTML diff between two content strings.

const html = app.versions.getDiffHtml(baseHtml, versionHtml);

getDeltaFromOriginal

Returns change ratio (0–1) from the original baseline.

const delta = app.versions.getDeltaFromOriginal();

captureSnapshot

Captures a snapshot without saving to the store.

const snapshot = app.versions.captureSnapshot();

getStore / setStore

Get or replace the full internal store.

const store = app.versions.getStore();