The Problem with Save-and-Preview

The traditional CMS workflow is: edit content, save, open new tab, refresh, check the result, go back, edit more, save again, refresh again. For a 1,000-word blog post with 3-4 rounds of editing, that's 8-12 context switches. Each one breaks your focus and wastes 5-10 seconds.

We built a live preview system that shows changes in real-time as you type. No save required, no page refresh. The preview updates within 200ms of each keystroke.

Architecture: Server-Sent Events

We considered WebSocket but chose Server-Sent Events (SSE) for three reasons: SSE works over standard HTTP (no special server configuration), it's unidirectional (server to client) which is all we need for preview, and it auto-reconnects if the connection drops.

// admin/ajax/preview-stream.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

session_write_close(); // Release session lock

while (true) {
    $previewData = getPreviewData($_GET['post_id']);

    if ($previewData && $previewData['updated'] > $lastUpdate) {
        echo "data: " . json_encode($previewData) . "

";
        ob_flush();
        flush();
        $lastUpdate = $previewData['updated'];
    }

    sleep(1); // Check every second
    if (connection_aborted()) break;
}

Client-Side: Debounced Updates

On the editor side, we debounce content changes to avoid overwhelming the preview with updates on every keystroke:

const editor = document.querySelector('.content-editor');
let debounceTimer;

editor.addEventListener('input', () => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
        const content = editor.value;
        fetch('/admin/ajax/save-preview.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                post_id: postId,
                content: content,
                title: document.querySelector('#title').value,
            }),
        });
    }, 300); // Wait 300ms after last keystroke
});

Preview Iframe

The preview lives in an iframe that receives SSE updates and re-renders only the changed content (not the full page):

// In the preview iframe
const source = new EventSource('/admin/ajax/preview-stream.php?post_id=' + postId);

source.onmessage = (event) => {
    const data = JSON.parse(event.data);
    document.querySelector('.post-title').textContent = data.title;
    document.querySelector('.post-content').innerHTML = data.content;
};

By updating only the content elements instead of reloading the full iframe, we avoid flickering and preserve scroll position.

Responsive Preview

The preview panel includes device toggle buttons that resize the iframe to mobile (375px), tablet (768px), or desktop (100%) width. This lets content creators check how their post looks on different screen sizes without leaving the editor.

Auto-Save Integration

The preview system piggybacks on the auto-save mechanism. Every preview update is also saved as a draft revision. If the browser crashes or the tab closes, the latest draft is preserved. We keep the last 10 revisions per post, each timestamped, allowing content creators to revert to any previous version.