@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.
- package/README.md +109 -37
- package/dist/bin/init.js +135 -1
- 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
|
-
|
|
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
|
-
|
|
40
|
+
pnpm generate:importmap
|
|
41
|
+
pnpm dev
|
|
20
42
|
```
|
|
21
43
|
|
|
22
|
-
|
|
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'
|
|
70
|
+
collections: ['pages'],
|
|
31
71
|
}),
|
|
32
72
|
],
|
|
33
73
|
})
|
|
34
74
|
```
|
|
35
75
|
|
|
36
|
-
3. Create
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
78
|
-
4. Reorder, hide, or add anchor IDs to individual
|
|
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
|
-
|
|
175
|
+
Render each block type however you like — a switch statement or a component map both work well.
|
|
95
176
|
|
|
96
|
-
|
|
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
|
|
110
|
-
- **Versions** are stored in a `block-definition-versions` collection. Every time you publish a block
|
|
111
|
-
- **Documents** in
|
|
112
|
-
- **The DB Layout tab** is injected automatically
|
|
113
|
-
- **Four internal API endpoints** power the builder UI and the
|
|
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
|
-
##
|
|
189
|
+
## License
|
|
116
190
|
|
|
117
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|