# Editing on your site (/docs/guides/site-editing)

The [canvas](/docs/guides/canvas) shows your site inside the dashboard. Editors can also work on the site itself: they open `/cms/login` on your site, sign in to Obelism Content, and are sent back to the page in **draft mode**. The page then shows drafts, and a prompt bar at the bottom lets them change it by typing ("make the hero heading shorter"), then **Save** or **Publish**.

```
yoursite.com/cms/login?return_to=/pricing
  → content.obelism.studio/site/connect?…      (signed out? → www.obelism.studio/login, and back; then Continue)
  → yoursite.com/cms/callback?code=…&state=…   (code → site token, draft mode on)
  → yoursite.com/pricing                       (drafts + prompt bar)
```

## Setting it up [#setting-it-up]

With the SDK it is one route and a few lines in the code that already reads content.

1. **Set the preview URL** of the environment (Settings) to your site. Sign-in only sends people back to that origin.

2. **Mount the handler** on `/cms/*`. In Next.js (App Router):

   ```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 }
   ```

   In React Router (framework mode) it is one resource route, and no draft mode: the site token's cookie is the switch, so there is nothing to turn on.

   ```ts title="app/routes/cms.tsx"
   import { createSiteEditing } from '@obelism/content/site'

   import type { Route } from './+types/cms'

   const handler = createSiteEditing({
   	spaceId: process.env.CONTENT_SPACE_ID!,
   	environment: 'production',
   })

   export const loader = ({ request }: Route.LoaderArgs) => handler(request)
   export const action = ({ request }: Route.ActionArgs) => handler(request)
   ```

   Add `route('cms/*', 'routes/cms.tsx')` to `app/routes.ts`: it ranks above the splat that serves your pages. Return its `Response` as it is: it sets several cookies, and rebuilding the response loses them. On Oxygen the two settings come from the Worker's `env` binding ([how](/docs/getting-started/oxygen#1-store-the-token-in-the-environment)).

   The handler takes a standard `Request` and returns a `Response`, so it works the same in any framework that has those.

3. **Read drafts** with the session's token, which works with the delivery API like a preview token. In Next.js, while draft mode is on:

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

   const token = (await draftMode()).isEnabled
   	? readSiteToken(await cookies())
   	: null
   const content = token
   	? new ContentClient({
   			token,
   			space: process.env.CONTENT_SPACE_ID!,
   			environment: process.env.CONTENT_ENVIRONMENT!,
   			init: { cache: 'no-store' },
   		})
   	: delivery
   ```

   In React Router there is no draft mode: read the token from the request's cookie on every request (`readSiteToken(request.headers.get('cookie'))`), and answer draft responses with `Cache-Control: private, no-store` and `X-Robots-Tag: noindex` yourself. [Previews and draft mode](/docs/guides/preview#react-router-no-draft-mode) has the function.

   Once the token stops working (sign-out, 8 hours, or the person leaving the organization), the API answers 401. Show the published page then. After 8 hours the token's cookie is gone while Next.js keeps draft mode on; see [Next.js draft mode](/docs/guides/preview#nextjs-draft-mode).

4. **Mark the page's entry** next to its blocks, so the prompt bar knows what it is editing:

   ```jsx
   <main {...editable(entry)}>
   	<section {...editable(block)}>…</section>
   </main>
   ```

   A block inside another marked entry (a navigation, say) is edited in that entry.

5. **Render live changes** with `useLiveEntry(entry)` (React) or `bridge.on('update', …)`, as for the canvas. Prompts change content in the browser's memory, and `update` shows it without saving. A page without an `update` listener shows the change once it is saved, after reloading.

`<ContentBridge />` (or `getContentBridge()`) starts the prompt bar on its own whenever the canvas isn't around and the editing cookie is set.

## What editors get [#what-editors-get]

- **Prompt**: as in the canvas, about the selected block (click one while the bar is open) or the whole entry, in the page's language (`<html lang>`, when the space has it). "Add a section on eligibility" adds a block, of the type whose [description](/docs/guides/content-model) fits best; with a block selected it goes right after it, and only that block can change. **Undo** steps back through the prompts.
- **Form**: switch the bar from Prompt to Form to edit the selected block, or the whole entry, with the same form as the dashboard and the canvas: every field, the asset and entry pickers, uploads, and blocks with **+ Add block**, in the page's language. Edits show on the page as you type and stay unsaved until **Save**; a prompt, an undo or another selection shows in the form. It replaces the JSON editor of 0.13 (`bridge.view()` and `bridge.applyJson()` remain). (`@obelism/content` 0.14 and up)
- **Pages**: the page menu lists every page, drafts included, and opens one, so a new draft that nothing links to yet can be reached. (`@obelism/content` 0.13 and up)
- **New page**: a path (`/pricing`, or `/solutions/agencies` under an existing page) and what the page is for. The model writes the whole page, with the content type of the page the editor is on and that page as an example of tone and blocks, and it is **stored as a draft right away**; the bar then opens it. On a page that isn't an entry, the bar only offers New page, and the space's page content type is used when it has just one.
- **Add a product page**: on a product route that marks its product with `editableProduct(product)` and has no [product page](/docs/guides/shopify#product-pages) yet, the bar offers this instead of a prompt. It makes a draft of the space's product page type for the product (titled after it, its handle as slug, under `products` when the space has that page or folder), and the page reloads to show it, ready for a prompt or the form. Product pages open from the page menu at your product route (`productPath`, default `/products/<handle>`). (`@obelism/content` 0.18 and up)
- **Add a collection page**: the same on a collection route that marks its collection with `editableCollection(collection)` and has no [collection page](/docs/guides/shopify#collection-pages) yet, under `collections`; collection pages open at `collectionPath` (default `/collections/<handle>`). (`@obelism/content` 0.20 and up)
- **Save** stores the changed content as the draft. If someone else saved the entry since it was loaded, the save is refused and nothing is lost: the changes stay in the browser.
- **Publish** saves first, then publishes every entry changed here (or the page's entry when nothing was).
- **Exit editing** signs out (`/cms/logout`) and turns draft mode off. Leaving the page with unsaved changes asks first.
- People who may only read content see drafts and an exit, and no prompt bar.

## How it works [#how-it-works]

- `/cms/login` sends the browser to `/site/connect` with the space, the environment, a `redirect_uri` (`/cms/callback` on your site), a random `state` and a PKCE challenge (S256). Signed out, that goes through the Obelism login on www.obelism.studio first. Signed in, Content checks the person may open the space and that `redirect_uri` is on the preview URL's origin, then shows them that origin, the space, the environment and their role. Only when they choose **Continue** does it redirect there with a one-time code, valid for a minute.
- `/cms/callback` checks the `state`, and trades the code and the PKCE verifier for a **site token** (`POST /api/site/v1/token`). It keeps the token in an httpOnly cookie (`obelism_content_token`), and sets a readable `obelism_content_editing` cookie that only tells the bridge to start.
- The site token acts as that person in that space environment, with their [role](/docs/guides/roles) at the time of each request, like a management token acts as its creator. It expires after 8 hours.
- The prompt bar calls `/cms/api/…` on your site. The handler forwards those calls to the [site API](/docs/api/site) with the token, so the token never reaches page scripts. Writes need an `x-obelism-content: 1` header, which a cross-site form can't send.
- **Form** is a page of Content (`/site/form` on content.obelism.studio) in an iframe in the bar. To open it, the handler asks the site API for a URL with a one-time code (`POST /cms/api/form` → `POST /api/site/v1/form`); the page trades the code for a **form token** of its own, kept in memory, which only works on the form's routes and ends with the site token (signing out, expiry). The page and the bar exchange the content by `postMessage`, the page only with your site's origin. References and shared blocks offer **Open**; since the dashboard refuses to be framed, that opens the entry in a new tab rather than in the panel. Content lets any site frame that page, so nothing on your side needs changing; if your site sets a `Content-Security-Policy`, its `frame-src` (or `child-src`/`default-src`) has to allow `https://content.obelism.studio`.
