create-nextblock 0.2.57 → 0.2.59

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.
@@ -55,13 +55,18 @@ const PACKAGE_VERSION_SOURCES = {
55
55
  program
56
56
  .name('create-nextblock')
57
57
  .description('Bootstrap a NextBlock CMS project')
58
- .argument('[project-directory]', 'The name of the project directory to create')
58
+ .argument(
59
+ '[project-directory]',
60
+ 'The name of the project directory to create',
61
+ )
59
62
  .option('--skip-install', 'Skip installing dependencies')
60
63
  .option('-y, --yes', 'Skip all interactive prompts and use defaults')
61
64
  .action(handleCommand);
62
65
 
63
66
  await program.parseAsync(process.argv).catch((error) => {
64
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
67
+ console.error(
68
+ chalk.red(error instanceof Error ? error.message : String(error)),
69
+ );
65
70
  process.exit(1);
66
71
  });
67
72
 
@@ -74,12 +79,17 @@ async function handleCommand(projectDirectory, options) {
74
79
  if (!projectName) {
75
80
  if (yes) {
76
81
  projectName = DEFAULT_PROJECT_NAME;
77
- console.log(chalk.blue(`Using default project name because --yes was provided: ${projectName}`));
82
+ console.log(
83
+ chalk.blue(
84
+ `Using default project name because --yes was provided: ${projectName}`,
85
+ ),
86
+ );
78
87
  } else {
79
88
  const projectPrompt = await clack.text({
80
89
  message: 'What is your project named?',
81
90
  initialValue: DEFAULT_PROJECT_NAME,
82
- validate: (value) => (!value ? 'Project name is required' : undefined),
91
+ validate: (value) =>
92
+ !value ? 'Project name is required' : undefined,
83
93
  });
84
94
  if (clack.isCancel(projectPrompt)) {
85
95
  handleWizardCancel('Setup cancelled.');
@@ -156,17 +166,27 @@ async function handleCommand(projectDirectory, options) {
156
166
  if (!yes) {
157
167
  await runSetupWizard(projectDir, projectName);
158
168
  } else {
159
- console.log(chalk.yellow('Skipping interactive setup wizard because --yes was provided.'));
169
+ console.log(
170
+ chalk.yellow(
171
+ 'Skipping interactive setup wizard because --yes was provided.',
172
+ ),
173
+ );
160
174
  }
161
175
 
162
176
  await initializeGit(projectDir);
163
177
 
164
- console.log(chalk.green(`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`));
178
+ console.log(
179
+ chalk.green(
180
+ `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
181
+ ),
182
+ );
165
183
  console.log(chalk.cyan('Next step:'));
166
184
  console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
167
185
  } catch (error) {
168
186
  console.error(
169
- chalk.red(error instanceof Error ? error.message : 'An unexpected error occurred'),
187
+ chalk.red(
188
+ error instanceof Error ? error.message : 'An unexpected error occurred',
189
+ ),
170
190
  );
171
191
  process.exit(1);
172
192
  }
@@ -183,10 +203,10 @@ async function runSetupWizard(projectDir, projectName) {
183
203
  await resetSupabaseProjectRef(projectPath);
184
204
 
185
205
  clack.note(
186
- "Before proceeding, ensure you have a Supabase project ready.\n\n" +
187
- "1. Supabase Cloud: Create a project at https://supabase.com/dashboard\n" +
188
- "2. Vercel Storage: If created via Vercel > Storage, check your .env.local snippet on Vercel for keys.",
189
- "Supabase Prerequisites"
206
+ 'Before proceeding, ensure you have a Supabase project ready.\n\n' +
207
+ '1. Supabase Cloud: Create a project at https://supabase.com/dashboard\n' +
208
+ '2. Vercel Storage: If created via Vercel > Storage, check your .env.local snippet on Vercel for keys.',
209
+ 'Supabase Prerequisites',
190
210
  );
191
211
 
192
212
  clack.note('Connecting to Supabase...');
@@ -231,7 +251,10 @@ async function runSetupWizard(projectDir, projectName) {
231
251
  }
232
252
  const siteUrl = siteUrlPrompt.trim();
233
253
 
234
- clack.note('Please go to your Supabase project dashboard to get the following secrets.');
254
+ clack.note(
255
+ 'Please go to your Supabase project dashboard to get the following secrets.\n' +
256
+ 'Note: For "Access Token", go to Account > Access Tokens > Generate New Token.',
257
+ );
235
258
  const supabaseKeys = await clack.group(
236
259
  {
237
260
  postgresUrl: () =>
@@ -239,7 +262,8 @@ async function runSetupWizard(projectDir, projectName) {
239
262
  message:
240
263
  'What is your Connection String? (Supabase: Project Dashboard > Connect (Top Left) > Connection String > URI | Vercel: POSTGRES_URL)',
241
264
  placeholder: 'postgresql://...',
242
- validate: (val) => (!val ? 'Connection string is required' : undefined),
265
+ validate: (val) =>
266
+ !val ? 'Connection string is required' : undefined,
243
267
  }),
244
268
  anonKey: () =>
245
269
  clack.password({
@@ -251,7 +275,15 @@ async function runSetupWizard(projectDir, projectName) {
251
275
  clack.password({
252
276
  message:
253
277
  'What is your Service Role Key (service_role key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_SERVICE_ROLE_KEY)',
254
- validate: (val) => (!val ? 'Service Role Key is required' : undefined),
278
+ validate: (val) =>
279
+ !val ? 'Service Role Key is required' : undefined,
280
+ }),
281
+ accessToken: () =>
282
+ clack.password({
283
+ message:
284
+ 'What is your Personal Access Token? (Supabase: Account > Access Tokens). Required for deployment.',
285
+ validate: (val) =>
286
+ !val ? 'Access Token is required for deployment' : undefined,
255
287
  }),
256
288
  },
257
289
  { onCancel: () => handleWizardCancel('Setup cancelled.') },
@@ -272,7 +304,8 @@ async function runSetupWizard(projectDir, projectName) {
272
304
 
273
305
  if (!dbPassword) {
274
306
  const passwordPrompt = await clack.password({
275
- message: 'Could not extract password from URL. What is your Database Password?',
307
+ message:
308
+ 'Could not extract password from URL. What is your Database Password?',
276
309
  validate: (val) => (!val ? 'Password is required' : undefined),
277
310
  });
278
311
  if (clack.isCancel(passwordPrompt)) {
@@ -284,11 +317,15 @@ async function runSetupWizard(projectDir, projectName) {
284
317
  const envPath = resolve(projectPath, '.env');
285
318
  const appendEnvBlock = async (label, lines) => {
286
319
  const normalized = lines.join('\n');
287
- const blockContent = normalized.endsWith('\n') ? normalized : `${normalized}\n`;
320
+ const blockContent = normalized.endsWith('\n')
321
+ ? normalized
322
+ : `${normalized}\n`;
288
323
  if (canWriteEnv) {
289
324
  await fs.appendFile(envPath, blockContent);
290
325
  } else {
291
- clack.note(`Add the following ${label} values to your existing .env:\n${blockContent}`);
326
+ clack.note(
327
+ `Add the following ${label} values to your existing .env:\n${blockContent}`,
328
+ );
292
329
  }
293
330
  };
294
331
  const envLines = [
@@ -298,6 +335,7 @@ async function runSetupWizard(projectDir, projectName) {
298
335
  `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
299
336
  `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
300
337
  `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
338
+ `SUPABASE_ACCESS_TOKEN=${supabaseKeys.accessToken}`,
301
339
  `POSTGRES_URL=${postgresUrl}`,
302
340
  '',
303
341
  '# Revalidation',
@@ -316,7 +354,9 @@ async function runSetupWizard(projectDir, projectName) {
316
354
  }
317
355
  if (!overwrite) {
318
356
  canWriteEnv = false;
319
- clack.note('Keeping existing .env. Add/merge the generated values manually.');
357
+ clack.note(
358
+ 'Keeping existing .env. Add/merge the generated values manually.',
359
+ );
320
360
  }
321
361
  }
322
362
 
@@ -353,7 +393,7 @@ async function runSetupWizard(projectDir, projectName) {
353
393
  const linkArgs = supabaseBin === 'npx' ? ['supabase', 'link'] : ['link'];
354
394
  linkArgs.push('--project-ref', projectId);
355
395
  linkArgs.push('--password', dbPassword);
356
-
396
+
357
397
  await execa(command, linkArgs, {
358
398
  stdio: 'inherit',
359
399
  cwd: projectPath,
@@ -361,7 +401,8 @@ async function runSetupWizard(projectDir, projectName) {
361
401
 
362
402
  // 2. Push the schema using the linked state
363
403
  dbPushSpinner.message('Pushing database schema...');
364
- const pushArgs = supabaseBin === 'npx' ? ['supabase', 'db', 'push'] : ['db', 'push'];
404
+ const pushArgs =
405
+ supabaseBin === 'npx' ? ['supabase', 'db', 'push'] : ['db', 'push'];
365
406
  pushArgs.push('--include-all');
366
407
 
367
408
  await execa(command, pushArgs, {
@@ -373,10 +414,31 @@ async function runSetupWizard(projectDir, projectName) {
373
414
  SUPABASE_DB_PASSWORD: dbPassword,
374
415
  },
375
416
  });
376
- dbPushSpinner.stop('Database schema pushed successfully!');
417
+
418
+ // 3. Push the config (for Auth settings like site_url)
419
+ dbPushSpinner.message('Pushing Supabase config (auth settings)...');
420
+ const configPushArgs =
421
+ supabaseBin === 'npx'
422
+ ? ['supabase', 'config', 'push']
423
+ : ['config', 'push'];
424
+
425
+ await execa(command, configPushArgs, {
426
+ stdio: ['pipe', 'inherit', 'inherit'],
427
+ cwd: projectPath,
428
+ env: {
429
+ ...process.env,
430
+ SUPABASE_DB_PASSWORD: dbPassword,
431
+ // Ensure NEXT_PUBLIC_URL is available for env() substitution in config.toml
432
+ NEXT_PUBLIC_URL: siteUrl,
433
+ },
434
+ });
435
+
436
+ dbPushSpinner.stop('Database schema and config pushed successfully!');
377
437
  }
378
438
  } catch (error) {
379
- dbPushSpinner.stop('Database push failed. Please run `npx supabase db push` manually.');
439
+ dbPushSpinner.stop(
440
+ 'Database push failed. Please run `npx supabase db push` manually.',
441
+ );
380
442
  if (error instanceof Error) {
381
443
  clack.note(error.message);
382
444
  }
@@ -386,7 +448,8 @@ async function runSetupWizard(projectDir, projectName) {
386
448
  'Optional Cloudflare R2 Setup:\nHave your Account ID, API token (Access + Secret), bucket name, and public bucket URL handy if you want media storage ready now.',
387
449
  );
388
450
  const setupR2 = await clack.confirm({
389
- message: 'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
451
+ message:
452
+ 'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
390
453
  });
391
454
  if (clack.isCancel(setupR2)) {
392
455
  handleWizardCancel('Setup cancelled.');
@@ -401,14 +464,17 @@ async function runSetupWizard(projectDir, projectName) {
401
464
  };
402
465
 
403
466
  if (setupR2) {
404
- clack.note('I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.');
467
+ clack.note(
468
+ 'I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.',
469
+ );
405
470
  await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
406
471
 
407
472
  const r2Keys = await clack.group(
408
473
  {
409
474
  accountId: () =>
410
475
  clack.text({
411
- message: 'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
476
+ message:
477
+ 'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
412
478
  validate: (val) => (!val ? 'Account ID is required' : undefined),
413
479
  }),
414
480
  bucketName: () =>
@@ -424,13 +490,15 @@ async function runSetupWizard(projectDir, projectName) {
424
490
  secretKey: () =>
425
491
  clack.password({
426
492
  message: 'R2: Paste your Secret Access Key:',
427
- validate: (val) => (!val ? 'Secret Access Key is required' : undefined),
493
+ validate: (val) =>
494
+ !val ? 'Secret Access Key is required' : undefined,
428
495
  }),
429
496
  publicBaseUrl: () =>
430
497
  clack.text({
431
498
  message:
432
499
  'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
433
- validate: (val) => (!val ? 'Public base URL is required' : undefined),
500
+ validate: (val) =>
501
+ !val ? 'Public base URL is required' : undefined,
434
502
  }),
435
503
  },
436
504
  { onCancel: () => handleWizardCancel('Setup cancelled.') },
@@ -458,7 +526,9 @@ async function runSetupWizard(projectDir, projectName) {
458
526
  if (setupR2) {
459
527
  clack.note('Cloudflare R2 configuration saved!');
460
528
  } else if (canWriteEnv) {
461
- clack.note('Cloudflare R2 placeholders added to .env. Configure them later when ready.');
529
+ clack.note(
530
+ 'Cloudflare R2 placeholders added to .env. Configure them later when ready.',
531
+ );
462
532
  }
463
533
 
464
534
  clack.note(
@@ -541,10 +611,14 @@ async function runSetupWizard(projectDir, projectName) {
541
611
  if (setupSMTP) {
542
612
  clack.note('SMTP configuration saved!');
543
613
  } else if (canWriteEnv) {
544
- clack.note('SMTP placeholders added to .env. Configure them later when ready.');
614
+ clack.note(
615
+ 'SMTP placeholders added to .env. Configure them later when ready.',
616
+ );
545
617
  }
546
618
 
547
- clack.outro(`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`);
619
+ clack.outro(
620
+ `🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`,
621
+ );
548
622
  }
549
623
 
550
624
  function handleWizardCancel(message) {
@@ -560,7 +634,9 @@ async function ensureEmptyDirectory(projectDir) {
560
634
 
561
635
  const contents = await fs.readdir(projectDir);
562
636
  if (contents.length > 0) {
563
- throw new Error(`Directory "${projectDir}" already exists and is not empty.`);
637
+ throw new Error(
638
+ `Directory "${projectDir}" already exists and is not empty.`,
639
+ );
564
640
  }
565
641
  }
566
642
 
@@ -776,11 +852,14 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
776
852
  const destSupabaseDir = resolve(projectDir, 'supabase');
777
853
  await fs.ensureDir(destSupabaseDir);
778
854
 
779
- const { dir: packageSupabaseDir, triedPaths } = await resolvePackageSupabaseDir(projectDir);
855
+ const { dir: packageSupabaseDir, triedPaths } =
856
+ await resolvePackageSupabaseDir(projectDir);
780
857
  if (!packageSupabaseDir) {
781
858
  const message =
782
859
  'Unable to locate supabase assets in @nextblock-cms/db. Please ensure dependencies are installed.' +
783
- (triedPaths.length > 0 ? `\nChecked:\n - ${triedPaths.join('\n - ')}` : '');
860
+ (triedPaths.length > 0
861
+ ? `\nChecked:\n - ${triedPaths.join('\n - ')}`
862
+ : '');
784
863
  if (required) {
785
864
  throw new Error(message);
786
865
  } else {
@@ -795,14 +874,20 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
795
874
  const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
796
875
  const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
797
876
  if (await fs.pathExists(sourceConfigPath)) {
798
- await fs.copy(sourceConfigPath, destinationConfigPath, { overwrite: true, errorOnExist: false });
877
+ await fs.copy(sourceConfigPath, destinationConfigPath, {
878
+ overwrite: true,
879
+ errorOnExist: false,
880
+ });
799
881
  configCopied = true;
800
882
  }
801
883
 
802
884
  const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
803
885
  const destMigrations = resolve(destSupabaseDir, 'migrations');
804
886
  if (await fs.pathExists(sourceMigrations)) {
805
- await fs.copy(sourceMigrations, destMigrations, { overwrite: true, errorOnExist: false });
887
+ await fs.copy(sourceMigrations, destMigrations, {
888
+ overwrite: true,
889
+ errorOnExist: false,
890
+ });
806
891
  migrationsCopied = true;
807
892
  }
808
893
 
@@ -826,7 +911,12 @@ async function resolvePackageSupabaseDir(projectDir) {
826
911
  const triedPaths = [];
827
912
  const candidateBases = new Set();
828
913
 
829
- const installDir = resolve(projectDir, 'node_modules', '@nextblock-cms', 'db');
914
+ const installDir = resolve(
915
+ projectDir,
916
+ 'node_modules',
917
+ '@nextblock-cms',
918
+ 'db',
919
+ );
830
920
  candidateBases.add(installDir);
831
921
 
832
922
  const tryResolveFrom = (fromPath) => {
@@ -880,7 +970,12 @@ async function resolvePackageSupabaseDir(projectDir) {
880
970
  }
881
971
 
882
972
  async function readSupabaseProjectRef(projectDir) {
883
- const projectRefPath = resolve(projectDir, 'supabase', '.temp', 'project-ref');
973
+ const projectRefPath = resolve(
974
+ projectDir,
975
+ 'supabase',
976
+ '.temp',
977
+ 'project-ref',
978
+ );
884
979
  if (await fs.pathExists(projectRefPath)) {
885
980
  const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
886
981
  if (/^[a-z0-9]{20,}$/i.test(value)) {
@@ -937,31 +1032,32 @@ async function ensureClientProviders(projectDir) {
937
1032
  }
938
1033
 
939
1034
  let content = await fs.readFile(providersPath, 'utf8');
940
- const wrapperImportStatement = "import { TranslationsProvider } from '@nextblock-cms/utils';";
941
- const existingImportRegex =
942
- /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
943
- const legacyImportRegex =
944
- /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
945
-
946
- if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
947
- content = content
948
- .replace(existingImportRegex, wrapperImportStatement)
949
- .replace(legacyImportRegex, wrapperImportStatement);
950
- } else if (!content.includes(wrapperImportStatement)) {
951
- const lines = content.split(/\r?\n/);
952
- const firstImport = lines.findIndex((line) => line.startsWith('import'));
953
- const insertIndex = firstImport === -1 ? 0 : firstImport + 1;
954
- lines.splice(insertIndex, 0, wrapperImportStatement);
955
- content = lines.join('\n');
956
- }
957
-
958
- await fs.writeFile(providersPath, content);
959
-
960
- const wrapperPath = resolve(projectDir, 'lib/client-translations.tsx');
961
- if (await fs.pathExists(wrapperPath)) {
962
- await fs.remove(wrapperPath);
963
- }
1035
+ const wrapperImportStatement =
1036
+ "import { TranslationsProvider } from '@nextblock-cms/utils';";
1037
+ const existingImportRegex =
1038
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
1039
+ const legacyImportRegex =
1040
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
1041
+
1042
+ if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
1043
+ content = content
1044
+ .replace(existingImportRegex, wrapperImportStatement)
1045
+ .replace(legacyImportRegex, wrapperImportStatement);
1046
+ } else if (!content.includes(wrapperImportStatement)) {
1047
+ const lines = content.split(/\r?\n/);
1048
+ const firstImport = lines.findIndex((line) => line.startsWith('import'));
1049
+ const insertIndex = firstImport === -1 ? 0 : firstImport + 1;
1050
+ lines.splice(insertIndex, 0, wrapperImportStatement);
1051
+ content = lines.join('\n');
1052
+ }
1053
+
1054
+ await fs.writeFile(providersPath, content);
1055
+
1056
+ const wrapperPath = resolve(projectDir, 'lib/client-translations.tsx');
1057
+ if (await fs.pathExists(wrapperPath)) {
1058
+ await fs.remove(wrapperPath);
964
1059
  }
1060
+ }
965
1061
 
966
1062
  async function ensureEditorUtils(projectDir) {
967
1063
  const exists = await fs.pathExists(EDITOR_UTILS_SOURCE_DIR);
@@ -970,7 +1066,9 @@ async function ensureEditorUtils(projectDir) {
970
1066
  }
971
1067
 
972
1068
  const entries = await fs.readdir(EDITOR_UTILS_SOURCE_DIR);
973
- const utilNames = entries.filter((name) => name.endsWith('.ts')).map((name) => name.replace(/\.ts$/, ''));
1069
+ const utilNames = entries
1070
+ .filter((name) => name.endsWith('.ts'))
1071
+ .map((name) => name.replace(/\.ts$/, ''));
974
1072
 
975
1073
  if (utilNames.length === 0) {
976
1074
  return [];
@@ -989,7 +1087,10 @@ async function ensureEditorUtils(projectDir) {
989
1087
  }
990
1088
 
991
1089
  async function sanitizeBlockEditorImports(projectDir) {
992
- const blockEditorPath = resolve(projectDir, 'app/cms/blocks/components/BlockEditorArea.tsx');
1090
+ const blockEditorPath = resolve(
1091
+ projectDir,
1092
+ 'app/cms/blocks/components/BlockEditorArea.tsx',
1093
+ );
993
1094
  if (!(await fs.pathExists(blockEditorPath))) {
994
1095
  return;
995
1096
  }
@@ -1001,7 +1102,8 @@ async function sanitizeBlockEditorImports(projectDir) {
1001
1102
  ];
1002
1103
 
1003
1104
  const updated = replacements.reduce(
1004
- (current, { pattern, replacement }) => current.replace(pattern, replacement),
1105
+ (current, { pattern, replacement }) =>
1106
+ current.replace(pattern, replacement),
1005
1107
  content,
1006
1108
  );
1007
1109
 
@@ -1024,7 +1126,10 @@ async function sanitizeUiImports(projectDir) {
1024
1126
 
1025
1127
  for (const filePath of files) {
1026
1128
  const original = await fs.readFile(filePath, 'utf8');
1027
- const updated = original.replace(/@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g, '@nextblock-cms/ui');
1129
+ const updated = original.replace(
1130
+ /@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g,
1131
+ '@nextblock-cms/ui',
1132
+ );
1028
1133
  if (updated !== original) {
1029
1134
  await fs.writeFile(filePath, updated);
1030
1135
  }
@@ -1078,16 +1183,12 @@ async function sanitizeLayout(projectDir) {
1078
1183
  ];
1079
1184
 
1080
1185
  const content = await fs.readFile(layoutPath, 'utf8');
1081
- let updated = content.replace(
1082
- /import\s+['"]\.\/globals\.css['"];?\s*/g,
1083
- '',
1084
- );
1085
- updated = updated.replace(
1086
- /import\s+['"]\.\/editor\.css['"];?\s*/g,
1087
- '',
1088
- );
1186
+ let updated = content.replace(/import\s+['"]\.\/globals\.css['"];?\s*/g, '');
1187
+ updated = updated.replace(/import\s+['"]\.\/editor\.css['"];?\s*/g, '');
1089
1188
 
1090
- const missingImports = requiredImports.filter((statement) => !updated.includes(statement));
1189
+ const missingImports = requiredImports.filter(
1190
+ (statement) => !updated.includes(statement),
1191
+ );
1091
1192
  if (missingImports.length > 0) {
1092
1193
  updated = `${missingImports.join('\n')}\n${updated}`;
1093
1194
  }
@@ -1125,7 +1226,7 @@ async function ensureEditorStyles(projectDir) {
1125
1226
  if (
1126
1227
  content === '' ||
1127
1228
  content.startsWith('/* Editor styles placeholder') ||
1128
- content.includes("@nextblock-cms/editor/styles")
1229
+ content.includes('@nextblock-cms/editor/styles')
1129
1230
  ) {
1130
1231
  await fs.remove(filePath);
1131
1232
  }
@@ -1319,7 +1420,9 @@ async function transformPackageJson(projectDir) {
1319
1420
 
1320
1421
  packageJson.dependencies = packageJson.dependencies ?? {};
1321
1422
 
1322
- for (const [pkgName, manifestPath] of Object.entries(PACKAGE_VERSION_SOURCES)) {
1423
+ for (const [pkgName, manifestPath] of Object.entries(
1424
+ PACKAGE_VERSION_SOURCES,
1425
+ )) {
1323
1426
  if (pkgName in packageJson.dependencies) {
1324
1427
  const current = packageJson.dependencies[pkgName];
1325
1428
  if (typeof current === 'string' && current.startsWith('workspace:')) {
@@ -1410,7 +1513,9 @@ async function runSupabaseCli(args, options = {}) {
1410
1513
  if (code === 0) {
1411
1514
  resolve();
1412
1515
  } else {
1413
- reject(new Error(`supabase ${args.join(' ')} exited with code ${code}`));
1516
+ reject(
1517
+ new Error(`supabase ${args.join(' ')} exited with code ${code}`),
1518
+ );
1414
1519
  }
1415
1520
  });
1416
1521
  });
@@ -1431,7 +1536,11 @@ function buildNextConfigContent(editorUtilNames) {
1431
1536
 
1432
1537
  for (const moduleName of UI_PROXY_MODULES) {
1433
1538
  aliasLines.push(
1434
- " '@nextblock-cms/ui/" + moduleName + "': path.join(process.cwd(), 'lib/ui/" + moduleName + "'),",
1539
+ " '@nextblock-cms/ui/" +
1540
+ moduleName +
1541
+ "': path.join(process.cwd(), 'lib/ui/" +
1542
+ moduleName +
1543
+ "'),",
1435
1544
  );
1436
1545
  }
1437
1546
 
@@ -1455,17 +1564,17 @@ function buildNextConfigContent(editorUtilNames) {
1455
1564
  " * @type {import('next').NextConfig}",
1456
1565
  ' **/',
1457
1566
  'const nextConfig = {',
1458
- " outputFileTracingRoot: path.join(__dirname),",
1567
+ ' outputFileTracingRoot: path.join(__dirname),',
1459
1568
  ' env: {',
1460
- " NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,",
1461
- " NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,",
1569
+ ' NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,',
1570
+ ' NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,',
1462
1571
  ' },',
1463
1572
  ' images: {',
1464
1573
  " formats: ['image/avif', 'image/webp'],",
1465
1574
  ' imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],',
1466
1575
  ' deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],',
1467
1576
  ' minimumCacheTTL: 31536000,',
1468
- " dangerouslyAllowSVG: false,",
1577
+ ' dangerouslyAllowSVG: false,',
1469
1578
  " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
1470
1579
  ' remotePatterns: [',
1471
1580
  " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
@@ -1474,14 +1583,14 @@ function buildNextConfigContent(editorUtilNames) {
1474
1583
  ' ? [',
1475
1584
  ' {',
1476
1585
  " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
1477
- " hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,",
1586
+ ' hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,',
1478
1587
  ' },',
1479
1588
  ' ]',
1480
1589
  ' : []),',
1481
1590
  ' ],',
1482
1591
  ' },',
1483
1592
  ' experimental: {',
1484
- " optimizeCss: true,",
1593
+ ' optimizeCss: true,',
1485
1594
  " cssChunking: 'strict',",
1486
1595
  ' },',
1487
1596
  " transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],",
@@ -1519,8 +1628,8 @@ function buildNextConfigContent(editorUtilNames) {
1519
1628
  ' config.module = config.module || {};',
1520
1629
  ' config.module.rules = config.module.rules || [];',
1521
1630
  ' config.module.rules.push({',
1522
- " test: /\\.svg$/i,",
1523
- " issuer: /\\.[jt]sx?$/,",
1631
+ ' test: /\\.svg$/i,',
1632
+ ' issuer: /\\.[jt]sx?$/,',
1524
1633
  " use: ['@svgr/webpack'],",
1525
1634
  ' });',
1526
1635
  '',
@@ -1531,14 +1640,14 @@ function buildNextConfigContent(editorUtilNames) {
1531
1640
  ' cacheGroups: {',
1532
1641
  ' ...(((config.optimization ?? {}).splitChunks ?? {}).cacheGroups ?? {}),',
1533
1642
  ' tiptap: {',
1534
- " test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,",
1643
+ ' test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,',
1535
1644
  " name: 'tiptap',",
1536
1645
  " chunks: 'async',",
1537
1646
  ' priority: 30,',
1538
1647
  ' reuseExistingChunk: true,',
1539
1648
  ' },',
1540
1649
  ' tiptapExtensions: {',
1541
- " test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,",
1650
+ ' test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,',
1542
1651
  " name: 'tiptap-extensions',",
1543
1652
  " chunks: 'async',",
1544
1653
  ' priority: 25,',
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../../libs/sdk/tsconfig.lib.json"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.57",
3
+ "version": "0.2.59",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -31,7 +31,7 @@ export default function Login() {
31
31
  const formMessage = getMessage(searchParams);
32
32
 
33
33
  return (
34
- <form className="flex-1 flex flex-col min-w-64">
34
+ <form className="flex-1 flex flex-col min-w-64 mx-auto">
35
35
  <h1 className="text-2xl font-medium">{t('sign_in')}</h1>
36
36
  <p className="text-sm text-foreground">
37
37
  {t('dont_have_account')}{" "}
@@ -10,6 +10,8 @@ export const signUpAction = async (formData: FormData) => {
10
10
  const password = formData.get("password")?.toString();
11
11
  const supabase = await createClient();
12
12
  const origin = (await headers()).get("origin");
13
+ const nextPublicUrl = process.env.NEXT_PUBLIC_URL;
14
+ const redirectBase = nextPublicUrl ? `https://${nextPublicUrl}` : origin;
13
15
 
14
16
  if (!email || !password) {
15
17
  return encodedRedirect(
@@ -23,7 +25,7 @@ export const signUpAction = async (formData: FormData) => {
23
25
  email,
24
26
  password,
25
27
  options: {
26
- emailRedirectTo: `${origin}/auth/callback`,
28
+ emailRedirectTo: `${redirectBase}/auth/callback`,
27
29
  },
28
30
  });
29
31
 
@@ -72,6 +74,8 @@ export const forgotPasswordAction = async (formData: FormData) => {
72
74
  const email = formData.get("email")?.toString();
73
75
  const supabase = await createClient();
74
76
  const origin = (await headers()).get("origin");
77
+ const nextPublicUrl = process.env.NEXT_PUBLIC_URL;
78
+ const redirectBase = nextPublicUrl ? `https://${nextPublicUrl}` : origin;
75
79
  const callbackUrl = formData.get("callbackUrl")?.toString();
76
80
 
77
81
  if (!email) {
@@ -79,7 +83,7 @@ export const forgotPasswordAction = async (formData: FormData) => {
79
83
  }
80
84
 
81
85
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
82
- redirectTo: `${origin}/auth/callback?redirect_to=/reset-password`,
86
+ redirectTo: `${redirectBase}/auth/callback?redirect_to=/reset-password`,
83
87
  });
84
88
 
85
89
  if (error) {
@@ -1,12 +1,12 @@
1
- "use client";
2
-
3
- import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent } from "react";
1
+ import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent, useCallback } from "react";
4
2
  import { cn } from "@nextblock-cms/utils";
5
3
  import {
6
4
  Dialog,
7
5
  DialogContent,
8
6
  DialogTitle,
9
- DialogClose,
7
+ DialogHeader,
8
+ DialogFooter,
9
+ DialogDescription,
10
10
  } from "@nextblock-cms/ui";
11
11
  import { Button } from "@nextblock-cms/ui";
12
12
  import { blockRegistry, type BlockType } from "@/lib/blocks/blockRegistry";
@@ -46,12 +46,14 @@ export function BlockEditorModal({
46
46
  sectionBackground,
47
47
  }: BlockEditorModalProps) {
48
48
  const [tempContent, setTempContent] = useState(block.content);
49
+ const [showConfirmClose, setShowConfirmClose] = useState(false);
49
50
  const isValid = true; // Placeholder for future validation logic
50
51
 
51
52
  useEffect(() => {
52
53
  // When the modal is opened with a new block, reset the temp content
53
54
  if (isOpen) {
54
55
  setTempContent(block.content);
56
+ setShowConfirmClose(false);
55
57
  }
56
58
  }, [isOpen, block.content]);
57
59
 
@@ -59,6 +61,18 @@ export function BlockEditorModal({
59
61
  onSave(tempContent);
60
62
  };
61
63
 
64
+ const hasUnsavedChanges = useCallback(() => {
65
+ return JSON.stringify(block.content) !== JSON.stringify(tempContent);
66
+ }, [block.content, tempContent]);
67
+
68
+ const handleCloseAttempt = useCallback(() => {
69
+ if (hasUnsavedChanges()) {
70
+ setShowConfirmClose(true);
71
+ } else {
72
+ onClose();
73
+ }
74
+ }, [hasUnsavedChanges, onClose]);
75
+
62
76
  const handleContentChange = (newContent: unknown) => {
63
77
  setTempContent(newContent);
64
78
  // Potentially add validation here and set isValid
@@ -68,70 +82,107 @@ export function BlockEditorModal({
68
82
  const displayText = blockInfo?.label || "Block";
69
83
 
70
84
  return (
71
- <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
72
- <DialogContent
73
- className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
74
- onInteractOutside={(e) => {
75
- // Prevent closing when interacting with Tiptap bubbles outside the dialog portal (rare but possible)
76
- }}
77
- >
78
- {/* Header */}
79
- <div className="flex items-center justify-between p-4 border-b bg-background/95 backdrop-blur z-10">
80
- <div className="flex items-center gap-2">
81
- <DialogTitle className="text-lg font-semibold">Edit {displayText}</DialogTitle>
82
- </div>
83
- <div className="flex items-center gap-2">
84
- <DialogClose asChild>
85
- <Button variant="ghost" size="sm">Cancel</Button>
86
- </DialogClose>
87
- <Button onClick={handleSave} disabled={!isValid} size="sm">
88
- Save (CMD+S)
89
- </Button>
90
- </div>
91
- </div>
85
+ <>
86
+ <Dialog open={isOpen} onOpenChange={(open) => {
87
+ if (!open) {
88
+ handleCloseAttempt();
89
+ }
90
+ }}>
91
+ <DialogContent
92
+ className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
93
+ onInteractOutside={(e) => {
94
+ e.preventDefault();
95
+ handleCloseAttempt();
96
+ }}
97
+ onEscapeKeyDown={(e) => {
98
+ e.preventDefault();
99
+ handleCloseAttempt();
100
+ }}
101
+ >
102
+ {/* Header */}
103
+ <div className="flex items-center justify-between p-4 border-b bg-background/95 backdrop-blur z-10">
104
+ <div className="flex items-center gap-2">
105
+ <DialogTitle className="text-lg font-semibold">Edit {displayText}</DialogTitle>
106
+ </div>
107
+ <div className="flex items-center gap-2">
108
+ <Button variant="ghost" size="sm" onClick={handleCloseAttempt}>Cancel</Button>
109
+ <Button onClick={handleSave} disabled={!isValid} size="sm">
110
+ Save (CMD+S)
111
+ </Button>
112
+ </div>
113
+ </div>
114
+
115
+ {/* Editor Area with Contextual Background */}
116
+ <div
117
+ className={cn(
118
+ "flex-1 overflow-y-auto p-6",
119
+ // Conditional Background Logic:
120
+ // Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
121
+ // For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
122
+ (block.type === 'text' || block.type === 'heading') ? (
123
+ // If no specific background, use white/dark default
124
+ (!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
125
+ ) : "bg-muted/10", // Default for non-text blocks
92
126
 
93
- {/* Editor Area with Contextual Background */}
94
- <div
95
- className={cn(
96
- "flex-1 overflow-y-auto p-6",
97
- // Conditional Background Logic:
98
- // Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
99
- // For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
100
- (block.type === 'text' || block.type === 'heading') ? (
101
- // If no specific background, use white/dark default
102
- (!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
103
- ) : "bg-muted/10", // Default for non-text blocks
127
+ // Apply theme classes if present (ONLY for text/heading)
128
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
129
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
130
+ (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
131
+
132
+ // Dark mode prose invert if dark background (approximate check for solid color)
133
+ (block.type === 'text' || block.type === 'heading') && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert"
134
+ )}
135
+ style={{
136
+ // Only apply custom color/gradient styles for text/heading
137
+ backgroundColor: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
138
+ backgroundImage: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
139
+ `${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
140
+ : undefined
141
+ }}
142
+ >
143
+ <div className="max-w-6xl mx-auto">
144
+ <Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
145
+ <EditorComponent
146
+ block={block}
147
+ content={tempContent}
148
+ onChange={handleContentChange}
149
+ className="bg-transparent border-none shadow-none focus-within:ring-0 min-h-[60vh]" // Make editor transparent
150
+ sectionBackground={sectionBackground} // Pass down if editor supports it
151
+ />
152
+ </Suspense>
153
+ </div>
154
+ </div>
155
+
156
+ </DialogContent>
157
+ </Dialog>
104
158
 
105
- // Apply theme classes if present (ONLY for text/heading)
106
- (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
107
- (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
108
- (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
109
-
110
- // Dark mode prose invert if dark background (approximate check for solid color)
111
- (block.type === 'text' || block.type === 'heading') && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert"
112
- )}
113
- style={{
114
- // Only apply custom color/gradient styles for text/heading
115
- backgroundColor: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
116
- backgroundImage: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
117
- `${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
118
- : undefined
119
- }}
120
- >
121
- <div className="max-w-6xl mx-auto">
122
- <Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
123
- <EditorComponent
124
- block={block}
125
- content={tempContent}
126
- onChange={handleContentChange}
127
- className="bg-transparent border-none shadow-none focus-within:ring-0 min-h-[60vh]" // Make editor transparent
128
- sectionBackground={sectionBackground} // Pass down if editor supports it
129
- />
130
- </Suspense>
131
- </div>
132
- </div>
133
-
134
- </DialogContent>
135
- </Dialog>
159
+ <Dialog open={showConfirmClose} onOpenChange={setShowConfirmClose}>
160
+ <DialogContent className="sm:max-w-[425px]">
161
+ <DialogHeader>
162
+ <DialogTitle>Save Changes?</DialogTitle>
163
+ <DialogDescription>
164
+ You have unsaved changes. Do you want to save them before closing?
165
+ </DialogDescription>
166
+ </DialogHeader>
167
+ <DialogFooter className="gap-2 sm:gap-0">
168
+ <Button variant="ghost" onClick={() => setShowConfirmClose(false)}>
169
+ Cancel
170
+ </Button>
171
+ <Button variant="destructive" onClick={() => {
172
+ setShowConfirmClose(false);
173
+ onClose();
174
+ }}>
175
+ Discard
176
+ </Button>
177
+ <Button onClick={() => {
178
+ setShowConfirmClose(false);
179
+ handleSave();
180
+ }}>
181
+ Save
182
+ </Button>
183
+ </DialogFooter>
184
+ </DialogContent>
185
+ </Dialog>
186
+ </>
136
187
  );
137
188
  }
@@ -17,9 +17,7 @@ import { getTranslations } from '@/app/cms/settings/extra-translations/actions';
17
17
  import type { Database } from '@nextblock-cms/db';
18
18
  import { headers, cookies } from 'next/headers';
19
19
 
20
- const defaultUrl = process.env.NEXT_PUBLIC_URL
21
- ? `https://${process.env.NEXT_PUBLIC_URL}`
22
- : "http://localhost:3000";
20
+ const defaultUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3000";
23
21
 
24
22
  const DEFAULT_LOCALE_FOR_LAYOUT = 'en';
25
23
 
@@ -106,6 +104,28 @@ export const metadata: Metadata = {
106
104
  metadataBase: new URL(defaultUrl),
107
105
  title: 'Nextblock CMS',
108
106
  description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
107
+ openGraph: {
108
+ title: 'Nextblock CMS',
109
+ description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
110
+ url: defaultUrl,
111
+ siteName: 'Nextblock CMS',
112
+ images: [
113
+ {
114
+ url: '/images/metadata_image.webp',
115
+ width: 1200,
116
+ height: 630,
117
+ alt: 'Nextblock CMS',
118
+ },
119
+ ],
120
+ locale: 'en_US',
121
+ type: 'website',
122
+ },
123
+ twitter: {
124
+ card: 'summary_large_image',
125
+ title: 'Nextblock CMS',
126
+ description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
127
+ images: ['/images/metadata_image.webp'],
128
+ },
109
129
  icons: {
110
130
  icon: [
111
131
  { url: '/favicon/favicon.ico' },
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -24,7 +24,7 @@
24
24
  "js-cookie": "^3.0.5",
25
25
  "lodash.debounce": "^4.0.8",
26
26
  "lucide-react": "^0.534.0",
27
- "next": "^15.5.4",
27
+ "next": "15.5.9",
28
28
  "nodemailer": "^7.0.4",
29
29
  "plaiceholder": "^3.0.0",
30
30
  "react": "19.0.0",
@@ -189,14 +189,37 @@ export default async function proxy(request: NextRequest) {
189
189
  if (process.env.NODE_ENV === 'production') {
190
190
  const nonceValue = requestHeaders.get('x-nonce');
191
191
  if (nonceValue) {
192
+ const supabaseHostname = new URL(supabaseUrl).hostname;
193
+
194
+ const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
195
+ const r2PublicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
196
+ const r2BucketName = process.env.R2_BUCKET_NAME;
197
+
198
+ let r2Hostnames = '';
199
+ if (r2BaseUrl) {
200
+ try {
201
+ r2Hostnames += ` https://${new URL(r2BaseUrl).hostname}`;
202
+ } catch (e) {
203
+ console.error('Invalid NEXT_PUBLIC_R2_BASE_URL', e);
204
+ }
205
+ }
206
+ if (r2PublicUrl && r2BucketName) {
207
+ try {
208
+ const publicHostname = new URL(r2PublicUrl).hostname;
209
+ r2Hostnames += ` https://${r2BucketName}.${publicHostname}`;
210
+ } catch (e) {
211
+ console.error('Invalid NEXT_PUBLIC_R2_PUBLIC_URL', e);
212
+ }
213
+ }
214
+
192
215
  const csp = [
193
216
  "default-src 'self'",
194
217
  `script-src 'self' blob: data: 'nonce-${nonceValue}'`,
195
218
  "style-src 'self' 'unsafe-inline'",
196
- "img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
219
+ `img-src 'self' data: blob:${r2Hostnames}`,
197
220
  "font-src 'self'",
198
221
  "object-src 'none'",
199
- "connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
222
+ `connect-src 'self' https://${supabaseHostname} wss://${supabaseHostname}${r2Hostnames}`,
200
223
  "frame-src 'self' blob: data: https://www.youtube.com",
201
224
  "form-action 'self'",
202
225
  "base-uri 'self'",
@@ -37,7 +37,6 @@
37
37
  "**/*.jsx",
38
38
  "next-env.d.ts",
39
39
  ".next/types/**/*.ts",
40
- "../../dist/apps/nextblock/.next/types/**/*.ts",
41
40
  "../../libs/ui/tailwind.config.js"
42
41
  ],
43
42
  "exclude": [