# How Instatic Manages Live Editing Versus Drafts in the Content Workspace

> Discover how Instatic's content workspace separates live edits from drafts using a React hook and draft-aware server, ensuring your public site stays untouched until published.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-28

---

**Instatic isolates draft edits inside a React hook and renders live previews through a draft-aware server endpoint, so the public site remains unchanged until the editor explicitly publishes.**

The CoreBunch/Instatic repository implements a draft-first workflow inside its Content workspace, letting editors iterate on posts, pages, and custom cells without risking the live site. By isolating unsaved changes in client-side state and merging them only inside a preview pipeline, Instatic ensures that live editing versus drafts remains strictly separated until publication.

## Draft State Lives in a Local React Hook

Instatic's content workspace centers on the **`useContentEntryDraft`** hook located in [`src/admin/pages/content/hooks/useContentEntryDraft.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/hooks/useContentEntryDraft.ts). This hook mirrors the selected `DataRow` fields—`title`, `slug`, `seoTitle`, `body`, and `customCells`—inside a set of `useState` values that serve as the editable draft copy.

When the user selects a new entry, the `applySelectedEntry` callback (lines 57–64) hydrates the hook's state using `read*Cell` helpers from [`src/core/data/cells.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/cells.ts). All subsequent edits mutate this local draft store instead of the database record, keeping the published version intact.

### Detecting Unsaved Changes with isDirty

The hook exposes an **`isDirty`** flag that compares the current hook values against the originally saved row (lines 86–97). This boolean disables the Save button when no changes exist and prevents accidental overwrites.

### Persisting Progress as a Draft

Editors persist their work by calling `handleSaveDraft`, which builds a payload and sends it to `saveCmsDataRowDraft` from [`src/core/persistence/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/index.ts) (lines 99–115). This writes the data with a `draft` status so it does not appear on the public route. Status changes—such as scheduling—route through `handleStatusChange` (lines 54–83), which saves the draft first before invoking `updateCmsDataRowStatus`.

The following example shows how a panel consumes the draft hook:

```tsx
import { useContentEntryDraft } from '@admin/pages/content/hooks/useContentEntryDraft'

function ContentEditor({ entry, onUpdate, onError }) {
  const {
    title, slug, body, isDirty,
    setTitle, setSlug, setBody,
    saveMessage, handleSaveDraft, handlePublish,
  } = useContentEntryDraft({
    selectedEntry: entry,
    updateSelectedEntry: onUpdate,
    setError: onError,
  })

  return (
    <>
      <input value={title} onChange={e => setTitle(e.target.value)} />
      <input value={slug} onChange={e => setSlug(e.target.value)} />
      <MarkdownEditor value={body} onChange={setBody} />
      <button disabled={!isDirty} onClick={handleSaveDraft}>
        {saveMessage === 'saving' ? 'Saving…' : 'Save Draft'}
      </button>
      <button onClick={handlePublish}>Publish</button>
    </>
  )
}

```

## Rendering a Live Preview Without Publishing

To let editors see their unsaved changes, Instatic provides a **draft-aware preview pipeline** that renders the entry on-the-fly without altering the live state.

### The Draft-Aware Preview Endpoint

The server handler in [`src/server/handlers/cms/data/preview.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/handlers/cms/data/preview.ts) reads any existing draft for the requested entry and merges it with the published version. It then returns the rendered HTML fragment, allowing the editor to inspect typography, layout, and custom cells exactly as they appear in draft form.

Below is a simplified view of that server-side flow:

```ts
import { readDraftIfExists, renderPage } from '@core/persistence'

export async function previewHandler(req) {
  const { entryId } = req.params
  const draft = await readDraftIfExists(entryId)   // pulls the draft row
  const html = await renderPage({ entryId, draft })
  return new Response(html, { headers: { 'Content-Type': 'text/html' } })
}

```

### The Write-Live Mode Toggle

The workspace header renders a **Write / Live** switch managed in [`src/admin/state/adminUi.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/adminUi.ts). When the workspace is in Live mode, the UI fetches the entry through the preview endpoint rather than the normal public route. This guarantees that the canvas reflects the latest draft while the actual public URL continues to serve the published version.

## Real-Time Bridge to the Live Editor

Instatic keeps the visual editor and the content form in sync through an **MCP bridge** registered in [`src/admin/pages/content/agent/useContentToolBridge.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/agent/useContentToolBridge.ts). The `registerContentToolBridge` function wires the workspace to the MCP live-edit channel and pushes the current draft store over the NDJSON stream used by the Site editor. Because the bridge always reads from the hook-managed draft state, the live canvas cannot diverge from the editor's current inputs.

## The Publish Workflow Step by Step

The complete separation between live editing and drafts follows a strict sequence:

1. **Select entry** — `applySelectedEntry` loads the row into the draft hook.
2. **Edit fields** — `setTitle`, `setBody`, and other setters update local state only.
3. **Live preview** — Toggling Live mode invokes the preview handler, which merges the draft into the render pipeline.
4. **Save draft** — `handleSaveDraft` calls `saveCmsDataRowDraft` to persist the draft to the database.
5. **Publish** — `handlePublish` (lines 32–53) first saves the draft, then calls `publishCmsDataRow` to copy the draft into the published status and update the UI.

Because `publishCmsDataRow` is the only action that promotes the draft to the public route, editors can iterate indefinitely without affecting the live site.

## Summary

- **[`useContentEntryDraft.ts`](https://github.com/CoreBunch/Instatic/blob/main/useContentEntryDraft.ts)** maintains isolated draft state in a React hook so edits never touch the published record until publication.
- The **`isDirty`** flag and `applySelectedEntry` callback provide reliable change detection and hydration.
- The **[`preview.ts`](https://github.com/CoreBunch/Instatic/blob/main/preview.ts)** endpoint merges drafts with published entries for on-the-fly previews without public exposure.
- The **Write / Live** toggle in [`adminUi.ts`](https://github.com/CoreBunch/Instatic/blob/main/adminUi.ts) routes the workspace between normal editing and draft-aware preview mode.
- The **[`useContentToolBridge.ts`](https://github.com/CoreBunch/Instatic/blob/main/useContentToolBridge.ts)** MCP bridge streams draft changes to the visual Site editor in real time.
- **Publishing** is explicit: `publishCmsDataRow` promotes the draft to live, while `saveCmsDataRowDraft` preserves it as work-in-progress.

## Frequently Asked Questions

### How does Instatic know if a content entry has unsaved changes?

The `useContentEntryDraft` hook calculates an `isDirty` boolean by comparing the current local state values against the saved `DataRow` fields (lines 86–97). If any field diverges, the flag becomes true and enables the Save Draft button.

### Can I see my draft changes without publishing them publicly?

Yes. The draft-aware preview handler in [`src/server/handlers/cms/data/preview.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/handlers/cms/data/preview.ts) merges your saved draft with the published entry and returns rendered HTML. When you switch the workspace to Live mode via the Write/Live toggle in [`src/admin/state/adminUi.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/state/adminUi.ts), you view this merged output without the draft ever reaching the public route.

### What happens to my draft when I click Publish?

The `handlePublish` callback in [`useContentEntryDraft.ts`](https://github.com/CoreBunch/Instatic/blob/main/useContentEntryDraft.ts) (lines 32–53) first persists the latest draft with `saveCmsDataRowDraft`, then calls `publishCmsDataRow` from [`src/core/persistence/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/index.ts) to copy the draft into the published status. After this succeeds, the live site serves the new content and the preview endpoint reflects the now-published version.

### Does the live Site editor receive updates while I type?

Yes. The MCP bridge in [`src/admin/pages/content/agent/useContentToolBridge.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/content/agent/useContentToolBridge.ts) registers an imperative tool surface that reads the draft store and streams updates over the NDJSON channel to the Site editor. This ensures the live canvas stays synchronized with your current draft state.