---
title: "Build a custom block"
description: "Implement a Company Note block from its Payload schema through rendering and live preview."
requested_language: ar
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/ar/guides/build-a-site/custom-block
version: unreleased (main)
docs_index: https://docs.systhema.app/ar/llms.txt
---
> This page isn't translated yet. Showing English.


## Goal

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 together

```text title="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 schema

Use the `CompanyNoteSchema` from [Content model](content-model.md). 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 presentation

```tsx title="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 fields

```tsx title="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.

```ts title="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 preview

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:

```tsx title="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:

```diff title="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 author

```bash
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 work

- 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](forms-and-seo.md). Reference: [Custom blocks](https://docs.systhema.app/ar/payload/custom-blocks.md).
