@brightsideoy/kama 0.1.0-alpha.100 → 0.1.0-alpha.107

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 (42) hide show
  1. package/README.md +174 -0
  2. package/bin/cli.js +321 -118
  3. package/{defaults → content}/definitions/node-layouts/two-column.yml +1 -1
  4. package/{defaults → content}/definitions/nodes/person.yaml +2 -0
  5. package/dist/assets/index-D12geIgG.js +1394 -0
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +1420 -1179
  8. package/documentation/SCHEMAS.md +182 -0
  9. package/package.json +4 -3
  10. package/scripts/bundle-definitions.js +44 -4
  11. package/dist/assets/index-u699KJW2.js +0 -1392
  12. /package/{defaults → content}/definitions/block-layouts/three-column.yaml +0 -0
  13. /package/{defaults → content}/definitions/block-layouts/two-column.yaml +0 -0
  14. /package/{defaults → content}/definitions/blocks/button.yaml +0 -0
  15. /package/{defaults → content}/definitions/blocks/header.yaml +0 -0
  16. /package/{defaults → content}/definitions/blocks/header_image.yaml +0 -0
  17. /package/{defaults → content}/definitions/blocks/hero.yaml +0 -0
  18. /package/{defaults → content}/definitions/blocks/image_card.yaml +0 -0
  19. /package/{defaults → content}/definitions/blocks/single_image.yaml +0 -0
  20. /package/{defaults → content}/definitions/blocks/text.yaml +0 -0
  21. /package/{defaults → content}/definitions/channels/ecommerce.yaml +0 -0
  22. /package/{defaults → content}/definitions/channels/website.yaml +0 -0
  23. /package/{defaults → content}/definitions/field-sets/access.yaml +0 -0
  24. /package/{defaults → content}/definitions/field-sets/channel-base.yaml +0 -0
  25. /package/{defaults → content}/definitions/field-sets/comments.yaml +0 -0
  26. /package/{defaults → content}/definitions/field-sets/content-block.yaml +0 -0
  27. /package/{defaults → content}/definitions/field-sets/page-base.yaml +0 -0
  28. /package/{defaults → content}/definitions/field-sets/page-meta.yaml +0 -0
  29. /package/{defaults → content}/definitions/field-sets/page-social.yaml +0 -0
  30. /package/{defaults → content}/definitions/field-sets/tree-base.yaml +0 -0
  31. /package/{defaults → content}/definitions/field-sets/visibility.yaml +0 -0
  32. /package/{defaults → content}/definitions/node-layouts/landing-page.yml +0 -0
  33. /package/{defaults → content}/definitions/node-layouts/single-column.yml +0 -0
  34. /package/{defaults → content}/definitions/node-layouts/three-column.yml +0 -0
  35. /package/{defaults → content}/definitions/nodes/address.yaml +0 -0
  36. /package/{defaults → content}/definitions/nodes/article.yaml +0 -0
  37. /package/{defaults → content}/definitions/nodes/event.yaml +0 -0
  38. /package/{defaults → content}/definitions/nodes/external-link.yaml +0 -0
  39. /package/{defaults → content}/definitions/nodes/page.yaml +0 -0
  40. /package/{defaults → content}/definitions/nodes/product.yaml +0 -0
  41. /package/{defaults → content}/definitions/trees/navigation.yaml +0 -0
  42. /package/{defaults → content}/definitions/trees/social-links.yaml +0 -0
package/bin/cli.js CHANGED
@@ -32,8 +32,25 @@ async function main() {
32
32
  */
33
33
  async function createFullWorkspace() {
34
34
  const projName = (await rl.question('Project folder name [kama-workspace]: ')) || 'kama-workspace';
35
- const cmsDomain = (await rl.question('CMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
36
- const siteDomain = (await rl.question('First Website Domain (e.g., kama.t3cloud.eu): ')) || 'kama.t3cloud.eu';
35
+
36
+ console.log('\n🌍 Where do you plan to deploy KAMA?');
37
+ console.log(' 1) Local Development Only (SQLite / Testing)');
38
+ console.log(' 2) Cloudflare Edge (Workers / D1 / R2)');
39
+ const envChoice = await rl.question('\nChoice [1/2]: ');
40
+ const isCloudflare = envChoice.trim() === '2';
41
+
42
+ let cmsDomain = 'localhost:8787';
43
+ let siteDomain = 'localhost:4321';
44
+
45
+ // Only ask for domains if deploying to Cloudflare Edge
46
+ if (isCloudflare) {
47
+ cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
48
+ siteDomain = (await rl.question('First Website Domain (e.g., kama.t3cloud.eu): ')) || 'kama.t3cloud.eu';
49
+ }
50
+
51
+ // Standardize scheme for localhost vs custom edge domains
52
+ const cmsProtocol = isCloudflare ? 'https://' : 'http://';
53
+ const cmsFullUrl = `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}`;
37
54
 
38
55
  const rootDir = join(process.cwd(), projName);
39
56
 
@@ -45,56 +62,81 @@ async function createFullWorkspace() {
45
62
  console.log(`\n📁 Creating workspace folder at ./${projName}...`);
46
63
  mkdirSync(rootDir, { recursive: true });
47
64
 
48
- // 🎯 Scaffold schemas directory for project-level custom YAML definitions
49
- mkdirSync(join(rootDir, 'schemas', 'nodes'), { recursive: true });
50
- mkdirSync(join(rootDir, 'schemas', 'blocks'), { recursive: true });
65
+ // 🎯 Scaffold full schema directory structure
66
+ const schemaCategories = [
67
+ 'block-layouts',
68
+ 'blocks',
69
+ 'channels',
70
+ 'field-sets',
71
+ 'node-layouts',
72
+ 'nodes',
73
+ 'trees'
74
+ ];
75
+
76
+ for (const category of schemaCategories) {
77
+ mkdirSync(join(rootDir, 'content', 'definitions', category), { recursive: true });
78
+ }
79
+
80
+ // 🎯 Populate starter example YAML files
81
+ scaffoldSampleSchemas(rootDir);
51
82
 
52
83
  // --- AUTO D1 CREATION MAGIC ---
53
- let d1DatabaseId = 'REPLACE_WITH_YOUR_D1_ID';
84
+ // Use a zeroed UUID for local testing so Wrangler's schema validator doesn't crash
85
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
54
86
  let isAuthenticated = false;
55
87
 
56
- console.log(`\n🔑 Checking Cloudflare authentication...`);
57
- try {
58
- execSync('npx wrangler whoami', { stdio: 'pipe' });
59
- isAuthenticated = true;
60
- console.log(`✅ Logged in to Cloudflare.`);
61
- } catch (error) {
62
- console.log(`⚠️ You are not logged in to Cloudflare.`);
63
- console.log(`🌐 Launching browser for Cloudflare login...`);
88
+ if (isCloudflare) {
89
+ console.log(`\n🔑 Checking Cloudflare authentication...`);
64
90
  try {
65
- execSync('npx wrangler login', { stdio: 'inherit' });
91
+ execSync('npx wrangler whoami', { stdio: 'pipe' });
66
92
  isAuthenticated = true;
67
- console.log(`✅ Successfully logged in!`);
68
- } catch (loginError) {
69
- console.log(`❌ Login failed or was cancelled.`);
93
+ console.log(`✅ Logged in to Cloudflare.`);
94
+ } catch (error) {
95
+ console.log(`⚠️ You are not logged in to Cloudflare.`);
96
+ console.log(`🌐 Launching browser for Cloudflare login...`);
97
+ try {
98
+ execSync('npx wrangler login', { stdio: 'inherit' });
99
+ isAuthenticated = true;
100
+ console.log(`✅ Successfully logged in!`);
101
+ } catch (loginError) {
102
+ console.log(`❌ Login failed or was cancelled.`);
103
+ }
70
104
  }
71
- }
72
-
73
- if (isAuthenticated) {
74
- try {
75
- console.log(`\n🗄️ Checking existing Cloudflare D1 databases...`);
76
- const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: 'pipe' });
77
105
 
78
- const jsonStart = listOutput.indexOf('[');
79
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
80
- let databases = [];
106
+ if (isAuthenticated) {
107
+ try {
108
+ console.log(`\n🗄️ Checking existing Cloudflare D1 databases...`);
109
+ const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: 'pipe' });
81
110
 
82
- if (jsonStart !== -1 && jsonEnd !== -1) {
83
- databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
84
- }
85
-
86
- const existingDb = databases.find(db => db.name === dbName);
111
+ const jsonStart = listOutput.indexOf('[');
112
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
113
+ let databases = [];
87
114
 
88
- if (existingDb) {
89
- console.log(`⚠️ Database '${dbName}' already exists on your account.`);
90
- const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
115
+ if (jsonStart !== -1 && jsonEnd !== -1) {
116
+ databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
117
+ }
91
118
 
92
- if (useExisting.toLowerCase() !== 'n') {
93
- d1DatabaseId = existingDb.uuid;
94
- console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
119
+ const existingDb = databases.find(db => db.name === dbName);
120
+
121
+ if (existingDb) {
122
+ console.log(`⚠️ Database '${dbName}' already exists on your account.`);
123
+ const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
124
+
125
+ if (useExisting.toLowerCase() !== 'n') {
126
+ d1DatabaseId = existingDb.uuid;
127
+ console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
128
+ } else {
129
+ dbName = (await rl.question(`Enter a new database name: `)).trim();
130
+ console.log(`\n🗄️ Creating new database '${dbName}'...`);
131
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
132
+ const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
133
+ if (match) {
134
+ d1DatabaseId = match[0];
135
+ console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
136
+ }
137
+ }
95
138
  } else {
96
- dbName = (await rl.question(`Enter a new database name: `)).trim();
97
- console.log(`\n🗄️ Creating new database '${dbName}'...`);
139
+ console.log(`\n🗄️ Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
98
140
  const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
99
141
  const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
100
142
  if (match) {
@@ -102,20 +144,14 @@ async function createFullWorkspace() {
102
144
  console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
103
145
  }
104
146
  }
105
- } else {
106
- console.log(`\n🗄️ Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
107
- const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
108
- const match = d1Output.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
109
- if (match) {
110
- d1DatabaseId = match[0];
111
- console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
112
- }
147
+ } catch (error) {
148
+ console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
113
149
  }
114
- } catch (error) {
115
- console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
150
+ } else {
151
+ console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
116
152
  }
117
153
  } else {
118
- console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
154
+ console.log(`\n💻 Local mode selected. Skipping Cloudflare authentication and domain setup...`);
119
155
  }
120
156
  // ------------------------------
121
157
 
@@ -143,9 +179,32 @@ async function createFullWorkspace() {
143
179
  const cmsDir = join(rootDir, 'cms-worker');
144
180
  mkdirSync(cmsDir, { recursive: true });
145
181
 
182
+ // 🎯 Properly wrap and inject BUNDLED_DEFINITIONS directly into fetch env
183
+ const cmsWorkerIndexContent = `import kamaWorker from '@brightsideoy/kama';
184
+ import * as bundledModule from '../node_modules/@brightsideoy/kama/dist/bundled-definitions.js';
185
+
186
+ // Resolve BUNDLED_DEFINITIONS across all module resolution formats
187
+ const BUNDLED_DEFINITIONS =
188
+ bundledModule.BUNDLED_DEFINITIONS ||
189
+ bundledModule.default?.BUNDLED_DEFINITIONS ||
190
+ bundledModule.default ||
191
+ bundledModule;
192
+
193
+ export default {
194
+ async fetch(request, env, ctx) {
195
+ const customEnv = {
196
+ ...env,
197
+ BUNDLED_DEFINITIONS,
198
+ };
199
+
200
+ return kamaWorker.fetch(request, customEnv, ctx);
201
+ },
202
+ };
203
+ `;
204
+
146
205
  writeFileSync(
147
206
  join(cmsDir, 'index.js'),
148
- `export { default } from '@brightsideoy/kama';\n`
207
+ cmsWorkerIndexContent
149
208
  );
150
209
 
151
210
  writeFileSync(
@@ -171,59 +230,70 @@ async function createFullWorkspace() {
171
230
  }, null, 2)
172
231
  );
173
232
 
174
- // 🎯 Fixed CMS wrangler.json (Added PUBLIC_FRONTEND_URL & CLOUDFLARE_ZONE_ID to public vars)
233
+ // 🎯 Fixed CMS wrangler.json
234
+ const cmsWranglerConfig = {
235
+ $schema: '../node_modules/wrangler/config-schema.json',
236
+ name: workerName,
237
+ main: 'index.js',
238
+ compatibility_date: '2026-08-01',
239
+ compatibility_flags: ['nodejs_compat'],
240
+ assets: {
241
+ directory: '../node_modules/@brightsideoy/kama/dist',
242
+ binding: 'ASSETS'
243
+ },
244
+ images: {
245
+ binding: 'IMAGES'
246
+ },
247
+ vars: {
248
+ CDN_DOMAIN: cmsDomain,
249
+ PUBLIC_FRONTEND_URL: isCloudflare ? `https://${siteDomain}` : 'http://localhost:4321',
250
+ CLOUDFLARE_ZONE_ID: isCloudflare ? "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID" : "local-zone-not-needed"
251
+ },
252
+ d1_databases: [{
253
+ binding: 'DB',
254
+ database_name: dbName,
255
+ database_id: d1DatabaseId,
256
+ }],
257
+ r2_buckets: [{
258
+ binding: 'MEDIA_BUCKET',
259
+ bucket_name: bucketName,
260
+ }],
261
+ };
262
+
263
+ if (isCloudflare) {
264
+ cmsWranglerConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
265
+ }
266
+
175
267
  writeFileSync(
176
268
  join(cmsDir, 'wrangler.json'),
177
- JSON.stringify({
178
- $schema: '../node_modules/wrangler/config-schema.json',
179
- name: workerName,
180
- main: 'index.js',
181
- compatibility_date: '2026-08-01',
182
- compatibility_flags: ['nodejs_compat'],
183
- assets: {
184
- directory: '../node_modules/@brightsideoy/kama/dist',
185
- binding: 'ASSETS'
186
- },
187
- images: {
188
- binding: 'IMAGES'
189
- },
190
- routes: [{ pattern: cmsDomain, custom_domain: true }],
191
- vars: {
192
- CDN_DOMAIN: cmsDomain,
193
- PUBLIC_FRONTEND_URL: `https://${siteDomain}`,
194
- CLOUDFLARE_ZONE_ID: "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID"
195
- },
196
- d1_databases: [{
197
- binding: 'DB',
198
- database_name: dbName,
199
- database_id: d1DatabaseId,
200
- }],
201
- r2_buckets: [{
202
- binding: 'MEDIA_BUCKET',
203
- bucket_name: bucketName,
204
- }],
205
- }, null, 2)
269
+ JSON.stringify(cmsWranglerConfig, null, 2)
206
270
  );
207
271
 
208
- // 🎯 Fixed .dev.vars (Only contains API token for local dev, not public Zone ID)
272
+ // 🎯 Fixed .dev.vars
209
273
  writeFileSync(
210
274
  join(cmsDir, '.dev.vars'),
211
- `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n`
275
+ `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=${isCloudflare ? 'cfut_YOUR_CUSTOM_PURGE_TOKEN' : 'local_token'}\n`
212
276
  );
213
277
 
214
278
  // 🎯 3. Setup First Astro Site tied to target workerName
215
- const siteFolderName = siteDomain.split('.')[0];
216
- await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsDomain, workerName);
279
+ const siteFolderName = isCloudflare ? siteDomain.split('.')[0] : 'web';
280
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare);
217
281
 
218
282
  console.log(`\n✨ Workspace ready! Next steps:`);
219
283
  console.log(` 1. cd ${projName}`);
220
284
  console.log(` 2. npm install`);
221
- if (d1DatabaseId === 'REPLACE_WITH_YOUR_D1_ID') {
222
- console.log(` 3. Run 'npx wrangler login' and create a D1 database.`);
285
+
286
+ if (isCloudflare) {
287
+ if (d1DatabaseId === '00000000-0000-0000-0000-000000000000' || d1DatabaseId === 'REPLACE_WITH_YOUR_D1_ID') {
288
+ console.log(` 3. Run 'npx wrangler login' and create a D1 database manually.`);
289
+ }
290
+ console.log(` 4. Update CLOUDFLARE_ZONE_ID in cms-worker/wrangler.json with your actual Zone ID`);
291
+ console.log(` 5. Securely store your purge token: cd cms-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
292
+ console.log(` 6. Run 'npm run dev:cms' from the workspace root to start the backend.`);
293
+ } else {
294
+ console.log(` 3. Run 'npm run dev:cms' from the workspace root to start the local SQLite backend.`);
295
+ console.log(`\n💡 Note: You can deploy to Cloudflare later by updating 'cms-worker/wrangler.json' with real IDs.`);
223
296
  }
224
- console.log(` 4. Update CLOUDFLARE_ZONE_ID in cms-worker/wrangler.json with your actual Zone ID`);
225
- console.log(` 5. Securely store your purge token: cd cms-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
226
- console.log(` 6. Run 'npm run dev:cms' from the workspace root to start the backend.`);
227
297
  }
228
298
 
229
299
  /**
@@ -246,19 +316,146 @@ async function createNewSiteOnly() {
246
316
  } catch (e) { }
247
317
  }
248
318
 
319
+ console.log('\n🌍 Deployment Mode:');
320
+ console.log(' 1) Local Development');
321
+ console.log(' 2) Cloudflare Edge');
322
+ const envChoice = await rl.question('\nChoice [1/2]: ');
323
+ const isCloudflare = envChoice.trim() === '2';
324
+
249
325
  const siteFolder = await rl.question('Site folder name (e.g., my-new-site): ');
250
- const siteDomain = await rl.question('Website Domain (e.g., brand.com): ');
251
- const cmsDomain = (await rl.question('CMS API Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
252
326
 
253
- await generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain.replace(/^https?:\/\//, ''), targetWorkerName);
327
+ let siteDomain = 'localhost:4321';
328
+ let cmsFullUrl = 'http://localhost:8787';
329
+
330
+ if (isCloudflare) {
331
+ siteDomain = await rl.question('Website Domain (e.g., brand.com): ');
332
+ const cmsDomainInput = (await rl.question('CMS API Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
333
+ cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
334
+ }
335
+
336
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
254
337
 
255
338
  console.log(`\n✨ Site created in sites/${siteFolder}! Run 'npm install' at workspace root.`);
256
339
  }
257
340
 
341
+ /**
342
+ * Helper: Scaffolds initial starter schemas for all category folders
343
+ */
344
+ function scaffoldSampleSchemas(rootDir) {
345
+ const defsDir = join(rootDir, 'content', 'definitions');
346
+
347
+ // 1. Channels
348
+ writeFileSync(join(defsDir, 'channels', 'website.yaml'), `id: website
349
+ label: Main Website
350
+ defaultLanguage: en
351
+ languages:
352
+ - en
353
+ - fi
354
+ `);
355
+
356
+ // 2. Nodes
357
+ writeFileSync(join(defsDir, 'nodes', 'page.yaml'), `id: page
358
+ label: Page
359
+ inTree: true
360
+ inVault: false
361
+ hasCanvas: true
362
+ tabs:
363
+ settings:
364
+ label: Settings
365
+ fields:
366
+ title:
367
+ label: Page Title
368
+ type: text
369
+ required: true
370
+ slug:
371
+ label: URL Slug
372
+ type: slug
373
+ linkedTo: title
374
+ `);
375
+
376
+ writeFileSync(join(defsDir, 'nodes', 'article.yaml'), `id: article
377
+ label: Article
378
+ inTree: false
379
+ inVault: true
380
+ hasCanvas: true
381
+ tabs:
382
+ settings:
383
+ label: Settings
384
+ fields:
385
+ title:
386
+ label: Title
387
+ type: text
388
+ required: true
389
+ slug:
390
+ label: Slug
391
+ type: slug
392
+ linkedTo: title
393
+ `);
394
+
395
+ // 3. Blocks
396
+ writeFileSync(join(defsDir, 'blocks', 'hero.yaml'), `id: hero
397
+ label: Hero Section
398
+ fields:
399
+ heading:
400
+ label: Heading
401
+ type: text
402
+ required: true
403
+ subheading:
404
+ label: Subheading
405
+ type: text
406
+ `);
407
+
408
+ writeFileSync(join(defsDir, 'blocks', 'text.yaml'), `id: text
409
+ label: Text Block
410
+ fields:
411
+ content:
412
+ label: Content
413
+ type: richtext
414
+ `);
415
+
416
+ // 4. Block Layouts
417
+ writeFileSync(join(defsDir, 'block-layouts', 'two-column.yaml'), `id: two-column
418
+ label: Two Column Layout
419
+ regions:
420
+ left:
421
+ label: Left Column
422
+ right:
423
+ label: Right Column
424
+ `);
425
+
426
+ // 5. Node Layouts
427
+ writeFileSync(join(defsDir, 'node-layouts', 'single-column.yml'), `id: single-column
428
+ label: Single Column Page
429
+ regions:
430
+ main:
431
+ label: Main Content Area
432
+ `);
433
+
434
+ // 6. Field Sets
435
+ writeFileSync(join(defsDir, 'field-sets', 'page-base.yaml'), `id: page-base
436
+ label: Base Page Fields
437
+ fields:
438
+ metaTitle:
439
+ label: Meta Title
440
+ type: text
441
+ metaDescription:
442
+ label: Meta Description
443
+ type: text
444
+ `);
445
+
446
+ // 7. Trees
447
+ writeFileSync(join(defsDir, 'trees', 'navigation.yaml'), `id: navigation
448
+ label: Main Navigation Tree
449
+ maxDepth: 3
450
+ allowedNodeTypes:
451
+ - page
452
+ `);
453
+ }
454
+
258
455
  /**
259
456
  * Helper: Scaffolds the Astro site with middleware, wrangler.json, & slug page
260
457
  */
261
- async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, workerName = 'kama-worker') {
458
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false) {
262
459
  console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
263
460
 
264
461
  const siteDir = join(rootDir, 'sites', siteFolder);
@@ -289,30 +486,36 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, wor
289
486
  }, null, 2)
290
487
  );
291
488
 
292
- // 🎯 Fixed Astro wrangler.json: Added `run_worker_first: true` to prevent media routing 404s on Edge
489
+ const siteWranglerConfig = {
490
+ $schema: 'node_modules/wrangler/config-schema.json',
491
+ name: siteFolder,
492
+ compatibility_date: '2026-08-01',
493
+ compatibility_flags: ['nodejs_compat'],
494
+ assets: {
495
+ directory: './dist/client',
496
+ binding: 'ASSETS',
497
+ html_handling: 'drop-trailing-slash',
498
+ not_found_handling: '404-page',
499
+ run_worker_first: true
500
+ },
501
+ services: [{ binding: 'CMS_WORKER', service: workerName }],
502
+ vars: {
503
+ PUBLIC_CMS_URL: cmsFullUrl,
504
+ PUBLIC_API_URL: cmsFullUrl,
505
+ },
506
+ };
507
+
508
+ if (isCloudflare) {
509
+ siteWranglerConfig.routes = [{ pattern: siteDomain, custom_domain: true }];
510
+ }
511
+
293
512
  writeFileSync(
294
513
  join(siteDir, 'wrangler.json'),
295
- JSON.stringify({
296
- $schema: 'node_modules/wrangler/config-schema.json',
297
- name: siteFolder,
298
- compatibility_date: '2026-08-01',
299
- compatibility_flags: ['nodejs_compat'],
300
- assets: {
301
- directory: './dist/client',
302
- binding: 'ASSETS',
303
- html_handling: 'drop-trailing-slash',
304
- not_found_handling: '404-page',
305
- run_worker_first: true
306
- },
307
- services: [{ binding: 'CMS_WORKER', service: workerName }],
308
- routes: [{ pattern: siteDomain, custom_domain: true }],
309
- vars: {
310
- PUBLIC_CMS_URL: `https://${cmsDomain}`,
311
- PUBLIC_API_URL: `https://${cmsDomain}`,
312
- },
313
- }, null, 2)
514
+ JSON.stringify(siteWranglerConfig, null, 2)
314
515
  );
315
516
 
517
+ const cmsHostname = cmsFullUrl.replace(/^https?:\/\//, '').split(':')[0];
518
+
316
519
  const astroConfig = `import { defineConfig } from 'astro/config';
317
520
  import cloudflare from '@astrojs/cloudflare';
318
521
 
@@ -321,8 +524,8 @@ export default defineConfig({
321
524
  trailingSlash: 'ignore',
322
525
  image: {
323
526
  remotePatterns: [
324
- { protocol: 'http', hostname: 'localhost', port: '3000' },
325
- { protocol: 'https', hostname: '${cmsDomain}' }
527
+ { protocol: 'http', hostname: 'localhost', port: '8787' },
528
+ { protocol: '${isCloudflare ? 'https' : 'http'}', hostname: '${cmsHostname}' }
326
529
  ]
327
530
  },
328
531
  build: {
@@ -336,7 +539,7 @@ export default defineConfig({
336
539
 
337
540
  writeFileSync(
338
541
  join(siteDir, '.env'),
339
- `PUBLIC_CMS_URL="https://${cmsDomain}"\nPUBLIC_API_URL="https://${cmsDomain}"\n`
542
+ `PUBLIC_CMS_URL="${cmsFullUrl}"\nPUBLIC_API_URL="${cmsFullUrl}"\n`
340
543
  );
341
544
  }
342
545
 
@@ -1,4 +1,4 @@
1
- # defaults/definitions/layouts/two-column-split.yml
1
+ # content/definitions/layouts/two-column-split.yml
2
2
  id: two-column
3
3
  label: Two Column
4
4
 
@@ -33,5 +33,7 @@ tabs:
33
33
  label: Last Name
34
34
  showAsEditorTitle: true
35
35
  - position:
36
+ label: Position
37
+ required: false
36
38
 
37
39
  - fieldSet: access