Client mode
When to use client mode and its caveats.
On this page
In client mode the preview iframe re-renders in the browser from the editor's unsaved form state, without a draft cookie or a save.
When to use clientLink to this section
client mode needs no draft-mode cookie at all, so it live-updates on a locally built site in every browser and across a cross-origin admin. It also feels snappier (updates as you type). Switch to it for a cross-origin local build, or simply if you prefer real-time preview:
withSysthema(payloadConfig, { livePreview: { mode: 'client' } })client mode caveatsLink to this section
-
Register custom blocks and text states with
livePreview.clientSetup.customBlocksandcustomTextStatesare registered during server-side config resolution, so in client mode a project's custom blocks render as nothing and its text states render unstyled — while the published page stays correct. Importing a registration module into your own views is not enough: Systhema's built-in views (DefaultPageClientView,DefaultPostClientView,DefaultArchiveClientView) cannot import consumer code, so any page on a built-in template would still be wrong.Point the option at a
'use client'module whose module scope does the registering, and Systhema wraps every preview with it:src/payload/previewClientSetup.tsx 'use client' import { registerSysthemaClientBlocks, registerSysthemaClientTextStates, } from '@systhemaui/payload/next/client' import { MyBlock, myBlockConverter } from './blocks/MyBlock' import { customTextStates } from './textStates' registerSysthemaClientBlocks([{ type: 'block', data: MyBlock, converter: myBlockConverter }]) registerSysthemaClientTextStates(customTextStates) export default function PreviewClientSetup({ children }: { children: React.ReactNode }) { return <>{children}</> }withSysthema(payloadConfig, { livePreview: { mode: 'client', clientSetup: PreviewClientSetup }, })It wraps rather than sitting beside the view deliberately: the registries are plain non-reactive objects, so a registration landing after the first render would never trigger a re-render. Wrapping makes the ordering structural instead of a race. In development, an unregistered block or text state now warns in the browser console rather than failing silently.
In development the two lists are also diffed by slug, so a block you added to
payload.config.tsand forgot inclientSetupis named in the browser console instead of rendering asunknown node:[systhema] Custom block "gradientSection" is registered in payload.config.ts but not in livePreview.clientSetup, so it renders as "unknown node" in client-mode preview. Add it to the registerSysthemaClientBlocks call.The reverse case warns too — a
clientSetupentry the server no longer defines (a renamed or removed block) will never render anything. Only slugs cross the boundary, only in development: a production build ships and checks nothing.Your converter modules run in the browser in this mode, so they must be client-safe — no
fs, no server-only imports, andRichTextimported from@systhemaui/payload/next/client. -
Material Symbols and Apple emoji icons are fetched, not bundled. Both packs store an identifier and resolve their markup from a digest only the server loads, so a browser-rendered tree would paint them empty. Client-mode preview batches the ids it could not resolve into one request to
GET <routes.api>/systhema/icons/resolve— including icons the editor picks mid-session — and re-renders. Nothing to configure; see Icons → Rendering icons in the browser. -
Videos are downgraded to
preload="none"inside the preview so a media-heavy page can't hold the iframe'sloadevent open and leave Payload's skeleton up. Published pages keep whatever each block configured. -
The live document is raw editor form state, so server-injected values are absent until you save — most visibly the
_tier/_inCardmarkers. A block you have just inserted or moved can render untiered in the overlay and correctly after publishing. -
Client mode still needs draft mode for the initial document.
/sys/previewenables it regardless of mode; only the update channel is cookie-free. Where the draft cookie cannot be set at all, a never-published document resolves to a 404 in the frame. -
Custom blocks need their converters handed to the browser.
customBlocksare registered through the plugin option, which only runs on the server, so a client-mode preview renders them against an empty registry — each one logsfound <slug> block, but no converter is providedand renders nothing, while the published page stays correct. Register them from a'use client'module your view imports:'use client' import { registerSysthemaClientBlocks } from '@systhemaui/payload/next/client' import { MyBlock, myBlockConverter } from '@/payload/blocks/MyBlock' registerSysthemaClientBlocks([{ type: 'block', data: MyBlock, converter: myBlockConverter }])It is a no-op on the server, so it can never clobber the plugin's own registration.
-
In the preview only, Lexical-nested form blocks, Google Maps embeds, and homepage link resolution rely on server context and fall back gracefully. Published pages render all of these normally.
-
Autosave still runs (draft persistence) but no longer drives preview latency.