> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Publishing

> How a draft becomes the live site: the head check, sandboxed publish build, deployment statuses, unpublishing, restore, and rollback.

Publishing promotes the head version to the published version through a framework quality gate and a sandboxed build — the live site only ever changes when both succeed.

Published sites in Free organizations display a compact **Made with Cactal** badge linking to Cactal. Paid organizations have no badge on either Cactal or custom domains. The badge is added when serving the page, not stored in website source or deployment assets; billing updates and cache invalidation change it without republishing. Draft previews do not display the badge.

## The publish flow

<Steps>
  <Step title="Check the head version">
    `POST /v1/websiteSourceCode/head/check` runs the framework quality gate without changing anything: format, typecheck, lint, build, and a server-render of every page. It returns `{ "version": 12, "ok": true }` or `{ "version": 12, "ok": false, "message": "...", "details": { ... } }` describing what to fix.
  </Step>

  <Step title="Publish">
    `POST /v1/websiteSourceCode/head/publish` with `{ "websiteId": "...", "leaseToken": "..." }` validates the head version, builds it in a sandbox, and promotes it. Publishing requires an active [edit lease](/docs/concepts/source-code).
  </Step>

  <Step title="Wait for the deployment">
    The publish response includes the deployment. Its status is `building`, `ready`, or `failed`. You can poll `POST /v1/websiteSourceCode/deployment/status` with `{ "websiteId": "...", "target": "published" }` at any time.
  </Step>

  <Step title="Verify the live site">
    On success, `publishedVersion` moves to the published head version. Open the platform subdomain and confirm the change. See [Domains](/docs/concepts/domains).
  </Step>
</Steps>

<Check>
  Run `head/check` before every publish. It surfaces framework validation errors cheaply, without holding the live site's fate on a build.
</Check>

Both `head/check` and `head/publish` run real builds and use the expensive rate-limit class. See [Rate limits](/docs/api-reference/rate-limits).

## Unpublish a site

`POST /v1/websiteSourceCode/published/unpublish` with `{ "websiteId": "..." }` takes the site offline on its platform and custom domains. Cached pages may take a few minutes to disappear. It clears `publishedVersion` without changing head, deleting the built deployment, or rewriting version history. No edit lease or rebuild is required.

Unpublish is the fastest recovery when nothing should remain public, including when an agent published without approval. Publish head again whenever the site is ready to return.

## What the head check covers

The check goes past compilation. After formatting, typechecking, linting, and bundling, it server-renders every page in the build, including dynamic routes such as `/blog/:slug`, and reports the pages that threw.

Rendering needs page data, so the check synthesizes placeholder content from your CMS schema rather than reading CMS items. Each query a page declares becomes a sample item, or a one-item list, built from that collection's field definitions, and each route parameter gets a placeholder value. That has two consequences:

* The result depends only on your source and your CMS schema. Identical source checks identically every time, and a page never fails the gate because of one unusual CMS row.
* The check catches render-time failures such as browser-only globals like `window`, invalid hook usage, and libraries that do not render on the server. It does not catch failures that depend on specific content.

Pages that compile but throw while rendering come back in `renderFailures`, one entry per page:

```json theme={null}
{
  "version": 12,
  "ok": false,
  "message": "1 page compiled but threw while server-rendering.",
  "renderFailures": [
    {
      "pageId": "blog_post",
      "route": "/blog/:slug",
      "message": "Page \"blog_post\" threw an error while server-rendering.\n\nwindow is not defined"
    }
  ]
}
```

## Deployment statuses

The status response includes whether pages exist, the available `pageId` and `route` values, an opaque preview revision, and whether the draft is ready to publish.

| Status     | Meaning                              | What to do                                                                   |
| ---------- | ------------------------------------ | ---------------------------------------------------------------------------- |
| `building` | The sandboxed build is still running | Retry shortly; a publish that hits a still-building deployment returns `409` |
| `ready`    | The build succeeded and is servable  | Nothing — publish completes against it                                       |
| `failed`   | The build failed                     | Read `failureMessage`, fix the source, and publish again                     |

## What can go wrong

Publish is designed to fail safe: if any step fails, `publishedVersion` does not move and the live site keeps serving the previous published version.

| Failure                           | Response                                                    | Recovery                                         |
| --------------------------------- | ----------------------------------------------------------- | ------------------------------------------------ |
| Framework validation fails        | `400` with `kind: "validation"`, a message, and details     | Fix the reported files in head and publish again |
| Build still in progress           | `409` `Website deployment is still building; retry shortly` | Poll `deployment/status`, then retry             |
| Lease replaced by another session | `409` `Invalid edit lease`                                  | Acquire a new lease and retry                    |
| Head changed mid-publish          | `409` `Website source changed; reload and try again`        | Re-read the affected files, re-check, retry      |

<Note>
  Publish can rewrite head once: if the framework returns canonical formatting or normalized route literals, the formatted files are committed as a new head version before promotion. The response then includes `"message": "Source formatting was applied before publishing."` — your published version may be one higher than the head you started from.
</Note>

Drafting and publishing are free on every plan. A site counts toward your plan when an organization owner enables custom domains for it. Sites on Cactal domains are unlimited and free, and the platform domain keeps serving the published version without a paid plan.

## Recovery paths

Both recovery operations require an edit lease, and both preserve history. Versions are append-only, so nothing is ever overwritten.

### Restore head to a previous state

`POST /v1/websiteSourceCode/head/restore` copies an earlier state's files into a new head version without touching the live site. Pass `{ "to": "published" }` to discard draft work when the live site is the state you trust (`404` `Website has no published version` on a never-published website), or `{ "to": "38" }` for any historical version between `1` and the current `headVersion` (anything higher returns `400` `Version does not exist`). Publish afterward if you want it live.

### Roll back the published site

`POST /v1/websiteSourceCode/published/rollback-version` with `{ "version": 38 }` re-publishes a prior version directly: it validates and builds that version, commits it as the new head, and moves `publishedVersion` to it. This is the fast path when a bad publish is live. It runs the same quality gate and build as a normal publish, so it can fail the same ways.

<Warning>
  Rollback changes head too. It commits the rolled-back files as a new head version. Any unpublished draft on top of the bad publish is no longer head after a rollback. Recover the draft afterward with `head/restore` pointing at the draft's version number.
</Warning>

## Prepare a build

`POST /v1/websiteSourceCode/deployment/ensure` with `{ "target": "head" }` or `{ "target": "published" }` prepares that version without publishing it. Draft pages do this automatically. If a draft build fails, use the retry action shown on the draft page.

## Next steps

<Columns cols={2}>
  <Card title="Roll back and recover" icon="rotate-ccw" href="/docs/guides/roll-back-and-recover">
    Step-by-step recovery from bad publishes and failed builds.
  </Card>

  <Card title="Agent-safe publishing" icon="shield-check" href="/docs/agents/agent-safe-publishing">
    Draft-first workflows and approval checkpoints for agents.
  </Card>
</Columns>
