Content lifecycle: draft / published
Content has its own draft/published state at the record level, independent of both code and schema. Editors publish content without a deploy and without an engineer.
Record state
Records on a draft-enabled model start as draft. Required-field and uniqueness
validation is deferred until you publish. set_publish_status moves a record
between draft and published (with bulk variants); publishing snapshots the record,
unpublishing clears the snapshot.
- Draft — editable, visible only in preview.
- Published — frozen into a published snapshot the live site reads from.
How draft vs published is decided
A single boolean, includeDrafts, is threaded from the render path down to the
query. Crucially, it's bound to the runtime, not to the request — a visitor
can't opt into drafts:
| Render path | includeDrafts | Reads |
|---|---|---|
servePublished (live site) | false | published snapshots only |
serveDraft (preview cookie) | true | live draft columns |
So the published site can never leak an unpublished record, and a site-code preview shows drafts automatically — the same URL, the same runtime, just the draft flag flipped.
Content changes need no publish_site. The live site re-reads content every request through its
GraphQL capability; a newly published record is live on the next request. Code deploys and content
publishing are entirely separate events.
Previewing unpublished content
To review draft content against a running site, use a preview_site URL — the
draft cookie selects the draft render path, which reads drafts. That shows all
draft records in context.
The CMS also mints its own per-record preview tokens (get_preview_url). Those are not currently
wired into the Loftur render path — draft content preview goes through the site's preview_site
cookie, not the per-record token. Reach for preview_site when you want to see drafts.
Assets and localization
The same content tools cover assets (upload, import from URL, replace) and localized fields via built-in locales — all editable content, all on the content loop, none of it requiring a code publish.