@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.
- package/README.md +174 -0
- package/bin/cli.js +515 -125
- package/content/definitions/block-layouts/three-column.yaml +20 -0
- package/content/definitions/block-layouts/two-column.yaml +29 -0
- package/content/definitions/blocks/button.yaml +15 -0
- package/content/definitions/blocks/header.yaml +15 -0
- package/content/definitions/blocks/header_image.yaml +22 -0
- package/content/definitions/blocks/hero.yaml +27 -0
- package/content/definitions/blocks/image_card.yaml +57 -0
- package/content/definitions/blocks/single_image.yaml +28 -0
- package/content/definitions/blocks/text.yaml +16 -0
- package/content/definitions/channels/ecommerce.yaml +24 -0
- package/content/definitions/channels/website.yaml +32 -0
- package/content/definitions/field-sets/access.yaml +29 -0
- package/content/definitions/field-sets/channel-base.yaml +31 -0
- package/content/definitions/field-sets/comments.yaml +11 -0
- package/content/definitions/field-sets/content-block.yaml +46 -0
- package/content/definitions/field-sets/page-base.yaml +49 -0
- package/content/definitions/field-sets/page-meta.yaml +29 -0
- package/content/definitions/field-sets/page-social.yaml +80 -0
- package/content/definitions/field-sets/tree-base.yaml +31 -0
- package/content/definitions/field-sets/visibility.yaml +8 -0
- package/content/definitions/node-layouts/landing-page.yml +22 -0
- package/content/definitions/node-layouts/single-column.yml +38 -0
- package/content/definitions/node-layouts/three-column.yml +41 -0
- package/content/definitions/node-layouts/two-column.yml +40 -0
- package/content/definitions/nodes/address.yaml +32 -0
- package/content/definitions/nodes/article.yaml +43 -0
- package/content/definitions/nodes/event.yaml +49 -0
- package/content/definitions/nodes/external-link.yaml +26 -0
- package/content/definitions/nodes/page.yaml +65 -0
- package/content/definitions/nodes/person.yaml +39 -0
- package/content/definitions/nodes/product.yaml +86 -0
- package/content/definitions/trees/navigation.yaml +29 -0
- package/content/definitions/trees/social-links.yaml +9 -0
- package/dist/assets/{index-C2vhaLxW.js → index-Bv1BhKvx.js} +3 -1
- package/dist/index.js +242 -7
- package/documentation/SCHEMAS.md +182 -0
- package/package.json +1 -12
- package/scripts/bundle-definitions.js +41 -1
- package/dist/index.html +0 -13
- package/dist/media/kama/2026/08/17a098c92bc54ea4b4441e531a34575b.jpg +0 -0
- package/dist/media/kama/2026/08/3e9ae457dd23461680cc699f42d07cf0.jpg +0 -0
- package/dist/media/kama/2026/08/8e5b3a2fd194459f9b962f8e12928bf0.jpg +0 -0
- 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
|
-
|
|
36
|
-
|
|
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
|
-
// 🎯
|
|
49
|
-
|
|
50
|
-
|
|
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 = '
|
|
114
|
+
let d1DatabaseId = '00000000-0000-0000-0000-000000000000';
|
|
54
115
|
let isAuthenticated = false;
|
|
55
116
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
120
|
+
execSync('npx wrangler whoami', { stdio: 'pipe' });
|
|
66
121
|
isAuthenticated = true;
|
|
67
|
-
console.log(`✅
|
|
68
|
-
} catch (
|
|
69
|
-
console.log(
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
const jsonStart = listOutput.indexOf('[');
|
|
141
|
+
const jsonEnd = listOutput.lastIndexOf(']') + 1;
|
|
142
|
+
let databases = [];
|
|
85
143
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
106
|
-
console.log(
|
|
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
|
-
}
|
|
115
|
-
console.log(`⚠️
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`⚠️ Skipping database creation. We'll leave a placeholder in wrangler.json.`);
|
|
116
181
|
}
|
|
117
182
|
} else {
|
|
118
|
-
console.log(
|
|
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':
|
|
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
|
-
|
|
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':
|
|
253
|
+
'@brightsideoy/kama': currentKamaVersion,
|
|
166
254
|
},
|
|
167
255
|
devDependencies: {
|
|
168
|
-
wrangler: '^
|
|
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
|
|
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
|
|
301
|
+
// 🎯 Fixed .dev.vars
|
|
209
302
|
writeFileSync(
|
|
210
303
|
join(cmsDir, '.dev.vars'),
|
|
211
|
-
`# Cloudflare Edge Cache Purging Secrets\nCLOUDFLARE_API_TOKEN
|
|
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,
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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,
|
|
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':
|
|
286
|
-
astro: '^
|
|
287
|
-
wrangler: '^
|
|
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
|
-
|
|
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: '
|
|
325
|
-
{ protocol: 'https', hostname: '${
|
|
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="
|
|
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);
|