The lifecycle: draft to live
Every node moves through a small, predictable lifecycle. It has very few states and very few transitions, on purpose. Workflow states — “scheduled”, “in review”, “archived” — create complexity that most sites don’t need and every site pays for. CTXR leaves them out.
Two states that matter
A node is draft-only, live, or both at once:
| State | What exists | Visitors see | Editable |
|---|---|---|---|
| Draft only | A draft | Nothing | Yes |
| Live | A live record | The live record | Make a draft first |
| Live + draft | Both | The live record; the draft is hidden | The draft |
That’s the whole model. While an editor works on a draft, the live version is completely untouched — visitors keep seeing published content right up until the moment of approval.
The transitions
create → a draft exists
save → a draft property changes
approve → the draft is compiled into the live graph
discard → the draft is dropped, live is unchanged
unpublish → the live record is removed, reverts to draft-only
delete → everything is removed
Approve is the one that does the heavy lifting. It’s the moment the compiler runs: the draft becomes the source, every reference is resolved, derived values are calculated, the result is written as the live record, the draft is cleared, and the change cascades to everything that referenced this node. One click; the platform handles the rest.
The others are intentionally cheap. Save touches only the draft. Discard reverts a mistake in milliseconds without rebuilding anything. Unpublish removes a node from the public site but keeps it as a draft, cascading so that pages which included it are rebuilt without it. Delete removes everything; references to a deleted node simply resolve to empty, which the compiler handles gracefully.
Viewing is not the same as publishing
There are two different things an editor does, and keeping them separate keeps the mental model clean.
Approving changes state: a draft becomes live. It’s an action with consequences — the compile, the cascade, the public site updating.
Viewing as changes nothing. Adding ?live or ?draft to a URL just selects which version of the same node you’re looking at. It’s a lens, not a lever. “Live” and “draft” aren’t two kinds of page; they’re two projections of one node, which is why switching between them is instant and harmless.
Why this matters for templates
Because there’s one node behind both lenses, there’s one template behind both audiences. Templates don’t have separate read and edit modes.
For visitors, a template receives the compiled live graph — pre-resolved, complete, ready to render. For editors, the same template receives the draft merged with live data, plus a few runtime flags: _isEditor (this viewer may edit), _hasDraft (an unpublished draft exists), and _diff (which properties differ between draft and live). The editing affordances — the highlight on a changed title, the inline controls — activate from those flags, not from a different URL. A template can read the diff directly to drive its own indicators:
$diff = $data['_diff'] ?? [];
$nameChanged = array_key_exists('name', $diff); // true when the draft title differs from live
These keys describe the current request only; they’re never stored on the node (see nodes & types).
So a page is written once and serves everyone. The editor experience isn’t a separate application bolted on; it’s the same page, reading the same data, told who’s looking.