Docs

This page isn't translated yet

next

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. customBlocks and customTextStates are 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.ts and forgot in clientSetup is named in the browser console instead of rendering as unknown 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 clientSetup entry 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, and RichText imported 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's load event 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/_inCard markers. 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/preview enables 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. customBlocks are registered through the plugin option, which only runs on the server, so a client-mode preview renders them against an empty registry — each one logs found <slug> block, but no converter is provided and 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.