Your workspace does not have a site and a staging mode. It has two complete websites. Same content, same templates, same code — built from one repository on two git branches, deployed as two separate workers, answering on two different hostnames.
The live site answers on your domain. The preview site answers on staging. plus your domain — so if your site is example.com, your preview site is staging.example.com. Both are real deployments with real certificates. The difference is who they are for, what reaches them, and a short list of things the preview one refuses to do.
The two sites side by side
| Live site | Preview site | |
|---|---|---|
| Address | your domain | staging. + your domain |
| Git branch | main | staging |
| What it is for | your visitors | reviewing the whole site before visitors get it |
| What builds it | publishing in the CMS, and promotion | file changes — CLI pushes, Layouts saves, managed-file refreshes — and an automatic rebuild after every live publish |
The Deploy screen mirrors this exactly: two stage cards at the top, two history columns underneath, one per site.
flowchart LR
subgraph PV["Preview site card"]
direction TB
P1["What the card explains:<br/>a private copy of the whole site"] --> P2["Visit preview site"]
P2 --> P3["Push preview live"]
end
subgraph LV["Live site card"]
direction TB
L1["The site your visitors see"] --> L2["Visit live site"]
end
PV -- "promotion" --> LV
Push preview live is the promotion control, and it is only offered when the preview site’s most recent deployment succeeded. If that build failed, the card replaces the button’s hint with a red notice and the control is unavailable until the error is resolved.
Which changes land where
The thing most people expect, and which is not true, is that you choose. There is no target selector anywhere in the CMS. Which site a change lands on is a fixed property of the kind of change, and it is the same for every workspace.
The rule underneath is simple once you see it: anything you edit as a record in the CMS goes straight to the live site. Anything that arrives as a file goes to preview first.
| Change | Lands on | Reason badge |
|---|---|---|
| Publishing a page, now or on a schedule | Live site | Pages |
| General site settings | Live site | Company Settings |
| A logo or favicon upload | Live site | Logo/Favicon |
| Team member profile details | Live site | User Profiles |
| Page types | Live site | Page Types |
| Labels | Live site | Labels |
| Autolinks | Live site | Autolinks |
| Forms | Live site | Forms |
| Menus | Live site | Menus |
| Search indexes | Live site | Search Indexes |
leed site push from the CLI | Preview site | Developer CLI |
| A managed-file refresh from Leed | Preview site | Managed Files |
| An administrative rebuild | Preview site | Admin Rebuild |
| The automatic catch-up after a live publish | Preview site | Published Files into Staging |
| Push preview live | Live site | Promoted Preview => Public |
Every deployment row is stamped with the badge in the right-hand column, so the history tells you what a build was for without expanding it — the full badge list is here.
A logo or favicon upload is the one row worth reading twice: the file is committed to your live branch the moment you upload it, but it does not start a build. It appears on your site at your next deployment, whatever that deployment happens to be for.
Template files never appear in the Pending changes list, because they are not edited in the CMS: they reach preview through a developer’s leed site push. A developer’s leed site push is the other way content reaches preview, traced step by step.
flowchart TB
CMS["CMS publish<br/>pages, settings, menus, labels, forms"] --> MAIN
PROMOTE["Push preview live"] --> MAIN
CLI["leed site push"] --> STG
MF["Managed-file refresh"] --> STG
subgraph REPO["One site repository"]
MAIN["main branch"]
STG["staging branch"]
end
MAIN -. "automatic rebase after every live publish" .-> STG
STG -. "merged on promotion" .-> MAIN
MAIN --> PW["Live site worker"] --> PROD(["example.com"])
STG --> SW["Preview site worker"] --> STAGE(["staging.example.com"])
Publishing live also rebuilds preview
Click Publish 3 to live site once and two rows appear — one under Live site, one under Preview site. That is not a bug and you have not published twice.
After a live publish, Leed rebases staging onto main and fires a parallel preview build so the preview site does not silently fall behind the live one. Without it, preview would be missing every CMS change you ever published, and reviewing it would tell you nothing. The catch-up row is badged Published Files into Staging and is initiated by the system rather than by you.
Two live builds skip it, both for the same reason — nothing has moved that preview does not already have:
- a promotion, because preview is by definition already in sync with what you just released;
- an administrative rebuild, because the live branch’s content did not change.
What the preview site does not do
The preview site is a full build of your site with four deliberate holes in it. All four are enforced at the edge, before your site’s code runs.
- Analytics events are swallowed. The tracking endpoint answers
204and records nothing. Traffic to your preview site never appears in your analytics, which is what you want — reviewing your own site all afternoon should not distort your numbers. - Form submissions are swallowed. A form on preview answers
204and creates no contact and no submission. The form will look like it worked, because the response is a success. - The Docs MCP and the site agent surfaces answer
404. The reader-facing MCP endpoint and the agent discovery documents are published-site features only. - Once your custom domain is connected through Domain Connect, preview’s temporary
workers.devaddress is switched off, leavingstaging.yourdomainas the only way in. If you set your DNS up by hand instead, that switch-off never fires and the temporary address keeps working.
Preview is private-by-convention, not by lock
The card on the Deploy screen calls the preview site “a private staging copy”. Read that as not advertised, not as protected.
Telling the two apart from the outside
Every response from either site carries two headers:
X-Preview—trueon the preview site,falseon the live site.X-Site-Version— the git commit the site was built from.
Together they answer both versions of “am I looking at what I think I’m looking at”: which site is this, and is this the build I just published? A request for the headers alone is the fastest check there is.
- Preview site
- Live site
curl -sI https://staging.example.com/ | grep -i '^x-'x-site-version: 9f3c1ab4e07d5b2a6c81f4d3e9027ab5c6d18e4f
x-preview: truecurl -sI https://example.com/ | grep -i '^x-'x-site-version: 4b7d02e91c5a3f86d2410b7e8c93a5f10d62b8ce
x-preview: falseIf X-Site-Version still shows the old commit after a deployment went Active, you are looking at a cached response rather than a stale build — request the URL again with a cache-busting query string before assuming the deployment failed.
When to promote
Preview is worth using when the change is one you cannot judge from a single page: a template edit, a navigation restructure, a styling change, anything a developer pushed. Look at the whole site on staging.yourdomain, click through it, and when you are satisfied, release it.
That release is a single button — Push preview live on the Preview site card. It moves everything on preview to live as one deployment; you cannot promote a subset. Promoting preview to live covers what it moves, who can press it, and every reason it is grayed out.
Before your own domain is connected, both sites answer on Leed-provided workers.dev addresses rather than example.com and staging.example.com, and the CMS’s Visit-site buttons point there. Setting the CNAME records yourself is the manual path; the one-click path is on the same page.