@nextbridgehq/payload-block-builder 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,70 @@
1
1
  # Payload Block Builder
2
2
 
3
- Developed and open-sourced by [Nextbridge](https://nextbridge.com). This plugin was built to solve a real problem we kept running into: content editors needing to manage flexible page layouts without requiring a developer for every change.
3
+ [![npm version](https://img.shields.io/npm/v/@nextbridgehq/payload-block-builder.svg)](https://www.npmjs.com/package/@nextbridgehq/payload-block-builder)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Payload CMS](https://img.shields.io/badge/Payload-v3-blue.svg)](https://payloadcms.com)
4
6
 
5
- ---
7
+ > 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.
6
8
 
7
- A visual block builder plugin for Payload v3. Design your content blocks through a drag-and-drop UI, store the schemas in your database, and let editors build pages without waiting on a developer every time something needs to change.
9
+ Developed and open-sourced by [Nextbridge](https://nextbridge.com).
8
10
 
9
- ## Use cases
11
+ [GitHub](https://github.com/nextbridgehq/block-builder) · [Releases](https://github.com/nextbridgehq/block-builder/releases) · [Issues](https://github.com/nextbridgehq/block-builder/issues) · [Changelog](./CHANGELOG.md) · [Payload CMS](https://payloadcms.com)
10
12
 
11
- - **Dynamic landing pages:** Let editors compose pages from a library of blocks (hero, features, testimonials, CTA) without any code changes.
12
- - **Multi-tenant platforms:** Each tenant can have its own block definitions without touching shared config or triggering redeployments.
13
- - **Marketing teams:** Give marketing full control to create, update, and reorder blocks on any page, any time.
14
- - **Evolving content schemas:** Roll out new block versions without breaking content that was built against older ones.
15
- - **Headless frontends:** Fetch structured block data from the Payload API and render it with any framework.
13
+ ## Screenshots
16
14
 
17
- ## Database compatibility
15
+ ![Block Builder canvas](https://raw.githubusercontent.com/nextbridgehq/block-builder/main/docs/screenshots/canvas.png)
16
+ ![Schema builder field](https://raw.githubusercontent.com/nextbridgehq/block-builder/main/docs/screenshots/schema-builder.png)
17
+ ![DB Layout tab on a collection](https://raw.githubusercontent.com/nextbridgehq/block-builder/main/docs/screenshots/db-layout-tab.png)
18
18
 
19
- Works with all Payload-supported databases — no direct SQL, no database-specific code:
19
+ ---
20
20
 
21
- | Database | Adapter |
22
- |---|---|
23
- | PostgreSQL / Supabase / Neon | `@payloadcms/db-postgres` |
24
- | SQLite / Turso / LibSQL | `@payloadcms/db-sqlite` |
25
- | MongoDB | `@payloadcms/db-mongodb` |
21
+ ## 🎯 The Problem
22
+
23
+ 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.
24
+
25
+ ## 💡 The Solution
26
+
27
+ 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.
26
28
 
27
29
  ---
28
30
 
29
- ## Quick start
31
+ ## Features
32
+
33
+ - **Visual drag-and-drop block designer** — No code required to create new block types
34
+ - **Database-stored schemas** — Block definitions live in your DB, not your codebase
35
+ - **Version snapshots with restore** — Every publish creates an immutable, concurrency-safe version; browse history, view any past version read-only, or restore it as a new version — existing content never breaks
36
+ - **Collection integration** — Adds a "DB Layout" tab to any collection with one line of config, plus a nav-sidebar shortcut and an "Edit in Builder" button on each block definition
37
+ - **16 field types**, including layout containers — text, textarea, number, email, date, checkbox, select, image, file, relationship, json, and the Row / Group / Array / Tabs / Collapsible layout fields for structuring and nesting a block's own fields
38
+ - **Live Preview** — A self-contained, zero-config panel that renders a block's schema as a mock form as you build it — labels, field slugs (the exact JSON path a frontend integration should read, including array/group nesting and named-tab keys), and realistic placeholders per field type. No external URL or frontend receiver needed.
39
+ - **JSON Import / Export** — Export any block's schema to a `.json` file, or import a hand-authored or previously-exported one back into the builder
40
+ - **Multi-tenant ready** — Each tenant can have its own block definitions without shared config changes
41
+ - **Framework agnostic frontend** — Fetch structured JSON and render with React, Vue, Svelte, or anything else
42
+ - **Automatic init command** — Get up and running in under 2 minutes
43
+ - **Works with all Payload databases** — PostgreSQL, SQLite, MongoDB — no database-specific code
44
+
45
+ ---
30
46
 
31
- ### Option A — Automatic setup (recommended)
47
+ ## 🚀 Quick Start
32
48
 
33
- Install the package and run the init command from your project root:
49
+ ### Option A Automatic Setup (Recommended)
34
50
 
35
51
  ```bash
52
+ # Install
36
53
  pnpm add @nextbridgehq/payload-block-builder
37
54
  # or: npm install @nextbridgehq/payload-block-builder
38
55
 
56
+ # Initialize
39
57
  npx payload-block-builder init
40
58
  ```
41
59
 
42
60
  The init command automatically:
43
61
 
44
- - Creates `src/app/block-builder/page.tsx` the builder UI page
45
- - Creates `src/app/block-builder/layout.tsx` — standalone layout with `<html>` and `<body>` tags
46
- - Updates `src/app/(payload)/custom.scss` — injects admin field styles
47
- - Updates `payload.config.ts` adds the `dynamicBlocksPlugin` import and config
62
+ | What it does | File |
63
+ |---|---|
64
+ | Creates the builder UI page | `src/app/block-builder/page.tsx` |
65
+ | Creates a standalone layout | `src/app/block-builder/layout.tsx` |
66
+ | Injects admin field styles | `src/app/(payload)/custom.scss` |
67
+ | Adds plugin config | `payload.config.ts` |
48
68
 
49
69
  Then regenerate the import map and start your dev server:
50
70
 
@@ -53,17 +73,10 @@ pnpm generate:importmap
53
73
  pnpm dev
54
74
  ```
55
75
 
56
- Visit `http://localhost:3000/block-builder` and you're ready to build.
57
-
58
- > **PostgreSQL users:** Payload will automatically push the new schema tables on first startup in dev mode. If you are using migrations in production, run:
59
- > ```bash
60
- > pnpm payload migrate:create --name=add_block_builder
61
- > pnpm payload migrate
62
- > ```
63
-
64
- ---
76
+ Visit `https://your-domain.com/block-builder` and you're ready to build.
65
77
 
66
- ### Option B — Manual setup
78
+ <details>
79
+ <summary><strong>Option B — Manual Setup</strong></summary>
67
80
 
68
81
  **1. Install:**
69
82
 
@@ -138,19 +151,70 @@ pnpm generate:importmap
138
151
  pnpm dev
139
152
  ```
140
153
 
154
+ </details>
155
+
141
156
  ---
142
157
 
143
- ## Plugin options
158
+ ## Upgrading from 0.1.x
159
+
160
+ **0.2.0 changes the database schema.** The `block-definition-versions`
161
+ collection gains a `versionIdString` column with a unique index — this is what
162
+ makes concurrent publishing safe, replacing the previous count-then-insert
163
+ version numbering that could hand out the same number twice.
164
+
165
+ Run a migration before deploying:
166
+
167
+ ```bash
168
+ pnpm payload migrate:create --name=block_builder_0_2_0
169
+ pnpm payload migrate
170
+ ```
171
+
172
+ In dev mode Payload pushes the column automatically; **production will not start
173
+ correctly without the migration.**
174
+
175
+ Notes:
176
+
177
+ - Existing version rows are backfilled with `NULL`, which the unique index
178
+ permits. Historical versions are readable and restorable as before, but the
179
+ uniqueness guarantee only applies to versions published from 0.2.0 onward.
180
+ - MongoDB users need no migration.
181
+
182
+ ### Other breaking changes in 0.2.0
183
+
184
+ | Change | Impact |
185
+ | --- | --- |
186
+ | 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. |
187
+ | `generateAllBlocks()` returns one file per block again | Pass `{ react: true }` to also emit the `.tsx` component stub, which 0.2.0-beta emitted unconditionally. |
188
+ | 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`. |
189
+ | `radio` and `upload` removed from the palette | Use `select` and `image`/`file`. Existing schemas still load. |
190
+
191
+ > **PostgreSQL users:** Payload will automatically push new schema tables on first startup in dev mode. For production migrations:
192
+ >
193
+ > ```bash
194
+ > pnpm payload migrate:create --name=add_block_builder
195
+ > pnpm payload migrate
196
+ > ```
197
+
198
+ ---
199
+
200
+ ## ⚙️ Configuration Options
144
201
 
145
202
  ```ts
146
203
  dynamicBlocksPlugin({
147
- enabled?: boolean // Disable without removing. Default: true
148
- collections?: string[] // Collection slugs that get the DB Layout tab. Default: []
149
- fieldName?: string // Name of the layout array field. Default: 'dbLayout'
150
- tabLabel?: string // Label shown on the tab in the admin UI. Default: 'DB Layout'
204
+ enabled?: boolean, // Disable without removing. Default: true
205
+ collections?: string[], // Collection slugs that get the DB Layout tab. Default: []
206
+ fieldName?: string, // Name of the layout array field. Default: 'dbLayout'
207
+ tabLabel?: string, // Label shown on the tab in the admin UI. Default: 'DB Layout'
151
208
  })
152
209
  ```
153
210
 
211
+ | Option | Type | Default | Description |
212
+ |---|---|---|---|
213
+ | `enabled` | `boolean` | `true` | Toggle the plugin on/off without removing it from config |
214
+ | `collections` | `string[]` | `[]` | Collection slugs that receive the DB Layout tab |
215
+ | `fieldName` | `string` | `'dbLayout'` | The field name for the layout array stored on documents |
216
+ | `tabLabel` | `string` | `'DB Layout'` | Label displayed on the tab in the Payload admin UI |
217
+
154
218
  The `--collections` flag is also supported in the init command:
155
219
 
156
220
  ```bash
@@ -159,74 +223,138 @@ npx payload-block-builder init --collections=pages,posts
159
223
 
160
224
  ---
161
225
 
162
- ## Usage
226
+ ## 📖 Usage
163
227
 
164
- ### Creating a block
228
+ ### Creating a Block
165
229
 
166
- 1. Open `/block-builder` in your browser.
230
+ 1. Open `/block-builder` in your browser (also reachable from the admin sidebar and via the "Edit in Builder" button on any block definition).
167
231
  2. Click "Add Block" and give it a name and slug.
168
- 3. Drag fields from the panel on the right onto the canvas.
169
- 4. Configure each field (label, name, required, options, etc.).
170
- 5. Click Publish. The block schema is saved to your database and a version snapshot is created.
232
+ 3. Drag fields from the panel on the left onto the canvas — including layout fields (Row, Group, Array, Tabs, Collapsible) to nest and structure fields; use each layout field's "Edit Fields" button to drill into its contents.
233
+ 4. Configure each field (label, name, required, options, etc.) in the panel on the right.
234
+ 5. Open **Live Preview** at any time to see a mock rendering of the block's current shape, with each field's exact data path.
235
+ 6. Click Publish — the block schema is validated and saved to your database, and a new immutable version snapshot is created.
236
+ 7. Use **Import JSON** / **Export JSON** in the toolbar to move a block's schema in or out as a file — handy for backups, sharing a schema between projects, or hand-authoring one.
171
237
 
172
- ### Using blocks in a collection
238
+ ### Using Blocks in a Collection
173
239
 
174
240
  Any collection listed in the `collections` option gets a new "DB Layout" tab in the Payload admin. Editors can:
175
241
 
176
242
  1. Click "Add Row" to add a block instance.
177
- 2. Select a block definition and the version of its schema to use.
243
+ 2. Select a block definition its current version is auto-selected, or pick a specific version manually.
178
244
  3. Fill in the fields — they render dynamically based on the selected schema.
179
245
  4. Reorder, hide, or add anchor IDs to individual block instances.
180
246
  5. Save the document as normal.
181
247
 
182
- ### Reading block data on the frontend
248
+ ### Live Preview
249
+
250
+ 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:
251
+
252
+ - The field's label and, right beside it, its **slug** — the exact key a frontend integration reads. For a top-level field this is just its name (`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.
253
+ - A realistic, type-appropriate placeholder (respecting an explicit `admin.placeholder` if one is set).
254
+
255
+ ### Reading Block Data on the Frontend
183
256
 
184
257
  ```ts
185
258
  const res = await fetch('/api/pages/my-page?depth=2')
186
259
  const page = await res.json()
187
260
 
188
261
  for (const block of page.dbLayout) {
189
- const type = block.blockDefinition.slug // e.g. "hero"
190
- const fields = block.data // { heading: '...', image: '...', ... }
262
+ const type = block.blockDefinition.slug // e.g. "hero"
263
+ const fields = block.data // { heading: '...', image: '...', ... }
191
264
  const isHidden = block.hidden
265
+ const anchor = block.anchor
266
+ }
267
+ ```
268
+
269
+ ### Example: React Component Map
270
+
271
+ ```tsx
272
+ const blockComponents = {
273
+ hero: HeroBlock,
274
+ features: FeaturesBlock,
275
+ testimonials: TestimonialsBlock,
276
+ cta: CTABlock,
277
+ }
278
+
279
+ function PageRenderer({ blocks }) {
280
+ return (
281
+ <>
282
+ {blocks
283
+ .filter((block) => !block.hidden)
284
+ .map((block, i) => {
285
+ const Component = blockComponents[block.blockDefinition.slug]
286
+ if (!Component) return null
287
+ return (
288
+ <section key={block.instanceId ?? i} id={block.anchor || undefined}>
289
+ <Component {...block.data} />
290
+ </section>
291
+ )
292
+ })}
293
+ </>
294
+ )
192
295
  }
193
296
  ```
194
297
 
195
- Render each block type however you like — a switch statement or a component map both work well.
298
+ ---
299
+
300
+ ## 🧩 Supported Field Types
301
+
302
+ | Category | Types |
303
+ |---|---|
304
+ | Basic | `text`, `textarea`, `number`, `email`, `date`, `checkbox` |
305
+ | Choice | `select` (custom label/value options) |
306
+ | Media | `image`, `file` |
307
+ | Relational | `relationship` (link to any collection, optionally `hasMany`) |
308
+ | Layout | `array`, `group`, `row`, `tabs`, `collapsible` |
309
+ | Advanced | `json` (raw JSON data) |
310
+
311
+ **Layout fields** structure and nest a block's other fields rather than holding a value themselves:
312
+
313
+ | Type | Behavior |
314
+ |---|---|
315
+ | `group` | Nests its fields' data under its own name (`group.field`) |
316
+ | `array` | A repeating list of fields; each row's data lives under the array's name (`items[].field`) |
317
+ | `row` | Presentation only — lays its fields out horizontally; their data flattens into the parent, no nesting |
318
+ | `collapsible` | Presentation only — an expandable section; its fields also flatten into the parent |
319
+ | `tabs` | A tab strip; a *named* tab nests its fields under the tab's name, an *unnamed* tab flattens into the parent |
320
+
321
+ Live Preview shows the exact resolved data path for every field, including through nested layout fields — see the Live Preview section above.
196
322
 
197
323
  ---
198
324
 
199
- ## How it works
325
+ ## 🗺️ Use Cases
200
326
 
201
- - **Block definitions** are stored in a `block-definitions` collection. Each document is a named block type with a slug and a list of field definitions.
202
- - **Versions** are stored in a `block-definition-versions` collection. Every time you publish a block, a snapshot of its schema is saved as a new version.
203
- - **Documents** in opted-in collections store a reference to the exact block version they were built against, so updating a block schema later does not break existing content.
204
- - **The DB Layout tab** is injected automatically into each collection you list. It renders a dynamic array field where editors pick a block and version, and the field UI adjusts to match.
205
- - **Four internal API endpoints** power the builder UI and the admin field components. You do not need to call them directly.
327
+ | Use Case | How It Helps |
328
+ |---|---|
329
+ | Dynamic landing pages | Editors compose pages from a library of blocks (hero, features, testimonials, CTA) without code changes |
330
+ | Multi-tenant platforms | Each tenant gets its own block definitions without touching shared config or triggering redeployments |
331
+ | Marketing teams | Full control to create, update, and reorder blocks on any page, any time |
332
+ | Evolving content schemas | Roll out new block versions without breaking content built against older ones |
333
+ | Headless frontends | Fetch structured block data from the Payload API and render with any framework |
206
334
 
207
335
  ---
208
336
 
209
- ## Supported field types
337
+ ## 📋 Compatibility
210
338
 
211
- These field types are available in the block builder and render correctly in the admin field UI:
339
+ | Requirement | Version |
340
+ |---|---|
341
+ | Payload CMS | v3.x |
342
+ | Node.js | ≥ 18 |
343
+ | Next.js | ≥ 14 |
344
+
345
+ ### Database Support
212
346
 
213
- | Type | Description |
347
+ | Database | Adapter |
214
348
  |---|---|
215
- | `text` | Single-line text input |
216
- | `textarea` | Multi-line text input |
217
- | `number` | Numeric input |
218
- | `email` | Email address |
219
- | `date` | Date picker |
220
- | `checkbox` | Boolean toggle |
221
- | `select` | Dropdown with custom options |
222
- | `radio` | Radio button group with custom options |
223
- | `upload` | File / image picker (from the media collection) |
224
- | `relationship` | Document picker from any collection |
225
- | `json` | Raw JSON data |
349
+ | PostgreSQL / Supabase / Neon | `@payloadcms/db-postgres` |
350
+ | SQLite / Turso / LibSQL | `@payloadcms/db-sqlite` |
351
+ | MongoDB | `@payloadcms/db-mongodb` |
226
352
 
227
353
  ---
228
354
 
229
- ## Using `dbLayoutField` directly
355
+ ## 🔧 Advanced Usage
356
+
357
+ ### Using `dbLayoutField` Directly
230
358
 
231
359
  If you prefer not to use the plugin's `collections` option, you can add the layout tab manually to any collection:
232
360
 
@@ -239,9 +367,9 @@ export const Pages: CollectionConfig = {
239
367
  {
240
368
  type: 'tabs',
241
369
  tabs: [
242
- { label: 'Content', fields: [] },
243
- dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
244
- dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
370
+ { label: 'Content', fields: [/* your fields */] },
371
+ dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
372
+ dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
245
373
  ],
246
374
  },
247
375
  ],
@@ -250,7 +378,44 @@ export const Pages: CollectionConfig = {
250
378
 
251
379
  ---
252
380
 
253
- ## CSS imports reference
381
+ ## 🏗️ Architecture
382
+
383
+ ```
384
+ ┌─────────────────────────────────────────────────────┐
385
+ │ Block Builder UI │
386
+ │ /block-builder (drag & drop) │
387
+ └──────────────────────────┬──────────────────────────┘
388
+ │ Publish
389
+
390
+ ┌─────────────────────────────────────────────────────┐
391
+ │ block-definitions collection │
392
+ │ (name, slug, field definitions) │
393
+ └──────────────────────────┬──────────────────────────┘
394
+ │ Snapshot
395
+
396
+ ┌─────────────────────────────────────────────────────┐
397
+ │ block-definition-versions collection │
398
+ │ (immutable schema snapshots per publish) │
399
+ └──────────────────────────┬──────────────────────────┘
400
+ │ Referenced by
401
+
402
+ ┌─────────────────────────────────────────────────────┐
403
+ │ Your Collection (e.g. "pages") │
404
+ │ dbLayout: [{ blockDefinition, version, data }] │
405
+ └─────────────────────────────────────────────────────┘
406
+ ```
407
+
408
+ Key design decisions:
409
+
410
+ - Block definitions are stored in a `block-definitions` collection. Each document is a named block type with a slug and a list of field definitions.
411
+ - Versions are stored in a `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.
412
+ - Documents in opted-in collections store a reference to the exact block version they were built against — updating a block schema later does not break existing content.
413
+ - The DB Layout tab is injected automatically into each collection you list. It renders a dynamic array field where editors pick a block and version.
414
+ - Four internal API endpoints power the builder UI and admin field components — each requires an authenticated Payload session and a `X-Block-Builder: 1` header (CSRF protection). You do not need to call them directly.
415
+
416
+ ---
417
+
418
+ ## 📦 CSS Imports Reference
254
419
 
255
420
  | Import path | Purpose |
256
421
  |---|---|
@@ -260,6 +425,45 @@ export const Pages: CollectionConfig = {
260
425
 
261
426
  ---
262
427
 
263
- ## License
428
+ ## 🧪 Testing
429
+
430
+ 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:
431
+
432
+ ```bash
433
+ npm test # run once
434
+ npm run test:watch # watch mode
435
+ npm run test:coverage # with coverage thresholds
436
+ npm run check:encoding # scan for stray UTF-8 BOMs / mojibake
437
+ ```
438
+
439
+ CI (`.github/workflows/ci.yml`) runs typecheck → test → build → encoding check on every push.
440
+
441
+ ---
442
+
443
+ ## 📝 Changelog
444
+
445
+ See [CHANGELOG.md](./CHANGELOG.md) for the full history. Latest release:
446
+
447
+ **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).
448
+
449
+ ---
450
+
451
+ ## 🤝 Contributing
452
+
453
+ Contributions are welcome! Please see our Contributing Guide for details.
454
+
455
+ - Fork the repository
456
+ - Create your feature branch (`git checkout -b feature/amazing-feature`)
457
+ - Commit your changes (`git commit -m 'Add amazing feature'`)
458
+ - Push to the branch (`git push origin feature/amazing-feature`)
459
+ - Open a Pull Request
460
+
461
+ ---
462
+
463
+ ## 📄 License
264
464
 
265
465
  MIT © [Nextbridge](https://nextbridge.com)
466
+
467
+ ---
468
+
469
+ Built and maintained by **[Nextbridge](https://nextbridge.com)** — 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.
package/dist/bin/init.js CHANGED
File without changes