create-sonicjs 3.0.0-beta.2 → 3.0.0-beta.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sonicjs",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.21",
4
4
  "description": "Create a new SonicJS application with zero configuration",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import fs from 'fs-extra'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
+ import { randomBytes } from 'crypto'
6
7
  import prompts from 'prompts'
7
8
  import kleur from 'kleur'
8
9
  import ora from 'ora'
@@ -50,7 +51,9 @@ const flags = {
50
51
  databaseName: args.find(arg => arg.startsWith('--database='))?.split('=')[1],
51
52
  bucketName: args.find(arg => arg.startsWith('--bucket='))?.split('=')[1],
52
53
  skipExample: args.includes('--skip-example'),
53
- includeExample: args.includes('--include-example')
54
+ includeExample: args.includes('--include-example'),
55
+ adminEmail: args.find(arg => arg.startsWith('--admin-email='))?.split('=')[1],
56
+ adminPassword: args.find(arg => arg.startsWith('--admin-password='))?.split('=')[1],
54
57
  }
55
58
 
56
59
  async function main() {
@@ -171,48 +174,39 @@ async function getProjectDetails(initialName) {
171
174
  })
172
175
  }
173
176
 
174
- // Seed admin user
175
- questions.push({
176
- type: 'confirm',
177
- name: 'seedAdmin',
178
- message: 'Create admin user?',
179
- initial: true
180
- })
181
-
182
- // Admin email (only if seeding)
183
- questions.push({
184
- type: (prev, values) => values.seedAdmin ? 'text' : null,
185
- name: 'adminEmail',
186
- message: 'Admin email:',
187
- validate: (value) => {
188
- if (!value) return 'Admin email is required'
189
- // Basic email validation
190
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
191
- if (!emailRegex.test(value)) return 'Please enter a valid email address'
192
- return true
193
- }
194
- })
195
-
196
- // Admin password (only if seeding)
197
- questions.push({
198
- type: (prev, values) => values.seedAdmin ? 'password' : null,
199
- name: 'adminPassword',
200
- message: 'Admin password:',
201
- validate: (value) => {
202
- if (!value) return 'Admin password is required'
203
- if (value.length < 8) return 'Password must be at least 8 characters'
204
- return true
205
- }
206
- })
207
-
208
- // Include example collection (only ask if neither flag is set)
209
- if (!flags.skipExample && !flags.includeExample) {
177
+ // Seed admin user (skip prompt if credentials provided via flags)
178
+ if (!flags.adminEmail || !flags.adminPassword) {
210
179
  questions.push({
211
180
  type: 'confirm',
212
- name: 'includeExample',
213
- message: 'Include example blog collection?',
181
+ name: 'seedAdmin',
182
+ message: 'Create admin user?',
214
183
  initial: true
215
184
  })
185
+
186
+ // Admin email (only if seeding)
187
+ questions.push({
188
+ type: (prev, values) => values.seedAdmin ? 'text' : null,
189
+ name: 'adminEmail',
190
+ message: 'Admin email:',
191
+ validate: (value) => {
192
+ if (!value) return 'Admin email is required'
193
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
194
+ if (!emailRegex.test(value)) return 'Please enter a valid email address'
195
+ return true
196
+ }
197
+ })
198
+
199
+ // Admin password (only if seeding)
200
+ questions.push({
201
+ type: (prev, values) => values.seedAdmin ? 'password' : null,
202
+ name: 'adminPassword',
203
+ message: 'Admin password:',
204
+ validate: (value) => {
205
+ if (!value) return 'Admin password is required'
206
+ if (value.length < 8) return 'Password must be at least 8 characters'
207
+ return true
208
+ }
209
+ })
216
210
  }
217
211
 
218
212
  // Create Cloudflare resources
@@ -246,10 +240,10 @@ async function getProjectDetails(initialName) {
246
240
  template: flags.template || 'starter', // Always default to starter template
247
241
  databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
248
242
  bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
249
- seedAdmin: answers.seedAdmin !== undefined ? answers.seedAdmin : true,
250
- adminEmail: answers.adminEmail,
251
- adminPassword: answers.adminPassword,
252
- includeExample: flags.skipExample ? false : (flags.includeExample ? true : (answers.includeExample !== undefined ? answers.includeExample : true)),
243
+ seedAdmin: (flags.adminEmail && flags.adminPassword) ? true : (answers.seedAdmin !== undefined ? answers.seedAdmin : true),
244
+ adminEmail: flags.adminEmail || answers.adminEmail,
245
+ adminPassword: flags.adminPassword || answers.adminPassword,
246
+ includeExample: true,
253
247
  createResources: flags.skipCloudflare ? false : answers.createResources,
254
248
  runMigrations: true, // Always run migrations automatically
255
249
  initGit: flags.skipGit ? false : answers.initGit,
@@ -415,7 +409,7 @@ async function copyTemplate(templateName, targetDir, options) {
415
409
 
416
410
  // Add @sonicjs-cms/core dependency
417
411
  packageJson.dependencies = {
418
- '@sonicjs-cms/core': '^3.0.0-beta.2',
412
+ '@sonicjs-cms/core': '^3.0.0-beta.21',
419
413
  ...packageJson.dependencies
420
414
  }
421
415
 
@@ -454,100 +448,88 @@ async function copyTemplate(templateName, targetDir, options) {
454
448
  password: options.adminPassword
455
449
  })
456
450
  }
451
+
452
+ // Generate .dev.vars with a random BETTER_AUTH_SECRET for local dev
453
+ const devVarsPath = path.join(targetDir, '.dev.vars')
454
+ if (!fs.existsSync(devVarsPath)) {
455
+ const secret = randomBytes(32).toString('hex')
456
+ await fs.writeFile(devVarsPath, `BETTER_AUTH_SECRET="${secret}"\n`)
457
+ }
457
458
  }
458
459
 
459
460
  async function createAdminSeedScript(targetDir, { email, password }) {
460
- const seedScriptContent = `import { createDb, users } from '@sonicjs-cms/core'
461
- import { eq } from 'drizzle-orm'
462
- import * as crypto from 'crypto'
461
+ const seedScriptContent = `import { bootstrapDocumentTypes, RbacService } from '@sonicjs-cms/core'
463
462
  import { getPlatformProxy } from 'wrangler'
464
463
 
465
464
  /**
466
465
  * Seed script to create initial admin user
467
466
  *
468
- * Run this script after migrations:
469
- * npm run db:migrate:local
470
- * npm run seed
471
- *
472
467
  * Admin credentials:
473
468
  * Email: ${email}
474
469
  * Password: [as entered during setup]
475
470
  */
476
471
 
472
+ async function hashPassword(password) {
473
+ const iterations = 100000
474
+ const salt = new Uint8Array(16)
475
+ crypto.getRandomValues(salt)
476
+ const encoder = new TextEncoder()
477
+ const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits'])
478
+ const hashBuffer = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, keyMaterial, 256)
479
+ const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
480
+ const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
481
+ return \`pbkdf2:\${iterations}:\${saltHex}:\${hashHex}\`
482
+ }
483
+
477
484
  async function seed() {
478
- // Get D1 database from Cloudflare environment using wrangler's getPlatformProxy
479
485
  const { env, dispose } = await getPlatformProxy()
480
486
 
481
487
  if (!env?.DB) {
482
- console.error('❌ Error: DB binding not found')
483
- console.error('')
484
- console.error('Make sure you have:')
485
- console.error('1. Created your D1 database: wrangler d1 create <database-name>')
486
- console.error('2. Updated wrangler.toml with the database_id')
487
- console.error('3. Run migrations: npm run db:migrate:local')
488
- console.error('')
488
+ console.error('❌ Error: DB binding not found. Run migrations first: npm run db:migrate:local')
489
489
  process.exit(1)
490
490
  }
491
491
 
492
- const db = createDb(env.DB)
493
-
494
492
  try {
495
493
  // Check if admin user already exists
496
- const existingUser = await db
497
- .select()
498
- .from(users)
499
- .where(eq(users.email, '${email}'))
500
- .get()
501
-
502
- if (existingUser) {
494
+ const existing = await env.DB.prepare('SELECT id FROM auth_user WHERE email = ?').bind('${email}').first()
495
+ if (existing) {
503
496
  console.log('✓ Admin user already exists')
504
- console.log(\` Email: ${email}\`)
505
- console.log(\` Role: \${existingUser.role}\`)
497
+ await dispose()
506
498
  return
507
499
  }
508
500
 
509
- // Hash password using SHA-256 (same as SonicJS auth system)
510
- const data = '${password}' + 'salt-change-in-production'
511
- const passwordHash = crypto.createHash('sha256').update(data).digest('hex')
512
- const now = Date.now()
513
- const odid = \`admin-\${now}-\${Math.random().toString(36).substr(2, 9)}\`
514
-
515
- // Create admin user
516
- await db
517
- .insert(users)
518
- .values({
519
- id: odid,
520
- email: '${email}',
521
- username: '${email.split('@')[0]}',
522
- firstName: 'Admin',
523
- lastName: 'User',
524
- passwordHash: passwordHash,
525
- role: 'admin',
526
- isActive: true,
527
- createdAt: now,
528
- updatedAt: now
529
- })
530
- .run()
501
+ const passwordHash = await hashPassword('${password}')
502
+ const nowMs = Date.now()
503
+ const odid = \`admin-\${nowMs}-\${Math.random().toString(36).substr(2, 9)}\`
504
+
505
+ await env.DB.batch([
506
+ env.DB.prepare(
507
+ 'INSERT INTO auth_user (id, email, first_name, last_name, role, is_active, created_at, updated_at, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
508
+ ).bind(odid, '${email}', 'Admin', 'User', 'admin', 1, nowMs, nowMs, 'Admin User'),
509
+ env.DB.prepare(
510
+ 'INSERT INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
511
+ ).bind(crypto.randomUUID(), odid, odid, 'credential', passwordHash, nowMs, nowMs),
512
+ ])
513
+
514
+ await bootstrapDocumentTypes(env.DB)
515
+ const rbac = new RbacService(env.DB)
516
+ await rbac.ensureSystemRbacSeed()
517
+ await rbac.addUserRoleByName(odid, 'admin')
531
518
 
532
519
  console.log('✓ Admin user created successfully')
533
520
  console.log(\` Email: ${email}\`)
534
521
  console.log(\` Role: admin\`)
535
- console.log('')
536
- console.log('You can now login at: http://localhost:8787/auth/login')
537
522
  } catch (error) {
538
523
  console.error('❌ Error creating admin user:', error)
539
524
  await dispose()
540
525
  process.exit(1)
541
526
  }
542
527
 
543
- // Clean up the platform proxy
544
528
  await dispose()
545
529
  }
546
530
 
547
- // Run seed
548
531
  seed()
549
532
  .then(() => {
550
- console.log('')
551
533
  console.log('✓ Seeding complete')
552
534
  process.exit(0)
553
535
  })
@@ -607,11 +589,19 @@ Or they should be automatically available after installation.
607
589
  }
608
590
 
609
591
  async function createCloudflareResources(databaseName, bucketName, targetDir) {
610
- // Check if wrangler is installed
592
+ // Resolve wrangler binary — prefer global, fall back to npx
593
+ let wranglerCmd = 'wrangler'
594
+ let wranglerArgs = []
611
595
  try {
612
596
  await execa('wrangler', ['--version'], { cwd: targetDir })
613
597
  } catch {
614
- throw new Error('wrangler is not installed. Install with: npm install -g wrangler')
598
+ try {
599
+ await execa('npx', ['--yes', 'wrangler', '--version'], { cwd: targetDir })
600
+ wranglerCmd = 'npx'
601
+ wranglerArgs = ['wrangler']
602
+ } catch {
603
+ throw new Error('wrangler is not installed. Install with: npm install -g wrangler')
604
+ }
615
605
  }
616
606
 
617
607
  let databaseId
@@ -620,7 +610,7 @@ async function createCloudflareResources(databaseName, bucketName, targetDir) {
620
610
 
621
611
  // Create D1 database
622
612
  try {
623
- const { stdout, stderr } = await execa('wrangler', ['d1', 'create', databaseName], {
613
+ const { stdout, stderr } = await execa(wranglerCmd, [...wranglerArgs, 'd1', 'create', databaseName], {
624
614
  cwd: targetDir
625
615
  })
626
616
 
@@ -648,7 +638,7 @@ async function createCloudflareResources(databaseName, bucketName, targetDir) {
648
638
 
649
639
  // Create R2 bucket
650
640
  try {
651
- await execa('wrangler', ['r2', 'bucket', 'create', bucketName], {
641
+ await execa(wranglerCmd, [...wranglerArgs, 'r2', 'bucket', 'create', bucketName], {
652
642
  cwd: targetDir
653
643
  })
654
644
  bucketCreated = true
@@ -774,49 +764,25 @@ function printSuccessMessage(answers) {
774
764
  console.log()
775
765
  console.log(kleur.bold().green('🎉 Success!'))
776
766
  console.log()
777
- console.log(kleur.bold('Next steps:'))
767
+ console.log(kleur.bold('Get started:'))
778
768
  console.log()
779
769
  console.log(kleur.cyan(` cd ${projectName}`))
780
770
 
781
771
  if (skipInstall) {
782
772
  console.log(kleur.cyan(' npm install'))
783
773
  console.log()
784
- console.log(kleur.yellow('⚠ Important: After npm install, copy migrations:'))
774
+ console.log(kleur.yellow('⚠ After npm install, copy migrations:'))
785
775
  console.log(kleur.dim(' cp -r node_modules/@sonicjs-cms/core/migrations ./'))
786
776
  }
787
777
 
788
- // Show resource creation steps if they weren't created or failed
789
- if (!createResources || !resourcesCreated) {
790
- console.log()
791
- console.log(kleur.bold('Create Cloudflare resources:'))
792
- if (!databaseIdSet) {
793
- console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
794
- console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
795
- }
796
- console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
778
+ if (!migrationsRan) {
779
+ console.log(kleur.cyan(' npm run db:migrate:local'))
797
780
  }
798
781
 
799
- // Only show migration/seed steps if they weren't completed (shouldn't normally happen)
800
- const needsMigrations = !migrationsRan
801
- const needsSeeding = seedAdmin && !adminSeeded
802
-
803
- if (needsMigrations || needsSeeding) {
804
- console.log()
805
- console.log(kleur.bold('Complete setup:'))
806
- if (needsMigrations) {
807
- console.log(kleur.cyan(' npm run db:migrate:local'))
808
- }
809
- if (needsSeeding) {
810
- console.log(kleur.cyan(' npm run seed'))
811
- }
782
+ if (seedAdmin && !adminSeeded) {
783
+ console.log(kleur.cyan(' npm run seed'))
812
784
  }
813
785
 
814
- console.log()
815
- if (migrationsRan && (!seedAdmin || adminSeeded)) {
816
- console.log(kleur.bold().green('✓ Database is ready! Start development:'))
817
- } else {
818
- console.log(kleur.bold('Start development:'))
819
- }
820
786
  console.log(kleur.cyan(' npm run dev'))
821
787
 
822
788
  if (seedAdmin && answers.adminEmail) {
@@ -826,15 +792,14 @@ function printSuccessMessage(answers) {
826
792
  console.log(kleur.dim(` Password: [as entered]`))
827
793
  }
828
794
 
829
- if (migrationsRan && (!seedAdmin || adminSeeded)) {
830
- console.log()
831
- console.log(kleur.green('✓ Everything is set up! Just run npm run dev and login.'))
832
- }
833
-
834
795
  console.log()
835
796
  console.log(kleur.bold('Visit:'))
836
797
  console.log(kleur.cyan(' http://localhost:8787/admin'))
837
798
 
799
+ console.log()
800
+ console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
801
+ console.log(kleur.cyan(' npm run deploy'))
802
+
838
803
  console.log()
839
804
  console.log(kleur.dim('Need help? Visit https://sonicjs.com'))
840
805
  console.log()
package/src/telemetry.js CHANGED
@@ -76,6 +76,8 @@ async function initTelemetry() {
76
76
  }
77
77
  }
78
78
 
79
+ const pendingRequests = []
80
+
79
81
  /**
80
82
  * Track an event using custom SonicJS stats endpoint
81
83
  */
@@ -105,12 +107,13 @@ async function track(event, properties = {}) {
105
107
  }
106
108
  }
107
109
 
108
- // Fire and forget - don't block on response
109
- fetch(`${TELEMETRY_ENDPOINT}/v1/events`, {
110
+ const req = fetch(`${TELEMETRY_ENDPOINT}/v1/events`, {
110
111
  method: 'POST',
111
112
  headers: { 'Content-Type': 'application/json' },
112
113
  body: JSON.stringify(payload)
113
- }).catch(() => {}) // Silent fail
114
+ }).catch(() => {})
115
+
116
+ pendingRequests.push(req)
114
117
 
115
118
  if (DEBUG) {
116
119
  console.log('[Telemetry] Tracked:', event, payload)
@@ -124,10 +127,12 @@ async function track(event, properties = {}) {
124
127
  }
125
128
 
126
129
  /**
127
- * Shutdown telemetry (no-op for fetch-based telemetry)
130
+ * Shutdown telemetry — flush pending requests (max 3s wait)
128
131
  */
129
132
  async function shutdown() {
130
- // No-op - fetch requests are fire and forget
133
+ if (pendingRequests.length === 0) return
134
+ const timeout = new Promise(resolve => setTimeout(resolve, 3000))
135
+ await Promise.race([Promise.all(pendingRequests), timeout])
131
136
  }
132
137
 
133
138
  /**
@@ -15,7 +15,8 @@
15
15
  "test": "vitest --run",
16
16
  "test:watch": "vitest",
17
17
  "update": "npm install @sonicjs-cms/core@latest --save",
18
- "update:beta": "npm install @sonicjs-cms/core@beta --save"
18
+ "update:beta": "npm install @sonicjs-cms/core@beta --save",
19
+ "update:plugin": "tsx scripts/update-plugin.ts"
19
20
  },
20
21
  "dependencies": {},
21
22
  "devDependencies": {
@@ -0,0 +1,101 @@
1
+ /**
2
+ * update-plugin.ts
3
+ *
4
+ * Downloads the latest version of a bundled plugin from the SonicJS starter
5
+ * template on GitHub and overwrites the local copies.
6
+ *
7
+ * Usage:
8
+ * npx tsx scripts/update-plugin.ts [plugin-name]
9
+ * npx tsx scripts/update-plugin.ts example # default
10
+ * npx tsx scripts/update-plugin.ts example --dry-run
11
+ *
12
+ * Why this exists:
13
+ * `create-sonicjs` scaffolds plugin source files into your project — they are
14
+ * your code, not a package dependency. Running `npm install` updates
15
+ * @sonicjs-cms/core but leaves the plugin files untouched. This script pulls
16
+ * the latest files from the canonical starter template on GitHub main and
17
+ * overwrites your local copies, giving you bug fixes and improvements without
18
+ * starting a fresh project.
19
+ *
20
+ * Caution:
21
+ * This overwrites the plugin files — commit or stash local changes first.
22
+ * Custom modifications you've made will be lost.
23
+ */
24
+
25
+ import { writeFileSync, mkdirSync } from 'fs'
26
+ import { dirname, join } from 'path'
27
+ import { fileURLToPath } from 'url'
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url))
30
+ const PROJECT_ROOT = join(__dirname, '..')
31
+
32
+ const GITHUB_RAW_BASE =
33
+ 'https://raw.githubusercontent.com/SonicJs-Org/sonicjs/main/packages/create-app/templates/starter/src/plugins'
34
+
35
+ // Map of plugin name → files relative to its plugin directory.
36
+ // Add new plugins here as they are added to the starter template.
37
+ const PLUGIN_MANIFEST: Record<string, string[]> = {
38
+ example: [
39
+ 'index.ts',
40
+ 'routes/api.ts',
41
+ 'routes/admin.ts',
42
+ 'collections/moods.collection.ts',
43
+ ],
44
+ }
45
+
46
+ async function fetchText(url: string): Promise<string> {
47
+ const res = await fetch(url)
48
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`)
49
+ return res.text()
50
+ }
51
+
52
+ async function updatePlugin(pluginName: string, dryRun: boolean) {
53
+ const files = PLUGIN_MANIFEST[pluginName]
54
+ if (!files) {
55
+ console.error(
56
+ `Unknown plugin "${pluginName}". Available: ${Object.keys(PLUGIN_MANIFEST).join(', ')}`
57
+ )
58
+ process.exit(1)
59
+ }
60
+
61
+ console.log(
62
+ `\n${dryRun ? '[dry-run] ' : ''}Updating plugin: ${pluginName} from SonicJS main branch\n`
63
+ )
64
+
65
+ for (const file of files) {
66
+ const url = `${GITHUB_RAW_BASE}/${pluginName}/${file}`
67
+ const localPath = join(PROJECT_ROOT, 'src', 'plugins', pluginName, file)
68
+
69
+ process.stdout.write(` fetching ${file} ... `)
70
+
71
+ try {
72
+ const content = await fetchText(url)
73
+ if (!dryRun) {
74
+ mkdirSync(dirname(localPath), { recursive: true })
75
+ writeFileSync(localPath, content, 'utf8')
76
+ }
77
+ console.log('✓')
78
+ } catch (err) {
79
+ console.log('✗')
80
+ console.error(` Error: ${(err as Error).message}`)
81
+ process.exit(1)
82
+ }
83
+ }
84
+
85
+ console.log(
86
+ `\n${dryRun ? '[dry-run] no files written — ' : ''}Done. ${files.length} file(s) updated.\n`
87
+ )
88
+
89
+ if (!dryRun) {
90
+ console.log('Next steps:')
91
+ console.log(' 1. Review the changes: git diff src/plugins/' + pluginName)
92
+ console.log(' 2. Run type check: npm run type-check')
93
+ console.log(' 3. Commit if happy: git add src/plugins/' + pluginName + ' && git commit -m "chore(plugins): update ' + pluginName + ' plugin"\n')
94
+ }
95
+ }
96
+
97
+ const args = process.argv.slice(2)
98
+ const pluginName = args.find(a => !a.startsWith('--')) ?? 'example'
99
+ const dryRun = args.includes('--dry-run')
100
+
101
+ updatePlugin(pluginName, dryRun)
@@ -4,11 +4,12 @@
4
4
  * Example collection configuration for blog posts
5
5
  */
6
6
 
7
- import type { CollectionConfig } from '@sonicjs-cms/core'
7
+ import type { CollectionConfig } from '@sonicjs-cms/core';
8
8
 
9
9
  export default {
10
- name: 'blog-posts',
11
- displayName: 'Blog Posts',
10
+ name: 'blog_post',
11
+ displayName: 'Blog Post',
12
+ slug: 'blog-posts',
12
13
  description: 'Manage your blog posts',
13
14
  icon: '📝',
14
15
 
@@ -19,57 +20,52 @@ export default {
19
20
  type: 'string',
20
21
  title: 'Title',
21
22
  required: true,
22
- maxLength: 200
23
+ maxLength: 200,
23
24
  },
24
25
  slug: {
25
26
  type: 'slug',
26
27
  title: 'URL Slug',
27
28
  required: true,
28
- maxLength: 200
29
- },
30
- excerpt: {
31
- type: 'textarea',
32
- title: 'Excerpt',
33
- maxLength: 500,
34
- helpText: 'A short summary of the post'
29
+ maxLength: 200,
35
30
  },
36
31
  content: {
37
- type: 'quill',
32
+ type: 'lexical',
38
33
  title: 'Content',
39
- required: true
40
- },
41
- featuredImage: {
42
- type: 'media',
43
- title: 'Featured Image'
34
+ required: true,
44
35
  },
45
36
  author: {
46
- type: 'string',
37
+ type: 'user',
47
38
  title: 'Author',
48
- required: true
39
+ required: true,
49
40
  },
50
41
  publishedAt: {
51
42
  type: 'datetime',
52
- title: 'Published Date'
53
- },
54
- status: {
55
- type: 'select',
56
- title: 'Status',
57
- enum: ['draft', 'published', 'archived'],
58
- enumLabels: ['Draft', 'Published', 'Archived'],
59
- default: 'draft'
43
+ title: 'Published Date',
60
44
  },
61
- tags: {
62
- type: 'string',
63
- title: 'Tags',
64
- helpText: 'Comma-separated tags'
65
- }
66
45
  },
67
- required: ['title', 'slug', 'content', 'author']
46
+ required: ['title', 'slug', 'content', 'author'],
68
47
  },
69
48
 
70
49
  // List view configuration
71
50
  listFields: ['title', 'author', 'status', 'publishedAt'],
72
- searchFields: ['title', 'excerpt', 'author'],
51
+ searchFields: ['title', 'content', 'author'],
73
52
  defaultSort: 'createdAt',
74
- defaultSortOrder: 'desc'
75
- } satisfies CollectionConfig
53
+ defaultSortOrder: 'desc',
54
+
55
+ // Mark as config-managed (code-based) collection
56
+ managed: true,
57
+ isActive: true,
58
+
59
+ // Opt in to public read access. Without this, only authenticated users
60
+ // (admin/editor) can read content via the API. See docs/authentication.md.
61
+ access: {
62
+ public: ['read'],
63
+ },
64
+
65
+ // Per-collection cache override. TTL in seconds; falls back to the cache plugin
66
+ // default (CACHE_CONFIGS.api.ttl, currently 300s) if unset.
67
+ cache: {
68
+ enabled: true,
69
+ ttl: 5,
70
+ },
71
+ } satisfies CollectionConfig;