Docs

This page isn't translated yet

Build a custom block

Implement a Company Note block from its Payload schema through rendering and live preview.

On this page

GoalLink to this section

Add a Company Note band for an editorial note that needs its own fixed presentation. Use a native Section for ordinary content; this example teaches the custom-block registration path.

1. Keep schema and renderer togetherLink to this section

Block folder
src/blocks/CompanyNote/
  schema.ts
  converter.tsx
  index.ts
  component.tsx

The public schema entry is index.ts and the renderer is component.tsx. Keep the server schema separate from the browser-safe converter so live preview can import the converter without the editor factories or Payload config.

2. Define the schemaLink to this section

Use the CompanyNoteSchema from Content model. Its slug is companyNote, with a required heading and a region-level content field. Keep the slug stable after editors save content using it.

3. Write the presentationLink to this section

src/blocks/CompanyNote/component.tsx
import type { ReactNode } from 'react'
import { Section, Heading } from '@systhemaui/next'

export function CompanyNote({ heading, children }: { heading: string; children: ReactNode }) {
  return (
    <Section theme="dark" layoutBackground="main">
      <Heading.h2>{heading}</Heading.h2>
      {children}
    </Section>
  )
}

The renderer returns one Section. The page template already supplies main and Article.

4. Convert the saved fieldsLink to this section

src/blocks/CompanyNote/converter.tsx
import type { CustomBlockConverter } from '@systhemaui/payload'
import { CompanyNote } from './component'

export const companyNoteConverter: CustomBlockConverter = ({ node, nodesToJSX }) => {
  const fields = node.fields as {
    heading?: string
    content?: { root?: { children?: unknown[] } }
  }
  if (!fields.heading) return null

  return (
    <CompanyNote heading={fields.heading}>
      {fields.content?.root?.children ? nodesToJSX({ nodes: fields.content.root.children }) : null}
    </CompanyNote>
  )
}

Missing fields are normal while an editor types. Recurse through nodesToJSX for the nested rich text; do not stringify it or insert stored HTML.

src/blocks/CompanyNote/index.ts
import type { CustomBlock } from '@systhemaui/payload'
import { CompanyNoteSchema } from './schema'
import { companyNoteConverter } from './converter'

export const companyNoteBlock: CustomBlock = {
  type: 'block',
  data: CompanyNoteSchema,
  editor: 'root',
  converter: companyNoteConverter,
}

5. Register server rendering and browser previewLink to this section

Import companyNoteBlock into src/payload.config.ts and add customBlocks: [companyNoteBlock] to its existing userSysthemaConfig. Preserve all the other options.

For the starter's client preview, add:

src/payload/previewClientSetup.tsx
'use client'

import type { ReactNode } from 'react'
import { registerSysthemaClientBlocks } from '@systhemaui/payload/next/client'
import { companyNoteConverter } from '../blocks/CompanyNote/converter'

registerSysthemaClientBlocks([
  {
    type: 'block',
    data: { slug: 'companyNote', fields: [] },
    editor: 'root',
    converter: companyNoteConverter,
  },
])

export default function PreviewClientSetup({ children }: { children: ReactNode }) {
  return <>{children}</>
}

Import PreviewClientSetup into the Payload config and update its existing option:

src/payload.config.ts
+import PreviewClientSetup from './payload/previewClientSetup'
+import { companyNoteBlock } from './blocks/CompanyNote'

-  livePreview: { mode: 'client' },
+  livePreview: { mode: 'client', clientSetup: PreviewClientSetup },
+  customBlocks: [companyNoteBlock],

Apply these additions to the existing plugin-options object. The wrapper registers at module scope before the preview view renders. Only the slug and converter are needed in the browser, so the client registration uses a minimal schema instead of importing the editor factory. Keep runtime Payload imports and database queries out of its import tree.

6. Regenerate and authorLink to this section

pnpm sync
pnpm exec tsc --noEmit

Insert Company Note at the root of a page's content. Add a heading and body, open live preview, then publish. For production, handle schema changes through the site's migration process.

Check your workLink to this section

  • The block appears in the root editor, not caption fields.
  • The empty block does not crash while editing.
  • Unsaved preview changes update the heading and body.
  • The signed-out published page renders the same content.
  • There is one section band and no duplicated container.

Next: Connect forms and SEO. Reference: Custom blocks.