@nextbridgehq/payload-block-builder 0.1.6 → 0.1.8
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 +181 -35
- package/dist/bin/init.js +79 -25
- package/dist/client.cjs +118 -65
- package/dist/client.js +142 -75
- package/dist/index.cjs +32 -15
- package/dist/index.js +32 -15
- package/package.json +2 -2
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
|
|
|
@@ -11,15 +14,64 @@ A visual block builder plugin for Payload v3. Design your content blocks through
|
|
|
11
14
|
- **Evolving content schemas:** Roll out new block versions without breaking content that was built against older ones.
|
|
12
15
|
- **Headless frontends:** Fetch structured block data from the Payload API and render it with any framework.
|
|
13
16
|
|
|
17
|
+
## Database compatibility
|
|
18
|
+
|
|
19
|
+
Works with all Payload-supported databases — no direct SQL, no database-specific code:
|
|
20
|
+
|
|
21
|
+
| Database | Adapter |
|
|
22
|
+
|---|---|
|
|
23
|
+
| PostgreSQL / Supabase / Neon | `@payloadcms/db-postgres` |
|
|
24
|
+
| SQLite / Turso / LibSQL | `@payloadcms/db-sqlite` |
|
|
25
|
+
| MongoDB | `@payloadcms/db-mongodb` |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
14
29
|
## Quick start
|
|
15
30
|
|
|
16
|
-
|
|
31
|
+
### Option A — Automatic setup (recommended)
|
|
32
|
+
|
|
33
|
+
Install the package and run the init command from your project root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm add @nextbridgehq/payload-block-builder
|
|
37
|
+
# or: npm install @nextbridgehq/payload-block-builder
|
|
38
|
+
|
|
39
|
+
npx payload-block-builder init
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The init command automatically:
|
|
43
|
+
|
|
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
|
|
48
|
+
|
|
49
|
+
Then regenerate the import map and start your dev server:
|
|
17
50
|
|
|
18
51
|
```bash
|
|
19
|
-
|
|
52
|
+
pnpm generate:importmap
|
|
53
|
+
pnpm dev
|
|
20
54
|
```
|
|
21
55
|
|
|
22
|
-
|
|
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
|
+
---
|
|
65
|
+
|
|
66
|
+
### Option B — Manual setup
|
|
67
|
+
|
|
68
|
+
**1. Install:**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm add @nextbridgehq/payload-block-builder
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**2. Add the plugin to `payload.config.ts`:**
|
|
23
75
|
|
|
24
76
|
```ts
|
|
25
77
|
import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'
|
|
@@ -27,36 +79,85 @@ import { dynamicBlocksPlugin } from '@nextbridgehq/payload-block-builder'
|
|
|
27
79
|
export default buildConfig({
|
|
28
80
|
plugins: [
|
|
29
81
|
dynamicBlocksPlugin({
|
|
30
|
-
collections: ['pages'
|
|
82
|
+
collections: ['pages'],
|
|
31
83
|
}),
|
|
32
84
|
],
|
|
33
85
|
})
|
|
34
86
|
```
|
|
35
87
|
|
|
36
|
-
3. Create
|
|
88
|
+
**3. Create `src/app/block-builder/layout.tsx`:**
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import React from 'react'
|
|
92
|
+
import { headers } from 'next/headers'
|
|
93
|
+
import { redirect } from 'next/navigation'
|
|
94
|
+
import { getPayload } from 'payload'
|
|
95
|
+
import config from '@payload-config'
|
|
96
|
+
import '@nextbridgehq/payload-block-builder/builder.css'
|
|
97
|
+
|
|
98
|
+
export const metadata = { title: 'Block Builder' }
|
|
99
|
+
|
|
100
|
+
export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
101
|
+
const payload = await getPayload({ config })
|
|
102
|
+
const { user } = await payload.auth({ headers: await headers() })
|
|
103
|
+
if (!user) redirect('/admin/login')
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
|
|
108
|
+
{children}
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**4. Create `src/app/block-builder/page.tsx`:**
|
|
37
116
|
|
|
38
117
|
```tsx
|
|
39
|
-
// app/block-builder/page.tsx
|
|
40
118
|
'use client'
|
|
41
119
|
|
|
42
120
|
import { BuilderShell } from '@nextbridgehq/payload-block-builder/client'
|
|
43
|
-
import '@nextbridgehq/payload-block-builder/builder.css'
|
|
44
121
|
|
|
45
122
|
export default function BlockBuilderPage() {
|
|
46
123
|
return <BuilderShell />
|
|
47
124
|
}
|
|
48
125
|
```
|
|
49
126
|
|
|
50
|
-
|
|
127
|
+
**5. Add admin field styles to `src/app/(payload)/custom.scss`:**
|
|
128
|
+
|
|
129
|
+
```scss
|
|
130
|
+
@import '@nextbridgehq/payload-block-builder/block-data-field.css';
|
|
131
|
+
@import '@nextbridgehq/payload-block-builder/schema-builder-field.css';
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**6. Regenerate the import map and start the dev server:**
|
|
51
135
|
|
|
52
136
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
137
|
+
pnpm generate:importmap
|
|
138
|
+
pnpm dev
|
|
55
139
|
```
|
|
56
140
|
|
|
57
|
-
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Plugin options
|
|
58
144
|
|
|
59
|
-
|
|
145
|
+
```ts
|
|
146
|
+
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'
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The `--collections` flag is also supported in the init command:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx payload-block-builder init --collections=pages,posts
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
60
161
|
|
|
61
162
|
## Usage
|
|
62
163
|
|
|
@@ -66,16 +167,16 @@ Visit `/block-builder` in your browser and you're in.
|
|
|
66
167
|
2. Click "Add Block" and give it a name and slug.
|
|
67
168
|
3. Drag fields from the panel on the right onto the canvas.
|
|
68
169
|
4. Configure each field (label, name, required, options, etc.).
|
|
69
|
-
5.
|
|
170
|
+
5. Click Publish. The block schema is saved to your database and a version snapshot is created.
|
|
70
171
|
|
|
71
172
|
### Using blocks in a collection
|
|
72
173
|
|
|
73
|
-
Any collection
|
|
174
|
+
Any collection listed in the `collections` option gets a new "DB Layout" tab in the Payload admin. Editors can:
|
|
74
175
|
|
|
75
|
-
1. Click "Add Row" to add a block.
|
|
176
|
+
1. Click "Add Row" to add a block instance.
|
|
76
177
|
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
|
|
178
|
+
3. Fill in the fields — they render dynamically based on the selected schema.
|
|
179
|
+
4. Reorder, hide, or add anchor IDs to individual block instances.
|
|
79
180
|
5. Save the document as normal.
|
|
80
181
|
|
|
81
182
|
### Reading block data on the frontend
|
|
@@ -91,29 +192,74 @@ for (const block of page.dbLayout) {
|
|
|
91
192
|
}
|
|
92
193
|
```
|
|
93
194
|
|
|
94
|
-
|
|
195
|
+
Render each block type however you like — a switch statement or a component map both work well.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## How it works
|
|
200
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Supported field types
|
|
210
|
+
|
|
211
|
+
These field types are available in the block builder and render correctly in the admin field UI:
|
|
212
|
+
|
|
213
|
+
| Type | Description |
|
|
214
|
+
|---|---|
|
|
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 |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Using `dbLayoutField` directly
|
|
95
230
|
|
|
96
|
-
|
|
231
|
+
If you prefer not to use the plugin's `collections` option, you can add the layout tab manually to any collection:
|
|
97
232
|
|
|
98
233
|
```ts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
234
|
+
import { dbLayoutField } from '@nextbridgehq/payload-block-builder'
|
|
235
|
+
|
|
236
|
+
export const Pages: CollectionConfig = {
|
|
237
|
+
slug: 'pages',
|
|
238
|
+
fields: [
|
|
239
|
+
{
|
|
240
|
+
type: 'tabs',
|
|
241
|
+
tabs: [
|
|
242
|
+
{ label: 'Content', fields: [] },
|
|
243
|
+
dbLayoutField(), // fieldName='dbLayout', tab label='DB Layout'
|
|
244
|
+
dbLayoutField('heroBlocks', 'Hero'), // custom field name and tab label
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
105
249
|
```
|
|
106
250
|
|
|
107
|
-
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## CSS imports reference
|
|
254
|
+
|
|
255
|
+
| Import path | Purpose |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `@nextbridgehq/payload-block-builder/builder.css` | Block Builder UI page styles |
|
|
258
|
+
| `@nextbridgehq/payload-block-builder/block-data-field.css` | DB Layout field styles in admin |
|
|
259
|
+
| `@nextbridgehq/payload-block-builder/schema-builder-field.css` | Schema Builder field styles in admin |
|
|
108
260
|
|
|
109
|
-
|
|
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.
|
|
261
|
+
---
|
|
114
262
|
|
|
115
|
-
##
|
|
263
|
+
## License
|
|
116
264
|
|
|
117
|
-
|
|
118
|
-
- Next.js 14+
|
|
119
|
-
- Any Payload-supported database (PostgreSQL, MongoDB, SQLite)
|
|
265
|
+
MIT © [Nextbridge](https://nextbridge.com)
|
package/dist/bin/init.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/bin/init.ts
|
|
5
4
|
import fs from "fs";
|
|
@@ -13,13 +12,21 @@ export default function BlockBuilderPage() {
|
|
|
13
12
|
}
|
|
14
13
|
`;
|
|
15
14
|
var LAYOUT_CONTENT = `import React from 'react'
|
|
15
|
+
import { headers } from 'next/headers'
|
|
16
|
+
import { redirect } from 'next/navigation'
|
|
17
|
+
import { getPayload } from 'payload'
|
|
18
|
+
import config from '@payload-config'
|
|
16
19
|
import '@nextbridgehq/payload-block-builder/builder.css'
|
|
17
20
|
|
|
18
21
|
export const metadata = {
|
|
19
22
|
title: 'Block Builder',
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
export default function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
25
|
+
export default async function BlockBuilderLayout({ children }: { children: React.ReactNode }) {
|
|
26
|
+
const payload = await getPayload({ config })
|
|
27
|
+
const { user } = await payload.auth({ headers: await headers() })
|
|
28
|
+
if (!user) redirect('/admin/login')
|
|
29
|
+
|
|
23
30
|
return (
|
|
24
31
|
<html lang="en">
|
|
25
32
|
<body style={{ margin: 0, padding: 0, height: '100vh', overflow: 'hidden' }}>
|
|
@@ -76,7 +83,7 @@ function addImport(content) {
|
|
|
76
83
|
while ((m = lastFromRegex.exec(content)) !== null) lastMatch = m;
|
|
77
84
|
if (!lastMatch) return newImport + "\n" + content;
|
|
78
85
|
const insertPos = lastMatch.index + lastMatch[0].length;
|
|
79
|
-
return content.slice(0, insertPos) + "\n" + newImport + content.slice(insertPos);
|
|
86
|
+
return content.slice(0, insertPos) + "\n" + newImport + "\n" + content.slice(insertPos);
|
|
80
87
|
}
|
|
81
88
|
function insertIntoPluginsArray(content, collectionsArg) {
|
|
82
89
|
const pluginsMatch = /\bplugins\s*:\s*\[/.exec(content);
|
|
@@ -84,13 +91,19 @@ function insertIntoPluginsArray(content, collectionsArg) {
|
|
|
84
91
|
const openPos = content.indexOf("[", pluginsMatch.index);
|
|
85
92
|
const closePos = findClosingBracket(content, openPos);
|
|
86
93
|
if (closePos === -1) return null;
|
|
94
|
+
const beforePlugins = content.slice(0, pluginsMatch.index);
|
|
95
|
+
const pluginsLineStart = beforePlugins.lastIndexOf("\n") + 1;
|
|
96
|
+
const outerIndent = content.slice(pluginsLineStart, pluginsMatch.index).match(/^([ \t]*)/)?.[1] ?? " ";
|
|
97
|
+
const entryIndent = outerIndent + " ";
|
|
87
98
|
const beforeClose = content.slice(0, closePos);
|
|
88
99
|
const prevNL = beforeClose.lastIndexOf("\n");
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
const isSingleLine = prevNL < openPos;
|
|
101
|
+
const newEntry = `${entryIndent}dynamicBlocksPlugin({ collections: [${collectionsArg}] }),`;
|
|
102
|
+
if (isSingleLine) {
|
|
103
|
+
return content.slice(0, openPos + 1) + "\n" + newEntry + "\n" + outerIndent + content.slice(closePos);
|
|
104
|
+
} else {
|
|
105
|
+
return content.slice(0, prevNL + 1) + newEntry + "\n" + content.slice(prevNL + 1);
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
108
|
function injectPluginsBlock(content, collectionsArg) {
|
|
96
109
|
const collMatch = /\bcollections\s*:\s*\[/.exec(content);
|
|
@@ -110,7 +123,13 @@ ${outerIndent}],`;
|
|
|
110
123
|
return content.slice(0, afterCollLine) + "\n" + pluginsBlock + content.slice(afterCollLine);
|
|
111
124
|
}
|
|
112
125
|
function modifyPayloadConfig(configPath, collectionsArg) {
|
|
113
|
-
let content
|
|
126
|
+
let content;
|
|
127
|
+
try {
|
|
128
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`Error: Could not read ${configPath}: ${err.message}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
114
133
|
if (content.includes("dynamicBlocksPlugin")) {
|
|
115
134
|
console.log(`Skipped: dynamicBlocksPlugin already present in ${configPath}`);
|
|
116
135
|
return;
|
|
@@ -119,8 +138,16 @@ function modifyPayloadConfig(configPath, collectionsArg) {
|
|
|
119
138
|
const noComments = content.replace(/\/\/[^\n]*/g, "");
|
|
120
139
|
const hasPluginsArray = /\bplugins\s*:\s*\[/.test(noComments);
|
|
121
140
|
const hasPluginsShorthand = /^\s*plugins\s*,/m.test(noComments);
|
|
141
|
+
function writeConfig(data) {
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(configPath, data, "utf8");
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Error: Could not write ${configPath}: ${err.message}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
122
149
|
if (hasPluginsShorthand && !hasPluginsArray) {
|
|
123
|
-
|
|
150
|
+
writeConfig(content);
|
|
124
151
|
console.log(`Updated: ${configPath} (added import)`);
|
|
125
152
|
console.log(` Note: 'plugins' is imported from another file.`);
|
|
126
153
|
console.log(` Add dynamicBlocksPlugin({ collections: ['pages'] }) to that file manually.`);
|
|
@@ -133,13 +160,13 @@ function modifyPayloadConfig(configPath, collectionsArg) {
|
|
|
133
160
|
result = injectPluginsBlock(content, collectionsArg);
|
|
134
161
|
}
|
|
135
162
|
if (result === null) {
|
|
136
|
-
|
|
163
|
+
writeConfig(content);
|
|
137
164
|
console.log(`Updated: ${configPath} (added import only)`);
|
|
138
165
|
console.log(` Could not auto-detect plugins array. Add manually:`);
|
|
139
166
|
console.log(` plugins: [ dynamicBlocksPlugin({ collections: [${collectionsArg}] }) ]`);
|
|
140
167
|
return;
|
|
141
168
|
}
|
|
142
|
-
|
|
169
|
+
writeConfig(result);
|
|
143
170
|
console.log(`Updated: ${configPath} (added dynamicBlocksPlugin)`);
|
|
144
171
|
}
|
|
145
172
|
function printNextSteps(dbAdapter) {
|
|
@@ -165,7 +192,14 @@ function printNextSteps(dbAdapter) {
|
|
|
165
192
|
function main() {
|
|
166
193
|
const args = process.argv.slice(2);
|
|
167
194
|
const collectionsFlag = args.find((a) => a.startsWith("--collections="));
|
|
168
|
-
const
|
|
195
|
+
const rawCollections = collectionsFlag ? collectionsFlag.replace("--collections=", "").split(",").map((s) => s.trim()) : ["pages"];
|
|
196
|
+
const invalidSlugs = rawCollections.filter((c) => !/^[a-z0-9_-]+$/i.test(c));
|
|
197
|
+
if (invalidSlugs.length > 0) {
|
|
198
|
+
console.error(`Error: Invalid collection slug(s): ${invalidSlugs.join(", ")}`);
|
|
199
|
+
console.error("Collection slugs may only contain letters, numbers, hyphens, and underscores.");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const collectionsValue = rawCollections;
|
|
169
203
|
const collectionsArg = collectionsValue.map((c) => `'${c}'`).join(", ");
|
|
170
204
|
const appDir = findAppDir();
|
|
171
205
|
if (!appDir) {
|
|
@@ -173,32 +207,52 @@ function main() {
|
|
|
173
207
|
process.exit(1);
|
|
174
208
|
}
|
|
175
209
|
const builderDir = path.join(appDir, "block-builder");
|
|
176
|
-
|
|
177
|
-
fs.
|
|
210
|
+
try {
|
|
211
|
+
if (!fs.existsSync(builderDir)) {
|
|
212
|
+
fs.mkdirSync(builderDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`Error: Could not create directory ${builderDir}: ${err.message}`);
|
|
216
|
+
process.exit(1);
|
|
178
217
|
}
|
|
179
218
|
const pagePath = path.join(builderDir, "page.tsx");
|
|
180
219
|
const layoutPath = path.join(builderDir, "layout.tsx");
|
|
181
220
|
if (fs.existsSync(pagePath)) {
|
|
182
221
|
console.log(`Skipped: ${pagePath} already exists`);
|
|
183
222
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(pagePath, PAGE_CONTENT);
|
|
225
|
+
console.log(`Created: ${pagePath}`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error(`Error: Could not write ${pagePath}: ${err.message}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
186
230
|
}
|
|
187
231
|
if (fs.existsSync(layoutPath)) {
|
|
188
232
|
console.log(`Skipped: ${layoutPath} already exists`);
|
|
189
233
|
} else {
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
try {
|
|
235
|
+
fs.writeFileSync(layoutPath, LAYOUT_CONTENT);
|
|
236
|
+
console.log(`Created: ${layoutPath}`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`Error: Could not write ${layoutPath}: ${err.message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
192
241
|
}
|
|
193
242
|
const payloadRouteDir = path.join(appDir, "(payload)");
|
|
194
243
|
const customScssPath = path.join(payloadRouteDir, "custom.scss");
|
|
195
244
|
if (fs.existsSync(customScssPath)) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
245
|
+
try {
|
|
246
|
+
const existing = fs.readFileSync(customScssPath, "utf8");
|
|
247
|
+
if (!existing.includes("@nextbridgehq/payload-block-builder")) {
|
|
248
|
+
fs.appendFileSync(customScssPath, "\n" + CUSTOM_SCSS_IMPORTS);
|
|
249
|
+
console.log(`Updated: ${customScssPath} (added admin field styles)`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(`Skipped: ${customScssPath} already has block-builder imports`);
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`Error: Could not update ${customScssPath}: ${err.message}`);
|
|
255
|
+
process.exit(1);
|
|
202
256
|
}
|
|
203
257
|
}
|
|
204
258
|
const configPath = findPayloadConfig();
|