Moves block definitions from code into database collections and provides a visual builder page for designing them. Adding the plugin to chosen collections gives each a DB Layout tab where editors pick a block and a version, then fill in dynamically rendered fields. It works with Postgres, SQLite and MongoDB, and includes an init CLI. Requires Payload 3 and Next.js.
Block Builder for Payload CMS
Installation
npm i @nextbridgehq/payload-block-builder8
Stars
0
Forks
0
Open Issues
1 months ago
Last Updated
0
Weekly Downloads
v0.2.0
npm Version
538.8 KB
Unpacked Size
7
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1274072908-root)<a href="https://payloaddirectory.dev/plugins/1274072908-root"><img src="https://payloaddirectory.dev/badge/1274072908-root.svg" alt="Payload Directory"></a>README
A visual block builder plugin for Payload CMS v3. Design content blocks through a drag-and-drop UI, store schemas in your database, and let editors build pages without waiting on a developer.
Developed and open-sourced by Nextbridge.
GitHub · Releases · Issues · Changelog · Payload CMS

Content editors need to manage flexible page layouts — but every new block type or layout change requires a developer to update code, redeploy, and migrate. This creates bottlenecks, slows down marketing teams, and turns simple content tasks into engineering tickets.
Payload Block Builder moves block schema definitions from code into your database. Editors design blocks visually, publish them instantly, and use them across any collection — all without touching code or triggering deployments.
.json file, or import a hand-authored or previously-exported one back into the builder# Install
pnpm add @nextbridgehq/payload-block-builder
# or: npm install @nextbridgehq/payload-block-builder
# Initialize
npx payload-block-builder init
The init command automatically:
| What it does | File |
|---|---|
| Creates the builder UI page | src/app/block-builder/page.tsx |
| Creates a standalone layout | src/app/block-builder/layout.tsx |
| Injects admin field styles | src/app/(payload)/custom.scss |
| Adds plugin config | payload.config.ts |
Then regenerate the import map and start your dev server:
pnpm generate:importmap
pnpm dev
Visit https://your-domain.com/block-builder and you're ready to build.
1. Install:
pnpm add @nextbridgehq/payload-block-builder
2. Add the plugin to payload.config.ts:
import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'
export default buildConfig({
plugins: [
dynamicBlocksPlugin({
collections: ['pages'],
}),
],
})
3. Create src/app/block-builder/layout.tsx:
import React from 'react'
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import config from '@payload-config'
import '@nextbridgehq/payload-block-builder/builder.css'
export const metadata = { title: 'Block Builder' }
export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers: await headers() })
if (!user) redirect('/admin/login')
return (
<html lang="en">
<body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
{children}
</body>
</html>
)
}
4. Create src/app/block-builder/page.tsx:
'use client'
import { BuilderShell } from '@nextbridgehq/payload-block-builder/client'
export default function BlockBuilderPage() {
return <BuilderShell />
}
5. Add admin field styles to src/app/(payload)/custom.scss:
@import '@nextbridgehq/payload-block-builder/block-data-field.css';
@import '@nextbridgehq/payload-block-builder/schema-builder-field.css';
6. Regenerate the import map and start the dev server:
pnpm generate:importmap
pnpm dev
0.2.0 changes the database schema. The block-definition-versions
collection gains a versionIdString column with a unique index — this is what
makes concurrent publishing safe, replacing the previous count-then-insert
version numbering that could hand out the same number twice.
Run a migration before deploying:
pnpm payload migrate:create --name=block_builder_0_2_0
pnpm payload migrate
In dev mode Payload pushes the column automatically; production will not start correctly without the migration.
Notes:
NULL, which the unique index
permits. Historical versions are readable and restorable as before, but the
uniqueness guarantee only applies to versions published from 0.2.0 onward.| Change | Impact |
|---|---|
Internal endpoints require an X-Block-Builder: 1 header | Only affects code calling /api/blocks/* directly. The builder UI and admin components send it already. |
generateAllBlocks() returns one file per block again | Pass { react: true } to also emit the .tsx component stub, which 0.2.0-beta emitted unconditionally. |
| Field-type vocabulary unified | The builder now uses richtext, image, file, and collection internally, matching the stored schema. Generated Payload config is unaffected — it is translated at emit time to richText, upload, and relationTo. |
radio and upload removed from the palette | Use select and image/file. Existing schemas still load. |
PostgreSQL users: Payload will automatically push new schema tables on first startup in dev mode. For production migrations:
pnpm payload migrate:create --name=add_block_builder pnpm payload migrate
dynamicBlocksPlugin({
enabled?: boolean, // Disable without removing. Default: true
collections?: string[], // Collection slugs that get the DB Layout tab. Default: []
fieldName?: string, // Name of the layout array field. Default: 'dbLayout'
tabLabel?: string, // Label shown on the tab in the admin UI. Default: 'DB Layout'
})
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Toggle the plugin on/off without removing it from config |
collections | string[] | [] | Collection slugs that receive the DB Layout tab |
fieldName | string | 'dbLayout' | The field name for the layout array stored on documents |
tabLabel | string | 'DB Layout' | Label displayed on the tab in the Payload admin UI |
The --collections flag is also supported in the init command:
npx payload-block-builder init --collections=pages,posts
/block-builder in your browser (also reachable from the admin sidebar and via the "Edit in Builder" button on any block definition).Any collection listed in the collections option gets a new "DB Layout" tab in the Payload admin. Editors can:
The Live Preview panel (toggled from the builder's toolbar) renders the shape of a block being designed — not a real page, since the builder has no way to know what a "Hero" block should look like on your actual frontend. For each field it shows:
heading); for a field nested in a Group it's dot-prefixed (cta.label); for one inside an Array it gets a trailing [] (items[].title); for a field inside a named Tab it's prefixed by the tab's name (seo.metaTitle). Row, Collapsible, and unnamed Tabs are presentation-only in Payload and flatten their children into the surrounding data — so they never show a slug of their own, and their children inherit whatever prefix they themselves received.admin.placeholder if one is set).const res = await fetch('/api/pages/my-page?depth=2')
const page = await res.json()
for (const block of page.dbLayout) {
const type = block.blockDefinition.slug // e.g. "hero"
const fields = block.data // { heading: '...', image: '...', ... }
const isHidden = block.hidden
const anchor = block.anchor
}
const blockComponents = {
hero: HeroBlock,
features: FeaturesBlock,
testimonials: TestimonialsBlock,
cta: CTABlock,
}
function PageRenderer({ blocks }) {
return (
<>
{blocks
.filter((block) => !block.hidden)
.map((block, i) => {
const Component = blockComponents[block.blockDefinition.slug]
if (!Component) return null
return (
<section key={block.instanceId ?? i} id={block.anchor || undefined}>
<Component {...block.data} />
</section>
)
})}
</>
)
}
| Category | Types |
|---|---|
| Basic | text, textarea, number, email, date, checkbox |
| Choice | select (custom label/value options) |
| Media | image, file |
| Relational | relationship (link to any collection, optionally hasMany) |
| Layout | array, group, row, tabs, collapsible |
| Advanced | json (raw JSON data) |
Layout fields structure and nest a block's other fields rather than holding a value themselves:
| Type | Behavior |
|---|---|
group | Nests its fields' data under its own name (group.field) |
array | A repeating list of fields; each row's data lives under the array's name (items[].field) |
row | Presentation only — lays its fields out horizontally; their data flattens into the parent, no nesting |
collapsible | Presentation only — an expandable section; its fields also flatten into the parent |
tabs | A tab strip; a named tab nests its fields under the tab's name, an unnamed tab flattens into the parent |
Live Preview shows the exact resolved data path for every field, including through nested layout fields — see the Live Preview section above.
| Use Case | How It Helps |
|---|---|
| Dynamic landing pages | Editors compose pages from a library of blocks (hero, features, testimonials, CTA) without code changes |
| Multi-tenant platforms | Each tenant gets its own block definitions without touching shared config or triggering redeployments |
| Marketing teams | Full control to create, update, and reorder blocks on any page, any time |
| Evolving content schemas | Roll out new block versions without breaking content built against older ones |
| Headless frontends | Fetch structured block data from the Payload API and render with any framework |
| Requirement | Version |
|---|---|
| Payload CMS | v3.x |
| Node.js | ≥ 18 |
| Next.js | ≥ 14 |
| Database | Adapter |
|---|---|
| PostgreSQL / Supabase / Neon | @payloadcms/db-postgres |
| SQLite / Turso / LibSQL | @payloadcms/db-sqlite |
| MongoDB | @payloadcms/db-mongodb |
dbLayoutField DirectlyIf you prefer not to use the plugin's collections option, you can add the layout tab manually to any collection:
import { dbLayoutField } from '@nextbridgehq/payload-block-builder'
export const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
type: 'tabs',
tabs: [
{ label: 'Content', fields: [/* your fields */] },
dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
],
},
],
}
┌─────────────────────────────────────────────────────┐
│ Block Builder UI │
│ /block-builder (drag & drop) │
└──────────────────────────┬──────────────────────────┘
│ Publish
▼
┌─────────────────────────────────────────────────────┐
│ block-definitions collection │
│ (name, slug, field definitions) │
└──────────────────────────┬──────────────────────────┘
│ Snapshot
▼
┌─────────────────────────────────────────────────────┐
│ block-definition-versions collection │
│ (immutable schema snapshots per publish) │
└──────────────────────────┬──────────────────────────┘
│ Referenced by
▼
┌─────────────────────────────────────────────────────┐
│ Your Collection (e.g. "pages") │
│ dbLayout: [{ blockDefinition, version, data }] │
└─────────────────────────────────────────────────────┘
Key design decisions:
block-definitions collection. Each document is a named block type with a slug and a list of field definitions.block-definition-versions collection. Every publish creates an immutable snapshot; version numbers are assigned with a retry loop against a unique constraint, so concurrent publishes from two editors can never collide.X-Block-Builder: 1 header (CSRF protection). You do not need to call them directly.| Import path | Purpose |
|---|---|
@nextbridgehq/payload-block-builder/builder.css | Block Builder UI page styles |
@nextbridgehq/payload-block-builder/block-data-field.css | DB Layout field styles in admin |
@nextbridgehq/payload-block-builder/schema-builder-field.css | Schema Builder field styles in admin |
The package ships a Vitest suite (114 tests, including a golden-file snapshot of generated code per field type) covering schema normalization, validation, and code generation:
npm test # run once
npm run test:watch # watch mode
npm run test:coverage # with coverage thresholds
npm run check:encoding # scan for stray UTF-8 BOMs / mojibake
CI (.github/workflows/ci.yml) runs typecheck → test → build → encoding check on every push.
See CHANGELOG.md for the full history. Latest release:
0.2.0 — Live Preview, Layout fields (Row/Group/Array/Tabs/Collapsible), JSON Import/Export, version history & restore, concurrency-safe publishing, CSRF protection, a 114-test suite with CI, and a large batch of correctness fixes (generated-code compilation, nested-field validation, breadcrumb navigation, and more — see CHANGELOG.md for the full list).
Contributions are welcome! Please see our Contributing Guide for details.
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)MIT © Nextbridge
Built and maintained by Nextbridge — If Payload Block Builder helped you ship content blocks without waiting on a developer, a ⭐ would mean a lot — it helps other developers discover Payload Block Builder.
theme management
Theme presets, live preview and WCAG contrast audit managed from the Payload admin, with SSR theme injection for Next.js
650/wk9v3
blockwright
Visual drag-and-drop page and theme builder for Payload with widgets, forms, menus and templates
610/wk3v3
payload theme
shadcn-style redesign of the Payload admin panel driven by one accent color, with dashboard, command palette and dark…
69909/wk26v3
payload nested docs page tree
Drag-and-drop page tree with status badges for Payload nested docs collections in the admin list view
690/wk35v3
payload nav studio
Replace the Payload admin sidebar with a drag-and-drop navigation that each user can customize
640/wk13v3
payload plugin
Payload plugin adding layouts, templates, menus, forms and a visual editor for the Blockwright page builder
600/wk3v3