How we built a live preview system using Server-Sent Events that updates the preview iframe as you type — no page reload needed.
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.