# Canvas (/docs/guides/canvas)

The **Canvas** (in the sidebar) shows every page of the environment you're editing on one board: your site, rendered at the environment's **preview URL** (Settings) in a frame per page, with the page's path appended (with `{locale}` in the preview URL, like `https://example.com/{locale}`, the locale chosen at the top goes there and the path is the one in that locale). A [product page](/docs/guides/shopify#product-pages) opens at the environment's product path instead (`/products/<handle>` by default).

Each page type is a column, with the home page's first. A column with more than six pages shows five of them and a sixth frame with a page switcher over it. **Edit on page** on an entry's page opens the board with that page selected and in view (`/canvas?entry=<id>`).

## Making a site editable [#making-a-site-editable]

1. **Include the bridge** on every page. With the [React SDK](/docs/sdk/react) that is `<ContentBridge />` in the root layout; with a bundler, `getContentBridge()` from `@obelism/content/bridge`; without one, a script tag:

   ```html
   <script src="https://content.obelism.studio/bridge.js"></script>
   ```

   It does nothing outside the canvas. Inside it, it only talks to the CMS origin it was loaded from. The script tag puts it on `window.ObelismBridge`. Options go on the tag: `data-prompt="false"` leaves the prompt box of [editing on your site](/docs/guides/site-editing) out, and `data-url` names the CMS when the file comes from an npm CDN.

2. **Mark blocks** with their uid, which every block in a delivery response has:

   ```jsx
   <section {...editable(block)}>…</section>
   // or, without the SDK:
   <section data-cms-uid={block._uid}>…</section>
   ```

3. **Show drafts in preview.** Every frame's URL carries `cms_preview=1`, `cms_entry=<id>`, `cms_environment=<name>` and `cms_token`: a token made for the person editing, each time the canvas opens. Read the page with `cms_token` as the bearer token, on your server, naming your space and environment (`space` and `environment` on `ContentClient`, the `Obelism-Space` and `Obelism-Environment` headers otherwise), and the delivery API answers drafts. Without them, a `cms_token` someone made for their own space would put that space's drafts on your site. It only reads, acts as that person (with their role at the time) and expires after 8 hours. A missing, made-up or expired token gets a `401`: show the published page then. [Previews and draft mode](/docs/guides/preview) has the code, and the rules that keep drafts off your public pages.

   <Callout type="warn">
   	Decide on drafts by `cms_token`, never by `cms_preview` alone with a
   	preview token of your own: anyone can add `cms_preview=1` to a URL.
   </Callout>

4. **Re-render on live updates** (optional, recommended): `useLiveEntry(entry)` in React, or

   ```js
   ObelismBridge.on('update', ({ entry }) => render(entry))
   ```

   `entry` has exactly the shape of a delivery API entry for a preview token, built from the unsaved draft. Without an `update` listener the page reloads after every save instead.

5. **Allow framing** by the CMS: if your site sends `Content-Security-Policy: frame-ancestors`, include `https://content.obelism.studio`, and drop `X-Frame-Options: DENY` for preview requests. The preview URL has to be reachable by the editor's browser too: a private host — Vercel deployment protection, Cloudflare Access, a private Oxygen environment — shows its login page in the frame instead of your site ([Shopify Oxygen](/docs/getting-started/oxygen#2-the-preview-environment-must-be-public)).

## Products [#products]

With a [Shopify store](/docs/guides/shopify) connected to the space, the canvas has a **Products** and a **Collections** column after the others. **Products** holds the [product pages](/docs/guides/shopify#product-pages) of every product page type and ends with **Any product**: your site's product route (the environment's product path, `/products/<handle>` by default) for a product of the store, the store's first to begin with. Select it and **Choose a product** to see another. **Collections** does the same for [collection pages](/docs/guides/shopify#collection-pages) and **Any collection**, at the collection path (`/collections/<handle>` by default).

For a product (and a collection the same way):

- A product with a page: **Show its page** selects that page's frame, where you edit it like any other page. Choosing a product that has one goes straight there.
- A product without one: **Add a product page** makes a draft page of the product page type for it, with the product filled in and its handle as slug, and opens it. The space needs a product page type for this (Settings → Shopify).

Products without a page are shown as visitors see them; there is nothing to edit on them in Content.

## Moving around [#moving-around]

The board fills the page beside the navigation, which shrinks to a rail of icons there. Each column is headed by its page type in the type's colour, with how many pages it has. At the bottom is the prompt bar, **Ask Obelism**. At the top left, the environment, a page type filter (**All page types**, or one), **Go to page** (any page of the board), **New page**, and **Save all** and **Publish all** while pages have changes. At the top right are the board's controls, and a pointer button whose tip sums them up:

- **Scroll** or **drag** to move the board, **⌘ or Ctrl and scroll** (or pinch on a trackpad) to zoom, or **−** and **+**. **Fit** shows the whole board. On the canvas, pinching zooms the board, not the browser.
- **Desktop** and **Mobile** render every frame 1440 or 390 pixels wide, so your site's own breakpoints apply. With more than one locale, the locale select shows every page in that one.
- Only the frames near what you're looking at load your site, at most eight at a time, each marked **Rendering** until it has loaded. The last dozen you've seen keep theirs when you move on; the others show an outline of a page until you move to them.

## Editing a page [#editing-a-page]

Click a page to select it. Its fields show in a panel floating on the right, in the same form as on the entry page, and clicks inside it now reach your site: clicking a block selects it. Hold **Space** to move the board over the selected page, and press **Esc** to clear the selection.

**Shift**-click (or **⌘**/**Ctrl**-click) adds pages to the selection or takes them out, and **⌘A** (**Ctrl A**) selects every page of the last clicked page's column. The prompt bar shows what it will change: the selected pages as chips, which you can take out one by one or with **Clear selection**. A store column's last frame (**Any product**, **Any collection**) has no page of its own, so the bar asks you to select a page while it's selected.

- Edits show on the page as you type, before they are saved.
- The prompt bar acts on the selected page, or the selected block (a chip of its own): type what to change and it changes it [by prompt](/docs/guides/prompt-editing). The change shows on the page at once, unsaved, and **Suggested by Obelism** above the bar says what changed: **Keep it**, **Try another** (undo it and ask again) or **Undo**, with the first text it changed before and after. Until you decide, the changed pages carry a **Preview** chip.
- With several pages selected, each gets the same prompt, one after another; the panel lists them as they land.
- **New page** asks for a path and what the page should say, and the model writes it as a draft of the selected page's type (with no page selected, the first page's of the board), which then opens selected (save or undo other changes first).
- With a block selected, **Entry fields** at the top of the panel goes back to the page's own fields.
- Every page keeps its unsaved changes while you edit others; a page with changes is marked **Unsaved**. The panel's **Save** saves the selected page and **Publish** saves and publishes it; **Save all** and **Publish all** do the same for every changed page, each with its own check that nobody saved it in the meantime. A page that can't be saved or published is named. Closing the tab with unsaved changes asks first.
- Where publishing is reviewed and you may not publish, **Publish** and **Publish all** ask for review instead.

## Variants side by side [#variants-side-by-side]

A page with an [A/B test or flag block](/docs/guides/ab-tests) lists its tests and flags under **Variants** in the panel. **Side by side** adds a column after the others with a frame per option of that test or flag, each showing the page as it is with that option's content, while you edit the page in its own frame. **Hide** takes the column away again.

Each variant frame gets the page's unsaved content with every block of that test or flag holding only its option's blocks, so whatever option your site picks for the visitor, it can only render that one. This needs a site that re-renders on live updates (`useLiveEntry`, or `ObelismBridge.on('update', …)`); a site that only reloads after saving shows its own pick in every frame.

## Changing the whole site [#changing-the-whole-site]

With no page selected, the prompt bar takes a request about the **whole site**, several pages at once: "make every hero shorter", "end each journal post with a call to action". It works in two steps:

1. A plan: the model reads an outline of the pages on the board (their names, paths, types and the start of each block) and answers which of them the request is about, and what to ask about each, at most 12.
2. Every step then runs as that page's own [prompt](/docs/guides/prompt-editing), one after another, with the same checks as a prompt on one page.

The panel lists the steps as they land; click one to go to its page. The changes are unsaved, marked on their frames: look them over, **Undo** them all from the suggestion above the bar, and **Save all** or **Publish all** when they're right. A request reaches the pages on the board, so in a column with a switcher only the page it shows.

## Whole pages [#whole-pages]

A frame is as tall as its page. The bridge tells the canvas the page's height, and the canvas tells the bridge the screen height the frame stands for (900 pixels on desktop, 844 on mobile). The bridge then rewrites `vh`, `svh`, `lvh` and `dvh` sizes in your stylesheets as pixels of that height, so a `100vh` hero stays one screen high instead of growing with the frame. Stylesheets from another origin can't be read and keep their units. This needs `@obelism/content` 0.20 or later (or Content's own `/bridge.js`); with an older bridge, frames are one screen high and scroll inside.

## Click rates [#click-rates]

With **Show click rates on the page** in the panel, the selected page shows how often each of its blocks is clicked, as a label on each block. The switch shows once the page has click rates. The switch is remembered in your browser. The numbers come from [Obelism numbers](https://numbers.obelism.studio), the events service every Obelism product shares:

1. Load numbers' script on every page of the site (its Setup page has the tag for your Organization and environment).
2. Tag a block's clickable element with its uid, in production too:

   ```tsx
   <a href={linkHref(block.link) ?? undefined} data-numbers-id={block._uid}>
   	{block.label}
   </a>
   ```

   The script sends a `click` event with that uid for every click on (or inside) the element.

A block's click rate is the share of the visitors who viewed a page it was clicked on who clicked it, over the last 30 days, in the environment you're editing. Blocks nobody clicked show nothing.

## When a frame shows published content [#when-a-frame-shows-published-content]

A frame that shows the published page, or no page, instead of the draft almost always comes from the `401` fallback in step 3 of [making a site editable](#making-a-site-editable). Log the response's body before falling back (`error.body` on the SDK's `ContentApiError`): it says which of these it is.

- **The entry was never published.** The fallback has nothing to show, so the frame shows the page as if it had no entry: on a product route, the product without its blocks.
- **`cms_token` doesn't reach the client**, and the error is `Invalid or revoked token`. Check what your server actually reads, especially after a rewrite or a redirect. A Next.js rewrite that captures the query with `has` and a bare `key` names the value by its letters only (`cms_token` is `:cmstoken` in the destination). Name it with a group instead:

  ```ts title="next.config.ts"
  {
  	source: '/products/:handle',
  	has: [{ type: 'query', key: 'cms_token', value: '(?<cmsToken>.+)' }],
  	destination: '/preview/products/:handle?cms_token=:cmsToken',
  }
  ```

- **The space or environment doesn't match** (`token_place_mismatch`). The token is for the environment you're editing, and the site names its own (`CONTENT_SPACE_ID`, `CONTENT_ENVIRONMENT`). Point each environment's preview URL at a deployment configured for that environment. With a [connected deployment](/docs/guides/api-tokens#without-a-stored-token), that is the host environment whose rule leads there, such as a preview deployment for staging.

## Bridge reference [#bridge-reference]

`window.ObelismBridge`, or what `getContentBridge()` returns:

- `isEditing`: true inside the canvas.
- `entryId`: the entry being edited.
- `on(event, listener)`: `update` (`{ entry }`), `saved`, `published`, and `change` for the prompt state.
- `prompt(text)`, `undo()`, `createPage(path, prompt)`: when [editing on your site](/docs/guides/site-editing); see [prompt editing](/docs/guides/prompt-editing).

The bridge and the canvas talk over `postMessage`. The site sends `{ source: 'obelism-content-bridge', type }` with `ready` (with the page's `height`), `size` (`{ height }`, whenever it changes) and `select`; the canvas sends `{ source: 'obelism-content-editor', type }` with `connect`, `viewport` (`{ height }`, the screen height the frame stands for), `update`, `select`, `saved`, `published` or `stats`. `stats` carries the blocks' click rates and the switch, `{ stats: { [uid]: { clicks, clickers, viewers, rate } }, visible }`, which the bridge shows on the page next to each `data-cms-uid` while `visible` is true (`@obelism/content` 0.7 and up; see the SDK's README, "Click rates"). Both sides check the other's origin.

Editors can also work on the site itself, outside the dashboard: see [editing on your site](/docs/guides/site-editing).
