# Previews and draft mode (/docs/guides/preview)

A delivery token reads what is published. Everything else a site shows of a draft goes through a token that reads drafts, and there are four ways one reaches your server:

| Way                                                                    | Token                                      | Reaches your server as                          | Lasts                          | For                                                      |
| ---------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------- | ------------------------------ | -------------------------------------------------------- |
| [Canvas](/docs/guides/canvas)                                          | Canvas, `cms_v_…`                          | `cms_token` in the query of every frame's URL   | 8 hours                        | Editors working in the dashboard                         |
| [Editing on your site](/docs/guides/site-editing)                      | Site, `cms_s_…`                            | The httpOnly cookie `obelism_content_token`     | 8 hours, or until sign-out     | Editors working on the site itself, with the prompt bar  |
| [Preview token](/docs/guides/api-tokens)                               | Preview, `cms_p_…`                         | Your server's environment variable              | Until it expires or is revoked | A staging or preview deployment that always shows drafts |
| [Connected deployment](/docs/guides/api-tokens#without-a-stored-token) | The host's OIDC token, with `drafts: true` | Asked from Vercel or GitHub Actions per request | Per request                    | The same, without a token to store                       |

The first two act as the person editing, with their [role](/docs/guides/roles) at the time of each request: they stop working when that person leaves the organization. The last two show drafts to anyone who can open the deployment, so put that deployment behind your host's protection (Vercel's deployment protection, Cloudflare Access, basic auth).

Oxygen issues no OIDC token, so a storefront on [Shopify Oxygen](/docs/getting-started/oxygen) uses a stored token: a delivery token for its public pages, a preview token for a deployment that always shows drafts.

All four are refused by the delivery API from a browser on any origin but the environment's **preview URL**: read drafts on your server.

## One client per request [#one-client-per-request]

Decide per request which token reads the page, in this order: the canvas's `cms_token`, then a signed-in editor's site token, then your delivery token. Everything a page reads (the page, its navigation, its settings) goes through that one client, so a draft page doesn't sit inside a published navigation.

```ts title="lib/content.ts"
import { ContentClient, readPreviewParams } from '@obelism/content'
import { readSiteToken } from '@obelism/content/site'

export const delivery = new ContentClient({
	token: process.env.CONTENT_DELIVERY_TOKEN!,
})

// Naming your space and environment makes a token someone made for another
// space read nothing here.
const drafts = (token: string) =>
	new ContentClient({
		token,
		space: process.env.CONTENT_SPACE_ID!,
		environment: process.env.CONTENT_ENVIRONMENT!,
		init: { cache: 'no-store' },
	})

export const contentFor = (request: Request) => {
	const preview = readPreviewParams(new URL(request.url))
	if (preview?.token) return drafts(preview.token)
	const siteToken = readSiteToken(request.headers.get('cookie'))
	if (siteToken) return drafts(siteToken)
	return delivery
}
```

In Next.js the site token is read in [draft mode](#nextjs-draft-mode), and the canvas's token from `searchParams`; the [Next.js quick start](/docs/getting-started/nextjs) has the same function written that way.

### When the token is refused [#when-the-token-is-refused]

A canvas or site token that expired, was revoked, belongs to another space, or was made up gets a `401`. Show the published page then, and log why:

```ts
import { ContentApiError } from '@obelism/content'

const content = contentFor(request)
const page = await content.getPage(path).catch((error: unknown) => {
	if (error instanceof ContentApiError && error.status === 401) {
		console.warn('Content refused the draft token:', error.body)
		return delivery.getPage(path)
	}
	throw error
})
```

`error.body` says why: `token_place_mismatch` as its `code` for a token of another space or environment, otherwise a message such as `Invalid or expired token` (see the delivery API's [errors](/docs/api/delivery#errors)). An editor who sees published content in the canvas, or on the site after a working day, has a token that ran out: opening the canvas again, or `/cms/login`, makes a new one.

## Rules for drafts [#rules-for-drafts]

1. **Decide by the token, never by `cms_preview`.** The canvas adds `cms_preview=1`, `cms_entry` and `cms_environment` to its URLs, but anyone can type those into a browser. Read drafts only with the token the request brings (`cms_token`, the site cookie), never with a preview token of your own because a parameter is there. `readPreviewParams` answers `{ entryId, environment, token }`; use `token`.
2. **Name your space and environment** on every client that reads with a token from the request (`space` and `environment` on `ContentClient`, the `Obelism-Space` and `Obelism-Environment` headers without the SDK). Anyone with a space of their own can make a canvas token for it. Without these, such a token would put their drafts on your site; with them, it gets `401` with `token_place_mismatch`.
3. **Keep draft-reading tokens on your server.** Preview tokens never go in client bundles or public environment variables (`NEXT_PUBLIC_…`, `PUBLIC_…`, `VITE_…`). The site token lives in an httpOnly cookie for this reason, and the prompt bar reaches Content through your server's `/cms/api/…`.
4. **Never cache a draft.** Fetch drafts with `cache: 'no-store'` (as above), and keep the page itself out of shared caches: a site token arrives on the same URL as the public page, so a CDN that caches by URL would hand an editor's draft to the next visitor. Answer draft requests with `Cache-Control: private, no-store`. Next.js does this itself in draft mode; elsewhere, set it wherever you pick the drafts client.
5. **Keep drafts out of search engines and analytics.** Send `X-Robots-Tag: noindex` with draft responses. The canvas's token is in the frame's URL, and analytics tools record full URLs: leave your analytics out when `readPreviewParams` answers, so neither the token nor editors' visits reach them.

## Live updates [#live-updates]

A token shows what is **saved**. The canvas and the prompt bar also send what an editor types before it is saved, through the [bridge](/docs/sdk/javascript#bridge):

- React: `useLiveEntry(entry)` returns the unsaved version while editing, and `entry` everywhere else. See [the React SDK](/docs/sdk/react).
- Anything else: `bridge.on('update', ({ entry }) => render(entry))`, with `entry` in exactly the shape the delivery API answers for a draft token.

Without an `update` listener, the page reloads after every save, which shows the saved draft through the token.

## A preview deployment [#a-preview-deployment]

A staging site, or a preview deployment per pull request, can show drafts to everyone who opens it, with no sign-in: give that deployment a **preview token** instead of a delivery token, or, on Vercel or GitHub Actions, a [connected deployment](/docs/guides/api-tokens#without-a-stored-token) whose rule reads drafts:

```ts
import { ContentClient } from '@obelism/content'
import { vercelOidc } from '@obelism/content/oidc'

export const delivery = new ContentClient({
	token: vercelOidc(),
	space: process.env.CONTENT_SPACE_ID,
	environment: process.env.CONTENT_ENVIRONMENT,
	drafts: process.env.VERCEL_ENV === 'preview',
	init: { cache: 'no-store' },
})
```

- Point the deployment at the environment it shows. A staging deployment reads staging, with staging's token: tokens belong to one environment.
- Draft responses are never cached by Content, so such a deployment renders every page on request. Turn its own caches off, as for any draft (`cache: 'no-store'`).
- Protect it. It shows everything that isn't published yet to anyone with the URL.
- It doesn't replace the canvas: it shows saved drafts, not what an editor is typing, and has no prompt bar. Set the environment's **preview URL** to it, include the bridge, and editors get both.

## Next.js draft mode [#nextjs-draft-mode]

[Draft mode](https://nextjs.org/docs/app/guides/draft-mode) is Next's switch for one browser to skip the caches: while its cookie is set, `fetch` and `'use cache'` skip the cache, static pages render on request, and responses are sent with `Cache-Control: private, no-cache, no-store`. Editing on the site turns it on at sign-in and off at sign-out:

```ts title="app/cms/[...path]/route.ts"
import { createSiteEditing } from '@obelism/content/site'
import { draftMode } from 'next/headers'

const handler = createSiteEditing({
	spaceId: process.env.CONTENT_SPACE_ID!,
	environment: 'production',
	onSignIn: async () => (await draftMode()).enable(),
	onSignOut: async () => (await draftMode()).disable(),
})

export { handler as GET, handler as POST }
```

Read the site token only while draft mode is on, so published pages never look at cookies:

```ts
import { readSiteToken } from '@obelism/content/site'
import { cookies, draftMode } from 'next/headers'

export const getContent = async () => {
	if ((await draftMode()).isEnabled) {
		const token = readSiteToken(await cookies())
		if (token) return drafts(token)
	}
	return delivery
}
```

Next can't read cookies inside `'use cache'`, so call this outside any cached function and pass the client in.

Draft mode and the site token end together on sign-out, but not otherwise: the token's cookie expires with the token after 8 hours, while Next's draft mode cookie lasts until the browser closes. The page then shows published content, rendered per request, until the editor signs in again or leaves through `/cms/logout`. A token revoked before then (the person left the organization) gets a `401`, and the fallback above shows published content too.

An exit link of your own goes to `/cms/logout` as a plain `<a>` or a form, or with `prefetch={false}` on `next/link`: Next prefetches links, and a prefetch of `/cms/logout` would end the session before anyone clicks.

The canvas doesn't use draft mode: its token arrives on every frame's URL, so a page that reads it from `searchParams` works without cookies inside the dashboard's frame.

## React Router: no draft mode [#react-router-no-draft-mode]

React Router has no switch to flip. The site token's cookie is the switch: read it from the request on every request (the `contentFor` function above), fetch drafts with `cache: 'no-store'`, and answer those responses with `Cache-Control: private, no-store` and `X-Robots-Tag: noindex` yourself. Keep them out of your own loader caches and out of Oxygen's full-page cache: the token arrives on the same URL as the public page.

The cookie expires after 8 hours and on sign-out, and a refused token gets a `401`, so the fallback above shows the published page; there is nothing else to turn off. The exit link goes to `/cms/logout` as a plain `<a>`, or with `prefetch="none"`: a prefetch would end the session before anyone clicks.

[Shopify Oxygen](/docs/getting-started/oxygen) has the host's side: a public environment for the canvas, and the headers the editor needs.

## Troubleshooting [#troubleshooting]

- **The canvas shows the published page.** See [when a frame shows published content](/docs/guides/canvas#when-a-frame-shows-published-content).
- **The canvas shows a Shopify login.** The environment is private, and the frame is loaded by the editor's browser, which isn't signed in to your store. Make that environment public, or point the preview URL at one that is ([Shopify Oxygen](/docs/getting-started/oxygen#2-the-preview-environment-must-be-public)).
- **Signed in on the site, but the page is published content.** Your page doesn't read the site token: check that the code above runs on the route (in Next.js, that draft mode is on and the page reads `cookies()` only then), and that your CDN didn't answer from its cache (rule 4).
- **`403` reading drafts.** A draft token from a browser on another origin than the preview URL: read on the server, or set the preview URL to the origin you are on. With a connected deployment and `code: drafts_not_allowed`, its connection's rule reads published content only: allow drafts in that rule, or leave `drafts` off.
- **`401` with `token_place_mismatch`.** The token is for another space or environment than your site names. Check `CONTENT_SPACE_ID` and `CONTENT_ENVIRONMENT` against the environment whose preview URL points here.
- **Drafts show to visitors.** A preview token in a deployment the public reaches, or a draft response cached under the public URL. Swap the token for a delivery token and purge that cache.
