@brightsideoy/kama 0.1.0-alpha.101 → 0.1.0-alpha.108

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 (45) hide show
  1. package/README.md +174 -0
  2. package/bin/cli.js +515 -125
  3. package/content/definitions/block-layouts/three-column.yaml +20 -0
  4. package/content/definitions/block-layouts/two-column.yaml +29 -0
  5. package/content/definitions/blocks/button.yaml +15 -0
  6. package/content/definitions/blocks/header.yaml +15 -0
  7. package/content/definitions/blocks/header_image.yaml +22 -0
  8. package/content/definitions/blocks/hero.yaml +27 -0
  9. package/content/definitions/blocks/image_card.yaml +57 -0
  10. package/content/definitions/blocks/single_image.yaml +28 -0
  11. package/content/definitions/blocks/text.yaml +16 -0
  12. package/content/definitions/channels/ecommerce.yaml +24 -0
  13. package/content/definitions/channels/website.yaml +32 -0
  14. package/content/definitions/field-sets/access.yaml +29 -0
  15. package/content/definitions/field-sets/channel-base.yaml +31 -0
  16. package/content/definitions/field-sets/comments.yaml +11 -0
  17. package/content/definitions/field-sets/content-block.yaml +46 -0
  18. package/content/definitions/field-sets/page-base.yaml +49 -0
  19. package/content/definitions/field-sets/page-meta.yaml +29 -0
  20. package/content/definitions/field-sets/page-social.yaml +80 -0
  21. package/content/definitions/field-sets/tree-base.yaml +31 -0
  22. package/content/definitions/field-sets/visibility.yaml +8 -0
  23. package/content/definitions/node-layouts/landing-page.yml +22 -0
  24. package/content/definitions/node-layouts/single-column.yml +38 -0
  25. package/content/definitions/node-layouts/three-column.yml +41 -0
  26. package/content/definitions/node-layouts/two-column.yml +40 -0
  27. package/content/definitions/nodes/address.yaml +32 -0
  28. package/content/definitions/nodes/article.yaml +43 -0
  29. package/content/definitions/nodes/event.yaml +49 -0
  30. package/content/definitions/nodes/external-link.yaml +26 -0
  31. package/content/definitions/nodes/page.yaml +65 -0
  32. package/content/definitions/nodes/person.yaml +39 -0
  33. package/content/definitions/nodes/product.yaml +86 -0
  34. package/content/definitions/trees/navigation.yaml +29 -0
  35. package/content/definitions/trees/social-links.yaml +9 -0
  36. package/dist/assets/{index-C2vhaLxW.js → index-Bv1BhKvx.js} +3 -1
  37. package/dist/index.js +242 -7
  38. package/documentation/SCHEMAS.md +182 -0
  39. package/package.json +1 -12
  40. package/scripts/bundle-definitions.js +41 -1
  41. package/dist/index.html +0 -13
  42. package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
  43. package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
  44. package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
  45. package/dist/media/kama/2026/08/8ea5c21026cd4944bdf78c99fe2544a9.jpg +0 -0
package/bin/cli.js CHANGED
@@ -1,9 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import * as readline from 'node:readline/promises';
6
7
 
8
+ // Dynamically resolve the current CLI version to inject into package.json files
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ let currentKamaVersion = 'alpha';
12
+ try {
13
+ const pkgPath = join(__dirname, '../package.json');
14
+ if (existsSync(pkgPath)) {
15
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
16
+ if (pkg.version) currentKamaVersion = `^${pkg.version}`;
17
+ }
18
+ } catch (e) {
19
+ console.warn("⚠️ Could not detect Kama version, defaulting to 'alpha'");
20
+ }
21
+
7
22
  const rl = readline.createInterface({
8
23
  input: process.stdin,
9
24
  output: process.stdout,
@@ -32,8 +47,25 @@ async function main() {
32
47
  */
33
48
  async function createFullWorkspace() {
34
49
  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';
50
+
51
+ console.log('\n🌍 Where do you plan to deploy KAMA?');
52
+ console.log(' 1) Local Development Only (SQLite / Testing)');
53
+ console.log(' 2) Cloudflare Edge (Workers / D1 / R2)');
54
+ const envChoice = await rl.question('\nChoice [1/2]: ');
55
+ const isCloudflare = envChoice.trim() === '2';
56
+
57
+ let cmsDomain = 'localhost:8787';
58
+ let siteDomain = 'localhost:4321';
59
+
60
+ // Only ask for domains if deploying to Cloudflare Edge
61
+ if (isCloudflare) {
62
+ cmsDomain = (await rl.question('\nCMS Admin Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
63
+ siteDomain = (await rl.question('First Website Domain (e.g., kama.t3cloud.eu): ')) || 'kama.t3cloud.eu';
64
+ }
65
+
66
+ // Standardize scheme for localhost vs custom edge domains
67
+ const cmsProtocol = isCloudflare ? 'https://' : 'http://';
68
+ const cmsFullUrl = `${cmsProtocol}${cmsDomain.replace(/^https?:\/\//, '')}`;
37
69
 
38
70
  const rootDir = join(process.cwd(), projName);
39
71
 
@@ -45,56 +77,95 @@ async function createFullWorkspace() {
45
77
  console.log(`\n📁 Creating workspace folder at ./${projName}...`);
46
78
  mkdirSync(rootDir, { recursive: true });
47
79
 
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 });
80
+ // 🎯 Generate root .gitignore so auto-generated bundles and secrets stay out of git
81
+ writeFileSync(
82
+ join(rootDir, '.gitignore'),
83
+ `node_modules/
84
+ .env
85
+ .DS_Store
86
+ cms-worker/.dev.vars
87
+ cms-worker/bundled-definitions.js
88
+ cms-worker/.wrangler/
89
+ sites/*/.env
90
+ sites/*/.astro/
91
+ sites/*/dist/
92
+ `
93
+ );
94
+
95
+ // 🎯 Scaffold full schema directory structure
96
+ const schemaCategories = [
97
+ 'block-layouts',
98
+ 'blocks',
99
+ 'channels',
100
+ 'field-sets',
101
+ 'node-layouts',
102
+ 'nodes',
103
+ 'trees'
104
+ ];
105
+
106
+ for (const category of schemaCategories) {
107
+ mkdirSync(join(rootDir, 'content', 'definitions', category), { recursive: true });
108
+ }
109
+
110
+ // 🎯 Populate starter example YAML files
111
+ scaffoldSampleSchemas(rootDir);
51
112
 
52
113
  // --- AUTO D1 CREATION MAGIC ---
53
- let d1DatabaseId = 'REPLACE_WITH_YOUR_D1_ID';
114
+ let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
54
115
  let isAuthenticated = false;
55
116
 
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...`);
117
+ if (isCloudflare) {
118
+ console.log(`\n🔑 Checking Cloudflare authentication...`);
64
119
  try {
65
- execSync('npx wrangler login', { stdio: 'inherit' });
120
+ execSync('npx wrangler whoami', { stdio: 'pipe' });
66
121
  isAuthenticated = true;
67
- console.log(`✅ Successfully logged in!`);
68
- } catch (loginError) {
69
- console.log(`❌ Login failed or was cancelled.`);
122
+ console.log(`✅ Logged in to Cloudflare.`);
123
+ } catch (error) {
124
+ console.log(`⚠️ You are not logged in to Cloudflare.`);
125
+ console.log(`🌐 Launching browser for Cloudflare login...`);
126
+ try {
127
+ execSync('npx wrangler login', { stdio: 'inherit' });
128
+ isAuthenticated = true;
129
+ console.log(`✅ Successfully logged in!`);
130
+ } catch (loginError) {
131
+ console.log(`❌ Login failed or was cancelled.`);
132
+ }
70
133
  }
71
- }
72
134
 
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
-
78
- const jsonStart = listOutput.indexOf('[');
79
- const jsonEnd = listOutput.lastIndexOf(']') + 1;
80
- let databases = [];
135
+ if (isAuthenticated) {
136
+ try {
137
+ console.log(`\n🗄️ Checking existing Cloudflare D1 databases...`);
138
+ const listOutput = execSync(`npx wrangler d1 list --json`, { encoding: 'utf-8', stdio: 'pipe' });
81
139
 
82
- if (jsonStart !== -1 && jsonEnd !== -1) {
83
- databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
84
- }
140
+ const jsonStart = listOutput.indexOf('[');
141
+ const jsonEnd = listOutput.lastIndexOf(']') + 1;
142
+ let databases = [];
85
143
 
86
- const existingDb = databases.find(db => db.name === dbName);
87
-
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]: `);
144
+ if (jsonStart !== -1 && jsonEnd !== -1) {
145
+ databases = JSON.parse(listOutput.substring(jsonStart, jsonEnd));
146
+ }
91
147
 
92
- if (useExisting.toLowerCase() !== 'n') {
93
- d1DatabaseId = existingDb.uuid;
94
- console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
148
+ const existingDb = databases.find(db => db.name === dbName);
149
+
150
+ if (existingDb) {
151
+ console.log(`⚠️ Database '${dbName}' already exists on your account.`);
152
+ const useExisting = await rl.question(`Do you want to link this existing database? (y/n) [y]: `);
153
+
154
+ if (useExisting.toLowerCase() !== 'n') {
155
+ d1DatabaseId = existingDb.uuid;
156
+ console.log(`✅ Linked existing database ID: ${d1DatabaseId}`);
157
+ } else {
158
+ dbName = (await rl.question(`Enter a new database name: `)).trim();
159
+ console.log(`\n🗄️ Creating new database '${dbName}'...`);
160
+ const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
161
+ 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}/);
162
+ if (match) {
163
+ d1DatabaseId = match[0];
164
+ console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
165
+ }
166
+ }
95
167
  } else {
96
- dbName = (await rl.question(`Enter a new database name: `)).trim();
97
- console.log(`\n🗄️ Creating new database '${dbName}'...`);
168
+ console.log(`\n🗄️ Attempting to auto-create Cloudflare D1 Database '${dbName}'...`);
98
169
  const d1Output = execSync(`npx wrangler d1 create ${dbName}`, { encoding: 'utf-8', stdio: 'pipe' });
99
170
  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
171
  if (match) {
@@ -102,20 +173,14 @@ async function createFullWorkspace() {
102
173
  console.log(`✅ Success! Database ID bound: ${d1DatabaseId}`);
103
174
  }
104
175
  }
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
- }
176
+ } catch (error) {
177
+ console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
113
178
  }
114
- } catch (error) {
115
- console.log(`⚠️ Database operation failed. We'll leave a placeholder.`);
179
+ } else {
180
+ console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
116
181
  }
117
182
  } else {
118
- console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
183
+ console.log(`\n💻 Local mode selected. Skipping Cloudflare authentication and domain setup...`);
119
184
  }
120
185
  // ------------------------------
121
186
 
@@ -133,7 +198,7 @@ async function createFullWorkspace() {
133
198
  'deploy:cms': 'npm run bundle:schemas && npm run deploy -w cms-worker',
134
199
  },
135
200
  dependencies: {
136
- '@brightsideoy/kama': 'alpha',
201
+ '@brightsideoy/kama': currentKamaVersion,
137
202
  }
138
203
  }, null, 2)
139
204
  );
@@ -143,9 +208,32 @@ async function createFullWorkspace() {
143
208
  const cmsDir = join(rootDir, 'cms-worker');
144
209
  mkdirSync(cmsDir, { recursive: true });
145
210
 
211
+ // 🎯 Properly wrap and inject BUNDLED_DEFINITIONS from LOCAL workspace, not node_modules
212
+ const cmsWorkerIndexContent = `import kamaWorker from '@brightsideoy/kama';
213
+ import * as bundledModule from './bundled-definitions.js';
214
+
215
+ // Resolve BUNDLED_DEFINITIONS across all module resolution formats
216
+ const BUNDLED_DEFINITIONS =
217
+ bundledModule.BUNDLED_DEFINITIONS ||
218
+ bundledModule.default?.BUNDLED_DEFINITIONS ||
219
+ bundledModule.default ||
220
+ bundledModule;
221
+
222
+ export default {
223
+ async fetch(request, env, ctx) {
224
+ const customEnv = {
225
+ ...env,
226
+ BUNDLED_DEFINITIONS,
227
+ };
228
+
229
+ return kamaWorker.fetch(request, customEnv, ctx);
230
+ },
231
+ };
232
+ `;
233
+
146
234
  writeFileSync(
147
235
  join(cmsDir, 'index.js'),
148
- `export { default } from '@brightsideoy/kama';\n`
236
+ cmsWorkerIndexContent
149
237
  );
150
238
 
151
239
  writeFileSync(
@@ -162,68 +250,79 @@ async function createFullWorkspace() {
162
250
  "deploy": "npm run schema:remote && wrangler deploy"
163
251
  },
164
252
  dependencies: {
165
- '@brightsideoy/kama': 'alpha',
253
+ '@brightsideoy/kama': currentKamaVersion,
166
254
  },
167
255
  devDependencies: {
168
- wrangler: '^4.127.1',
256
+ wrangler: '^3.80.0',
169
257
  tsx: '^4.19.2',
170
258
  },
171
259
  }, null, 2)
172
260
  );
173
261
 
174
- // 🎯 Fixed CMS wrangler.json (Added PUBLIC_FRONTEND_URL & CLOUDFLARE_ZONE_ID to public vars)
262
+ // 🎯 Fixed CMS wrangler.json
263
+ const cmsWranglerConfig = {
264
+ $schema: '../node_modules/wrangler/config-schema.json',
265
+ name: workerName,
266
+ main: 'index.js',
267
+ compatibility_date: '2026-08-01',
268
+ compatibility_flags: ['nodejs_compat'],
269
+ assets: {
270
+ directory: '../node_modules/@brightsideoy/kama/dist',
271
+ binding: 'ASSETS'
272
+ },
273
+ images: {
274
+ binding: 'IMAGES'
275
+ },
276
+ vars: {
277
+ CDN_DOMAIN: cmsDomain,
278
+ PUBLIC_FRONTEND_URL: isCloudflare ? `https://${siteDomain}` : 'http://localhost:4321',
279
+ CLOUDFLARE_ZONE_ID: isCloudflare ? "REPLACE_WITH_YOUR_32_CHAR_ZONE_ID" : "local-zone-not-needed"
280
+ },
281
+ d1_databases: [{
282
+ binding: 'DB',
283
+ database_name: dbName,
284
+ database_id: d1DatabaseId,
285
+ }],
286
+ r2_buckets: [{
287
+ binding: 'MEDIA_BUCKET',
288
+ bucket_name: bucketName,
289
+ }],
290
+ };
291
+
292
+ if (isCloudflare) {
293
+ cmsWranglerConfig.routes = [{ pattern: cmsDomain, custom_domain: true }];
294
+ }
295
+
175
296
  writeFileSync(
176
297
  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)
298
+ JSON.stringify(cmsWranglerConfig, null, 2)
206
299
  );
207
300
 
208
- // 🎯 Fixed .dev.vars (Only contains API token for local dev, not public Zone ID)
301
+ // 🎯 Fixed .dev.vars
209
302
  writeFileSync(
210
303
  join(cmsDir, '.dev.vars'),
211
- `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=cfut_YOUR_CUSTOM_PURGE_TOKEN\n`
304
+ `# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN=${isCloudflare ? 'cfut_YOUR_CUSTOM_PURGE_TOKEN' : 'local_token'}\n`
212
305
  );
213
306
 
214
307
  // 🎯 3. Setup First Astro Site tied to target workerName
215
- const siteFolderName = siteDomain.split('.')[0];
216
- await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsDomain, workerName);
308
+ const siteFolderName = isCloudflare ? siteDomain.split('.')[0] : 'web';
309
+ await generateAstroSite(rootDir, siteFolderName, siteDomain, cmsFullUrl, workerName, isCloudflare);
217
310
 
218
311
  console.log(`\n✨ Workspace ready! Next steps:`);
219
312
  console.log(` 1. cd ${projName}`);
220
313
  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.`);
314
+
315
+ if (isCloudflare) {
316
+ if (d1DatabaseId === '00000000-0000-0000-0000-000000000000' || d1DatabaseId === 'REPLACE_WITH_YOUR_D1_ID') {
317
+ console.log(` 3. Run 'npx wrangler login' and create a D1 database manually.`);
318
+ }
319
+ console.log(` 4. Update CLOUDFLARE_ZONE_ID in cms-worker/wrangler.json with your actual Zone ID`);
320
+ console.log(` 5. Securely store your purge token: cd cms-worker && npx wrangler secret put CLOUDFLARE_API_TOKEN`);
321
+ console.log(` 6. Run 'npm run dev:cms' from the workspace root to start the backend.`);
322
+ } else {
323
+ console.log(` 3. Run 'npm run dev:cms' from the workspace root to start the local SQLite backend.`);
324
+ console.log(`\n💡 Note: You can deploy to Cloudflare later by updating 'cms-worker/wrangler.json' with real IDs.`);
223
325
  }
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
326
  }
228
327
 
229
328
  /**
@@ -246,19 +345,146 @@ async function createNewSiteOnly() {
246
345
  } catch (e) { }
247
346
  }
248
347
 
348
+ console.log('\n🌍 Deployment Mode:');
349
+ console.log(' 1) Local Development');
350
+ console.log(' 2) Cloudflare Edge');
351
+ const envChoice = await rl.question('\nChoice [1/2]: ');
352
+ const isCloudflare = envChoice.trim() === '2';
353
+
249
354
  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
355
 
253
- await generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain.replace(/^https?:\/\//, ''), targetWorkerName);
356
+ let siteDomain = 'localhost:4321';
357
+ let cmsFullUrl = 'http://localhost:8787';
358
+
359
+ if (isCloudflare) {
360
+ siteDomain = await rl.question('Website Domain (e.g., brand.com): ');
361
+ const cmsDomainInput = (await rl.question('CMS API Domain (e.g., admin.t3cloud.eu): ')) || 'admin.t3cloud.eu';
362
+ cmsFullUrl = `https://${cmsDomainInput.replace(/^https?:\/\//, '')}`;
363
+ }
364
+
365
+ await generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, targetWorkerName, isCloudflare);
254
366
 
255
367
  console.log(`\n✨ Site created in sites/${siteFolder}! Run 'npm install' at workspace root.`);
256
368
  }
257
369
 
370
+ /**
371
+ * Helper: Scaffolds initial starter schemas for all category folders
372
+ */
373
+ function scaffoldSampleSchemas(rootDir) {
374
+ const defsDir = join(rootDir, 'content', 'definitions');
375
+
376
+ // 1. Channels
377
+ writeFileSync(join(defsDir, 'channels', 'website.yaml'), `id: website
378
+ label: Main Website
379
+ defaultLanguage: en
380
+ languages:
381
+ - en
382
+ - fi
383
+ `);
384
+
385
+ // 2. Nodes
386
+ writeFileSync(join(defsDir, 'nodes', 'page.yaml'), `id: page
387
+ label: Page
388
+ inTree: true
389
+ inVault: false
390
+ hasCanvas: true
391
+ tabs:
392
+ settings:
393
+ label: Settings
394
+ fields:
395
+ title:
396
+ label: Page Title
397
+ type: text
398
+ required: true
399
+ slug:
400
+ label: URL Slug
401
+ type: slug
402
+ linkedTo: title
403
+ `);
404
+
405
+ writeFileSync(join(defsDir, 'nodes', 'article.yaml'), `id: article
406
+ label: Article
407
+ inTree: false
408
+ inVault: true
409
+ hasCanvas: true
410
+ tabs:
411
+ settings:
412
+ label: Settings
413
+ fields:
414
+ title:
415
+ label: Title
416
+ type: text
417
+ required: true
418
+ slug:
419
+ label: Slug
420
+ type: slug
421
+ linkedTo: title
422
+ `);
423
+
424
+ // 3. Blocks
425
+ writeFileSync(join(defsDir, 'blocks', 'hero.yaml'), `id: hero
426
+ label: Hero Section
427
+ fields:
428
+ heading:
429
+ label: Heading
430
+ type: text
431
+ required: true
432
+ subheading:
433
+ label: Subheading
434
+ type: text
435
+ `);
436
+
437
+ writeFileSync(join(defsDir, 'blocks', 'text.yaml'), `id: text
438
+ label: Text Block
439
+ fields:
440
+ content:
441
+ label: Content
442
+ type: richtext
443
+ `);
444
+
445
+ // 4. Block Layouts
446
+ writeFileSync(join(defsDir, 'block-layouts', 'two-column.yaml'), `id: two-column
447
+ label: Two Column Layout
448
+ regions:
449
+ left:
450
+ label: Left Column
451
+ right:
452
+ label: Right Column
453
+ `);
454
+
455
+ // 5. Node Layouts
456
+ writeFileSync(join(defsDir, 'node-layouts', 'single-column.yml'), `id: single-column
457
+ label: Single Column Page
458
+ regions:
459
+ main:
460
+ label: Main Content Area
461
+ `);
462
+
463
+ // 6. Field Sets
464
+ writeFileSync(join(defsDir, 'field-sets', 'page-base.yaml'), `id: page-base
465
+ label: Base Page Fields
466
+ fields:
467
+ metaTitle:
468
+ label: Meta Title
469
+ type: text
470
+ metaDescription:
471
+ label: Meta Description
472
+ type: text
473
+ `);
474
+
475
+ // 7. Trees
476
+ writeFileSync(join(defsDir, 'trees', 'navigation.yaml'), `id: navigation
477
+ label: Main Navigation Tree
478
+ maxDepth: 3
479
+ allowedNodeTypes:
480
+ - page
481
+ `);
482
+ }
483
+
258
484
  /**
259
485
  * Helper: Scaffolds the Astro site with middleware, wrangler.json, & slug page
260
486
  */
261
- async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, workerName = 'kama-worker') {
487
+ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsFullUrl, workerName = 'kama-worker', isCloudflare = false) {
262
488
  console.log(`\n🚀 Scaffolding Astro site (${siteFolder}) mapped to ${siteDomain} (Service: ${workerName})...`);
263
489
 
264
490
  const siteDir = join(rootDir, 'sites', siteFolder);
@@ -282,37 +508,44 @@ async function generateAstroSite(rootDir, siteFolder, siteDomain, cmsDomain, wor
282
508
  },
283
509
  dependencies: {
284
510
  '@astrojs/cloudflare': '^14.2.5',
285
- '@brightsideoy/kama': 'alpha',
286
- astro: '^7.2.2',
287
- wrangler: '^4.127.1',
511
+ '@brightsideoy/kama': currentKamaVersion,
512
+ astro: '^5.0.0', // Ensure using modern Astro
513
+ wrangler: '^3.80.0',
288
514
  },
289
515
  }, null, 2)
290
516
  );
291
517
 
292
- // 🎯 Fixed Astro wrangler.json: Added `run_worker_first: true` to prevent media routing 404s on Edge
518
+ const siteWranglerConfig = {
519
+ $schema: 'node_modules/wrangler/config-schema.json',
520
+ name: siteFolder,
521
+ compatibility_date: '2026-08-01',
522
+ compatibility_flags: ['nodejs_compat'],
523
+ assets: {
524
+ directory: './dist/client',
525
+ binding: 'ASSETS',
526
+ html_handling: 'drop-trailing-slash',
527
+ not_found_handling: '404-page',
528
+ run_worker_first: true
529
+ },
530
+ services: [{ binding: 'CMS_WORKER', service: workerName }],
531
+ kv_namespaces: [{ binding: "HTML_CACHE", id: "REPLACE_WITH_YOUR_KV_ID", preview_id: "LOCAL_DEV_KV" }],
532
+ vars: {
533
+ PUBLIC_CMS_URL: cmsFullUrl,
534
+ PUBLIC_API_URL: cmsFullUrl,
535
+ },
536
+ };
537
+
538
+ if (isCloudflare) {
539
+ siteWranglerConfig.routes = [{ pattern: siteDomain, custom_domain: true }];
540
+ }
541
+
293
542
  writeFileSync(
294
543
  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)
544
+ JSON.stringify(siteWranglerConfig, null, 2)
314
545
  );
315
546
 
547
+ const cmsHostname = cmsFullUrl.replace(/^https?:\/\//, '').split(':')[0];
548
+
316
549
  const astroConfig = `import { defineConfig } from 'astro/config';
317
550
  import cloudflare from '@astrojs/cloudflare';
318
551
 
@@ -321,8 +554,8 @@ export default defineConfig({
321
554
  trailingSlash: 'ignore',
322
555
  image: {
323
556
  remotePatterns: [
324
- { protocol: 'http', hostname: 'localhost', port: '3000' },
325
- { protocol: 'https', hostname: '${cmsDomain}' }
557
+ { protocol: 'http', hostname: 'localhost', port: '8787' },
558
+ { protocol: '${isCloudflare ? 'https' : 'http'}', hostname: '${cmsHostname}' }
326
559
  ]
327
560
  },
328
561
  build: {
@@ -336,8 +569,165 @@ export default defineConfig({
336
569
 
337
570
  writeFileSync(
338
571
  join(siteDir, '.env'),
339
- `PUBLIC_CMS_URL="https://${cmsDomain}"\nPUBLIC_API_URL="https://${cmsDomain}"\n`
572
+ `PUBLIC_CMS_URL="${cmsFullUrl}"\nPUBLIC_API_URL="${cmsFullUrl}"\n`
573
+ );
574
+
575
+ // 🎯 Scaffold the Base Layout
576
+ writeFileSync(
577
+ join(siteDir, 'src/layouts/Layout.astro'),
578
+ `---
579
+ export interface Props {
580
+ \ttitle: string;
581
+ }
582
+ const { title } = Astro.props;
583
+ ---
584
+ <!DOCTYPE html>
585
+ <html lang="en">
586
+ \t<head>
587
+ \t\t<meta charset="UTF-8" />
588
+ \t\t<meta name="viewport" content="width=device-width" />
589
+ \t\t<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
590
+ \t\t<meta name="generator" content={Astro.generator} />
591
+ \t\t<title>{title}</title>
592
+ \t</head>
593
+ \t<body>
594
+ \t\t<slot />
595
+ \t</body>
596
+ </html>`
597
+ );
598
+
599
+ // 🎯 Scaffold the Dynamic Slug Page
600
+ writeFileSync(
601
+ join(siteDir, 'src/pages/[...slug].astro'),
602
+ `---
603
+ import Layout from '../layouts/Layout.astro';
604
+
605
+ const { slug } = Astro.params;
606
+ const cmsUrl = import.meta.env.PUBLIC_CMS_URL;
607
+
608
+ // Example Data Fetch: Replace with actual KAMA fetch logic
609
+ // const response = await fetch(\`\${cmsUrl}/api/pages/\${slug || 'home'}\`);
610
+ // const pageData = await response.json();
611
+ ---
612
+ <Layout title="Kama Generated Site">
613
+ \t<main>
614
+ \t\t<h1>Welcome to your new Kama Site</h1>
615
+ \t\t<p>Current route: <strong>{slug || '/'}</strong></p>
616
+ \t</main>
617
+ </Layout>`
618
+ );
619
+
620
+ // 🎯 Scaffold the Full Edge-Caching Middleware
621
+ writeFileSync(
622
+ join(siteDir, 'src/middleware.ts'),
623
+ `import { defineMiddleware } from "astro:middleware";
624
+ import { env } from "cloudflare:workers";
625
+
626
+ export const onRequest = defineMiddleware(async (context, next) => {
627
+ const pathname = context.url.pathname;
628
+ const cfContext = (context.locals as any).cfContext;
629
+ const workerEnv = cfContext?.env || (env as any);
630
+ const HTML_CACHE = workerEnv?.HTML_CACHE;
631
+
632
+ // 🎯 1. CACHE PURGE ENDPOINT
633
+ if (pathname === "/api/purge" || pathname === "/api/purge/") {
634
+ if (context.request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
635
+
636
+ try {
637
+ const body = await context.request.json();
638
+ const now = Date.now().toString();
639
+
640
+ if (HTML_CACHE) {
641
+ if (body.purge_everything) {
642
+ await HTML_CACHE.put("site_version", now);
643
+ } else if (Array.isArray(body.urls) && body.urls.length > 0) {
644
+ await Promise.all(body.urls.map((u: string) => {
645
+ const path = new URL(u).pathname.replace(/\\/$/, "") || "/";
646
+ return HTML_CACHE.put(\`v:\${path}\`, now);
647
+ }));
648
+ }
649
+
650
+ // Surgical delete
651
+ if (Array.isArray(body.deleted_urls) && body.deleted_urls.length > 0) {
652
+ await Promise.all(body.deleted_urls.map(async (u: string) => {
653
+ const path = new URL(u).pathname.replace(/\\/$/, "") || "/";
654
+ const [siteVer, pageVer] = await Promise.all([
655
+ HTML_CACHE.get("site_version"), HTML_CACHE.get(\`v:\${path}\`)
656
+ ]);
657
+ if (siteVer || pageVer) {
658
+ await HTML_CACHE.delete(\`\${path}:v:\${siteVer || "1"}:\${pageVer || "1"}\`);
659
+ }
660
+ await HTML_CACHE.delete(\`v:\${path}\`);
661
+ }));
662
+ }
663
+ }
664
+
665
+ // Astro Worker Cache layer flush
666
+ if (typeof cfContext?.cache?.purge === 'function') await cfContext.cache.purge({ purgeEverything: true });
667
+
668
+ return new Response(JSON.stringify({ success: true }), { status: 200 });
669
+ } catch (err: any) {
670
+ return new Response(JSON.stringify({ error: err.message }), { status: 500 });
671
+ }
672
+ }
673
+
674
+ // 🎯 2. KV HTML STATIC LOOKUP
675
+ const isWarmer = context.request.headers.get("X-Cache-Warmer") === "true";
676
+ const isHtmlPage = !pathname.includes('.') && !pathname.startsWith('/api/');
677
+ const cleanLookupPath = pathname.replace(/\\/$/, "") || "/";
678
+ let activeCacheKey = cleanLookupPath;
679
+
680
+ if (isHtmlPage && HTML_CACHE && !isWarmer) {
681
+ try {
682
+ const [siteVer, pageVer] = await Promise.all([
683
+ HTML_CACHE.get("site_version"),
684
+ HTML_CACHE.get(\`v:\${cleanLookupPath}\`)
685
+ ]);
686
+ activeCacheKey = \`\${cleanLookupPath}:v:\${siteVer || "1"}:\${pageVer || "1"}\`;
687
+
688
+ const cachedHtml = await HTML_CACHE.get(activeCacheKey);
689
+ if (cachedHtml) {
690
+ return new Response(cachedHtml, {
691
+ status: 200,
692
+ headers: {
693
+ "Content-Type": "text/html; charset=utf-8",
694
+ "X-Cache-Status": "HIT-KV-STATICFILE",
695
+ "Cache-Control": "no-store, no-cache, must-revalidate"
696
+ }
697
+ });
698
+ }
699
+ } catch (e) {
700
+ console.warn("[Middleware] KV skip:", e);
701
+ }
702
+ }
703
+
704
+ const response = await next();
705
+
706
+ // 🎯 3. KV HTML STATIC WRITE
707
+ if (isHtmlPage && HTML_CACHE && response.status === 200 && response.headers.get("content-type")?.includes("text/html")) {
708
+ const htmlText = await response.clone().text();
709
+ const writeTask = (async () => {
710
+ if (activeCacheKey === cleanLookupPath || isWarmer) {
711
+ const forcedVersion = Date.now().toString();
712
+ await HTML_CACHE.put(\`v:\${cleanLookupPath}\`, forcedVersion);
713
+ const siteVer = await HTML_CACHE.get("site_version") || "1";
714
+ activeCacheKey = \`\${cleanLookupPath}:v:\${siteVer}:\${forcedVersion}\`;
715
+ }
716
+ await HTML_CACHE.put(activeCacheKey, htmlText);
717
+ })();
718
+
719
+ if (cfContext?.waitUntil) cfContext.waitUntil(writeTask);
720
+ }
721
+
722
+ return response;
723
+ });`
340
724
  );
725
+
726
+ if (isCloudflare) {
727
+ console.log(`\n⚠️ Note for Astro Frontend: You will need to create a KV Namespace for the HTML Cache.`);
728
+ console.log(` Run: npx wrangler kv:namespace create HTML_CACHE`);
729
+ console.log(` Then update the ID in sites/${siteFolder}/wrangler.json`);
730
+ }
341
731
  }
342
732
 
343
733
  main().catch(console.error);