@nextbridgehq/payload-block-builder 0.1.5 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.md +109 -37
  2. package/dist/bin/init.js +135 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,8 @@
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.
4
+
5
+ ---
3
6
 
4
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.
5
8
 
@@ -13,13 +16,50 @@ A visual block builder plugin for Payload v3. Design your content blocks through
13
16
 
14
17
  ## Quick start
15
18
 
16
- 1. Install the plugin:
19
+ ### Option A — Automatic setup (recommended)
20
+
21
+ Install the package and run the init command from your project root:
22
+
23
+ ```bash
24
+ pnpm add @nextbridgehq/payload-block-builder
25
+ # or: npm install @nextbridgehq/payload-block-builder
26
+
27
+ npx payload-block-builder init
28
+ ```
29
+
30
+ The init command automatically:
31
+
32
+ - Creates `src/app/block-builder/page.tsx` — the builder UI page
33
+ - Creates `src/app/block-builder/layout.tsx` — standalone layout with `<html>` and `<body>` tags
34
+ - Updates `src/app/(payload)/custom.scss` — injects admin field styles
35
+ - Updates `payload.config.ts` — adds the `dynamicBlocksPlugin` import and config
36
+
37
+ Then regenerate the import map and start your dev server:
17
38
 
18
39
  ```bash
19
- npm install @nextbridgehq/payload-block-builder
40
+ pnpm generate:importmap
41
+ pnpm dev
20
42
  ```
21
43
 
22
- 2. Add the plugin to your `payload.config.ts`:
44
+ Visit `http://localhost:3000/block-builder` and you're ready to build.
45
+
46
+ > **PostgreSQL users:** Payload will automatically push the new schema tables on first startup in dev mode. If you are using migrations in production, run:
47
+ > ```bash
48
+ > pnpm payload migrate:create --name=add_block_builder
49
+ > pnpm payload migrate
50
+ > ```
51
+
52
+ ---
53
+
54
+ ### Option B — Manual setup
55
+
56
+ **1. Install:**
57
+
58
+ ```bash
59
+ pnpm add @nextbridgehq/payload-block-builder
60
+ ```
61
+
62
+ **2. Add the plugin to `payload.config.ts`:**
23
63
 
24
64
  ```ts
25
65
  import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'
@@ -27,36 +67,77 @@ import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'
27
67
  export default buildConfig({
28
68
  plugins: [
29
69
  dynamicBlocksPlugin({
30
- collections: ['pages', 'posts'],
70
+ collections: ['pages'],
31
71
  }),
32
72
  ],
33
73
  })
34
74
  ```
35
75
 
36
- 3. Create a page in your Next.js app to host the builder UI:
76
+ **3. Create `src/app/block-builder/layout.tsx`:**
77
+
78
+ ```tsx
79
+ import React from 'react'
80
+ import '@nextbridgehq/payload-block-builder/builder.css'
81
+
82
+ export const metadata = { title: 'Block Builder' }
83
+
84
+ export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
85
+ return (
86
+ <html lang="en">
87
+ <body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
88
+ {children}
89
+ </body>
90
+ </html>
91
+ )
92
+ }
93
+ ```
94
+
95
+ **4. Create `src/app/block-builder/page.tsx`:**
37
96
 
38
97
  ```tsx
39
- // app/block-builder/page.tsx
40
98
  'use client'
41
99
 
42
100
  import { BuilderShell } from '@nextbridgehq/payload-block-builder/client'
43
- import '@nextbridgehq/payload-block-builder/builder.css'
44
101
 
45
102
  export default function BlockBuilderPage() {
46
103
  return <BuilderShell />
47
104
  }
48
105
  ```
49
106
 
50
- 4. If you're on a SQL database, run the migration:
107
+ **5. Add admin field styles to `src/app/(payload)/custom.scss`:**
108
+
109
+ ```scss
110
+ @import '@nextbridgehq/payload-block-builder/block-data-field.css';
111
+ @import '@nextbridgehq/payload-block-builder/schema-builder-field.css';
112
+ ```
113
+
114
+ **6. Regenerate the import map and start the dev server:**
51
115
 
52
116
  ```bash
53
- npx payload migrate:create
54
- npx payload migrate:run
117
+ pnpm generate:importmap
118
+ pnpm dev
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Plugin options
124
+
125
+ ```ts
126
+ dynamicBlocksPlugin({
127
+ enabled?: boolean // Disable without removing. Default: true
128
+ collections?: string[] // Collection slugs that get the DB Layout tab. Default: []
129
+ fieldName?: string // Name of the layout array field. Default: 'dbLayout'
130
+ tabLabel?: string // Label shown on the tab in the admin UI. Default: 'DB Layout'
131
+ })
55
132
  ```
56
133
 
57
- MongoDB users can skip this step.
134
+ The `--collections` flag is also supported in the init command:
135
+
136
+ ```bash
137
+ npx payload-block-builder init --collections=pages,posts
138
+ ```
58
139
 
59
- Visit `/block-builder` in your browser and you're in.
140
+ ---
60
141
 
61
142
  ## Usage
62
143
 
@@ -66,16 +147,16 @@ Visit `/block-builder` in your browser and you're in.
66
147
  2. Click "Add Block" and give it a name and slug.
67
148
  3. Drag fields from the panel on the right onto the canvas.
68
149
  4. Configure each field (label, name, required, options, etc.).
69
- 5. Hit Publish. The block is saved to your database and a version snapshot is created.
150
+ 5. Click Publish. The block schema is saved to your database and a version snapshot is created.
70
151
 
71
152
  ### Using blocks in a collection
72
153
 
73
- Any collection you listed in `collections` gets a new "DB Layout" tab in the Payload admin. Editors can:
154
+ Any collection listed in the `collections` option gets a new "DB Layout" tab in the Payload admin. Editors can:
74
155
 
75
- 1. Click "Add Row" to add a block.
156
+ 1. Click "Add Row" to add a block instance.
76
157
  2. Select a block definition and the version of its schema to use.
77
- 3. Fill in the fields. They render dynamically based on the selected schema.
78
- 4. Reorder, hide, or add anchor IDs to individual blocks.
158
+ 3. Fill in the fields they render dynamically based on the selected schema.
159
+ 4. Reorder, hide, or add anchor IDs to individual block instances.
79
160
  5. Save the document as normal.
80
161
 
81
162
  ### Reading block data on the frontend
@@ -91,29 +172,20 @@ for (const block of page.dbLayout) {
91
172
  }
92
173
  ```
93
174
 
94
- From there it's just a switch or a component map. Render each block type however you like.
175
+ Render each block type however you like — a switch statement or a component map both work well.
95
176
 
96
- ## Options
97
-
98
- ```ts
99
- dynamicBlocksPlugin({
100
- enabled?: boolean // disable without removing. Default: true
101
- collections?: string[] // which collection slugs get the DB Layout tab. Default: []
102
- fieldName?: string // name of the layout array field. Default: 'dbLayout'
103
- tabLabel?: string // label shown on the tab in the admin. Default: 'DB Layout'
104
- })
105
- ```
177
+ ---
106
178
 
107
179
  ## How it works
108
180
 
109
- - **Block definitions** are stored in a `block-definitions` collection. Each document is a named block type with a slug, labels, and a list of field definitions.
110
- - **Versions** are stored in a `block-definition-versions` collection. Every time you publish a block in the builder, a snapshot of its current schema is saved as a new version.
111
- - **Documents** in your opted-in collections store a reference to the exact block version they were built against, so updating a block's schema later won't break existing content.
112
- - **The DB Layout tab** is injected automatically on each collection you list. It renders a dynamic array field where editors pick a block and version, and the field data UI adjusts to match the selected schema.
113
- - **Four internal API endpoints** power the builder UI and the `BlockDataField` admin component. You don't need to call them yourself.
181
+ - **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.
182
+ - **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.
183
+ - **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.
184
+ - **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.
185
+ - **Four internal API endpoints** power the builder UI and the admin field components. You do not need to call them directly.
186
+
187
+ ---
114
188
 
115
- ## Requirements
189
+ ## License
116
190
 
117
- - Payload v3
118
- - Next.js 14+
119
- - Any Payload-supported database (PostgreSQL, MongoDB, SQLite)
191
+ MIT © [Nextbridge](https://nextbridge.com)
package/dist/bin/init.js CHANGED
@@ -42,7 +42,131 @@ function findAppDir() {
42
42
  }
43
43
  return null;
44
44
  }
45
+ function findPayloadConfig() {
46
+ const candidates = [
47
+ path.join(process.cwd(), "src", "payload.config.ts"),
48
+ path.join(process.cwd(), "payload.config.ts")
49
+ ];
50
+ for (const c of candidates) {
51
+ if (fs.existsSync(c)) return c;
52
+ }
53
+ return null;
54
+ }
55
+ function detectDbAdapter(content) {
56
+ if (/postgresAdapter|db-postgres/.test(content)) return "postgres";
57
+ if (/sqliteAdapter|db-sqlite/.test(content)) return "sqlite";
58
+ return "other";
59
+ }
60
+ function findClosingBracket(content, openPos) {
61
+ let depth = 0;
62
+ for (let i = openPos; i < content.length; i++) {
63
+ if (content[i] === "[") depth++;
64
+ else if (content[i] === "]") {
65
+ depth--;
66
+ if (depth === 0) return i;
67
+ }
68
+ }
69
+ return -1;
70
+ }
71
+ function addImport(content) {
72
+ const newImport = `import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'`;
73
+ const lastFromRegex = /^.*from\s+['"][^'"]+['"]\s*;?\s*$/gm;
74
+ let lastMatch = null;
75
+ let m;
76
+ while ((m = lastFromRegex.exec(content)) !== null) lastMatch = m;
77
+ if (!lastMatch) return newImport + "\n" + content;
78
+ const insertPos = lastMatch.index + lastMatch[0].length;
79
+ return content.slice(0, insertPos) + "\n" + newImport + content.slice(insertPos);
80
+ }
81
+ function insertIntoPluginsArray(content, collectionsArg) {
82
+ const pluginsMatch = /\bplugins\s*:\s*\[/.exec(content);
83
+ if (!pluginsMatch) return null;
84
+ const openPos = content.indexOf("[", pluginsMatch.index);
85
+ const closePos = findClosingBracket(content, openPos);
86
+ if (closePos === -1) return null;
87
+ const beforeClose = content.slice(0, closePos);
88
+ const prevNL = beforeClose.lastIndexOf("\n");
89
+ const closingIndent = beforeClose.slice(prevNL + 1).match(/^([ \t]*)/)?.[1] ?? " ";
90
+ const entryIndent = closingIndent + " ";
91
+ const newLine = `${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),
92
+ `;
93
+ return content.slice(0, prevNL + 1) + newLine + content.slice(prevNL + 1);
94
+ }
95
+ function injectPluginsBlock(content, collectionsArg) {
96
+ const collMatch = /\bcollections\s*:\s*\[/.exec(content);
97
+ if (!collMatch) return null;
98
+ const collOpen = content.indexOf("[", collMatch.index);
99
+ const collClose = findClosingBracket(content, collOpen);
100
+ if (collClose === -1) return null;
101
+ const afterCollLine = content.indexOf("\n", collClose);
102
+ if (afterCollLine === -1) return null;
103
+ const beforeColl = content.slice(0, collMatch.index);
104
+ const collLineStart = beforeColl.lastIndexOf("\n") + 1;
105
+ const outerIndent = beforeColl.slice(collLineStart).match(/^([ \t]*)/)?.[1] ?? " ";
106
+ const entryIndent = outerIndent + " ";
107
+ const pluginsBlock = `${outerIndent}plugins: [
108
+ ${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),
109
+ ${outerIndent}],`;
110
+ return content.slice(0, afterCollLine) + "\n" + pluginsBlock + content.slice(afterCollLine);
111
+ }
112
+ function modifyPayloadConfig(configPath, collectionsArg) {
113
+ let content = fs.readFileSync(configPath, "utf8");
114
+ if (content.includes("dynamicBlocksPlugin")) {
115
+ console.log(`Skipped: dynamicBlocksPlugin already present in ${configPath}`);
116
+ return;
117
+ }
118
+ content = addImport(content);
119
+ const noComments = content.replace(/\/\/[^\n]*/g, "");
120
+ const hasPluginsArray = /\bplugins\s*:\s*\[/.test(noComments);
121
+ const hasPluginsShorthand = /^\s*plugins\s*,/m.test(noComments);
122
+ if (hasPluginsShorthand && !hasPluginsArray) {
123
+ fs.writeFileSync(configPath, content, "utf8");
124
+ console.log(`Updated: ${configPath} (added import)`);
125
+ console.log(` Note: 'plugins' is imported from another file.`);
126
+ console.log(` Add dynamicBlocksPlugin({ collections: ['pages'] }) to that file manually.`);
127
+ return;
128
+ }
129
+ let result = null;
130
+ if (hasPluginsArray) {
131
+ result = insertIntoPluginsArray(content, collectionsArg);
132
+ } else {
133
+ result = injectPluginsBlock(content, collectionsArg);
134
+ }
135
+ if (result === null) {
136
+ fs.writeFileSync(configPath, content, "utf8");
137
+ console.log(`Updated: ${configPath} (added import only)`);
138
+ console.log(` Could not auto-detect plugins array. Add manually:`);
139
+ console.log(` plugins: [ dynamicBlocksPlugin({ collections: [${collectionsArg}] }) ]`);
140
+ return;
141
+ }
142
+ fs.writeFileSync(configPath, result, "utf8");
143
+ console.log(`Updated: ${configPath} (added dynamicBlocksPlugin)`);
144
+ }
145
+ function printNextSteps(dbAdapter) {
146
+ console.log("\n--- Next Steps ---");
147
+ console.log("1. Regenerate the Payload import map:");
148
+ console.log(" pnpm generate:importmap");
149
+ if (dbAdapter === "postgres") {
150
+ console.log("\n2. PostgreSQL detected. Start the dev server \u2014 Payload will auto-push schema:");
151
+ console.log(" pnpm dev");
152
+ console.log("\n Or if you prefer migrations:");
153
+ console.log(" pnpm payload migrate:create --name=add_block_builder");
154
+ console.log(" pnpm payload migrate");
155
+ } else if (dbAdapter === "sqlite") {
156
+ console.log("\n2. Start the dev server \u2014 Payload will auto-migrate SQLite:");
157
+ console.log(" pnpm dev");
158
+ } else {
159
+ console.log("\n2. Start the dev server:");
160
+ console.log(" pnpm dev");
161
+ }
162
+ console.log("\nThen visit: http://localhost:3000/block-builder");
163
+ console.log("------------------");
164
+ }
45
165
  function main() {
166
+ const args = process.argv.slice(2);
167
+ const collectionsFlag = args.find((a) => a.startsWith("--collections="));
168
+ const collectionsValue = collectionsFlag ? collectionsFlag.replace("--collections=", "").split(",").map((s) => s.trim()) : ["pages"];
169
+ const collectionsArg = collectionsValue.map((c) => `'${c}'`).join(", ");
46
170
  const appDir = findAppDir();
47
171
  if (!appDir) {
48
172
  console.error("Could not find app directory. Make sure you are in the root of a Next.js project.");
@@ -77,6 +201,16 @@ function main() {
77
201
  console.log(`Skipped: ${customScssPath} already has block-builder imports`);
78
202
  }
79
203
  }
80
- console.log("\nDone! Visit /block-builder in your browser.");
204
+ const configPath = findPayloadConfig();
205
+ if (!configPath) {
206
+ console.log("\nNote: payload.config.ts not found. Add the plugin manually:");
207
+ console.log(` import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'`);
208
+ console.log(` plugins: [ dynamicBlocksPlugin({ collections: [${collectionsArg}] }) ]`);
209
+ printNextSteps("other");
210
+ } else {
211
+ const dbAdapter = detectDbAdapter(fs.readFileSync(configPath, "utf8"));
212
+ modifyPayloadConfig(configPath, collectionsArg);
213
+ printNextSteps(dbAdapter);
214
+ }
81
215
  }
82
216
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextbridgehq/payload-block-builder",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Block Builder for Payload CMS",
5
5
  "keywords": ["payload", "payload-plugin", "cms", "block-builder", "dynamic-blocks"],
6
6
  "homepage": "https://github.com/nextbridgehq/block-builder",