Visual editor
On Next.js and TanStack Start, @blokcms/next and @blokcms/tanstack-start do the site's part (tickets, drafts, live updates); npx @blokcms/cli init adds your dev server as a website. The steps below are for other frameworks.
Editors see your real website next to the form, update it live while they type, and click blocks to edit them.
- In Developers → Visual editor, add your site's preview URLs, each marked Local, Staging or Live (e.g.
http://localhost:4000,https://preview.example.com). Local ones only show for people with developer details on, since they only work on that computer. The editor opens the default website you pick there, else the first staging one; people can switch, and it remembers their choice. - Mark blocks: spread
editable(block)(React),v-editable(Vue) oruse:editable(Svelte) on each block's root element. - Use
useLiveEntry/liveEntryfor the page anduseLiveGlobals/liveGlobalsforgetGlobals(), so unsaved changes show up immediately. - Verify the editor ticket on your server and switch to draft content with a preview token:
import { verifyEditorTicket } from "@blokcms/client";
const editor = await verifyEditorTicket(request.url, process.env.BLOKCMS_PREVIEW_SECRET);
const client = editor
? blok.with({ token: PREVIEW_TOKEN, draft: !editor.release, release: editor.release ?? undefined })
: blok;
The preview secret (Developers → Visual editor) signs tickets that expire within an hour, so a leaked preview link stops working. The page runs inside an iframe: allow the admin origin in your frame-ancestors.
Globals
Globals (site settings, an announcement bar, the footer) have no page of their own: the editor previews them on a page you pick, the start page first. When your page reads them through useLiveGlobals (or useLiveEntry for a single getGlobal), they update while you type; otherwise the preview reloads after each save. Load globals in draft mode in the editor, like the page:
const [page, site] = await Promise.all([
client.getEntryByPath(path, { include: ["*"] }),
client.getGlobals(), // every published global; drafts with a preview token
]);
Not using a framework? Load https://docs.blokcms.app/bridge/v1.js and call BlokBridge.initBridge({ onUpdate }).
Sharing a preview
Editors can show a draft to people without a BlokCms account: Publish ▾ → Share a preview… makes a link that works for 1, 7 or 30 days, until it's withdrawn. Opening it sends the browser to the page on your preview URL, with blok_share=1 and a ticket for that page only: verifyEditorTicket accepts it on that path, in that language, and returns shared: true. Anywhere else on the site it returns null, so the link never shows other drafts.
Nothing else to build, as long as the preview URL is reachable from the internet (your production site works) and the site uses @blokcms/client 0.3 or later. Older versions don't accept share tickets and show the published page. Pass the URL as the browser requested it, base path included. To tell visitors what they're looking at:
{editor?.shared && <p role="status">Preview: this page isn't published yet.</p>}
Withdrawing a link stops it opening at once. A page someone already opened keeps its ticket for up to an hour; rotating the preview secret ends those too (links that weren't withdrawn keep working with the new secret).
Editing on the page
Clicking a block opens its fields, with the field you clicked focused. The selected block gets a toolbar: select its parent, move it, duplicate it, comment on it or delete it, and add a block before or after it with the + buttons. Drag its grip to move it, or drag a type from Add blocks onto the page; only places that take that type light up. Everything can be undone (⌘Z), also when someone else edits the same entry.
Two optional markers make this more precise. Without them, the editor finds the clicked field by its text.
<h1 {...editableField(block, "headline")}>{fields.headline}</h1> // the click focuses Headline
<div {...editableList(block, "items")}> // an empty list shows "Add card"
<Blocks blocks={fields.items} />
</div>
In Vue use v-bind="editableField(block, 'headline')"; in Svelte spread editableFieldAttrs(block, "headline") and editableListAttrs(block, "items"). Without a framework, set data-blok-field="<blockId>:<key>" and data-blok-list="<blockId>:<field>".
Clicks select blocks. To use accordions, tabs or forms in the preview, switch the toolbar to Interact, or hold Alt while clicking. Shortcuts work in the preview too: Esc selects the parent, Delete removes, ⌘D duplicates, Alt+↑/↓ moves, ⌘C/⌘V copy and paste blocks (also between entries), ⌘S saves.
Header and footer usually come from a global. Clicking them offers to edit that global on the same page. Report the roots of what you show by passing useLiveEntry/useLiveGlobals responses (they do this for you), or live: () => [{ id: entryId, root: content.id }] to initBridge.
Troubleshooting
- "The site isn't connected": check that the site runs at the preview URL, that the page starts the bridge (
useLiveEntryorinitBridge), and that a customadminOriginsincludes the admin's origin. - A blank or refused frame: the site sends
X-Frame-Optionsor aContent-Security-Policywithout the admin inframe-ancestors. Allowhttps://app.blokcms.appfor preview requests. - No toolbar on the selected block: the site runs an older
@blokcms/bridge. Update it; the hostedv1.jsis always current. - A share link shows the published page: the site runs a
@blokcms/clientolder than 0.3, or checks the ticket against a rewritten URL (a different path than the browser asked for). - Clicking does nothing: the element isn't inside an element with
editable(block). Mark each block's root element.