Guides · Editing

Editing on your site

Editors sign in on your own site at /cms/login, see drafts in draft mode and change the page by prompt, then save or publish. One route handler with the SDK.

7 min read

The 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

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):

    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.

    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).

    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:

    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 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.

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

    <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

  • 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 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 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 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

  • /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 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 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.

On this page

Marks in the margin

  • Note
  • Watch out
  • Updated

Scribes marked doubtful lines in the margin. That practice is called obelism.

Stuck on a step?