@majordigital/create-acorn 1.2.0 → 1.3.0

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 CHANGED
@@ -10,6 +10,8 @@ It eliminates the repetitive setup that comes with every new project — framewo
10
10
  npx @majordigital/create-acorn@latest
11
11
  ```
12
12
 
13
+ > **Note:** Always use `npx` to run the CLI. Do not use `npm install` — this is a scaffolding tool, not a library dependency.
14
+
13
15
  The CLI prompts you for a project name, creates the directory, then walks you through selecting a headless CMS — scaffolding a complete, opinionated project in seconds.
14
16
 
15
17
  ```
@@ -220,6 +220,216 @@ async function setupPrismic(projectName) {
220
220
  console.log('');
221
221
  }
222
222
 
223
+ async function setupStoryblok(projectName) {
224
+ console.log('Setting up Storyblok...');
225
+ console.log('');
226
+
227
+ // Install Storyblok SDK
228
+ console.log('Installing @storyblok/react...');
229
+ await runCommand('npm', ['install', '--legacy-peer-deps', '@storyblok/react']);
230
+ console.log('');
231
+
232
+ // Update next.config.ts — replace Prismic image patterns with Storyblok
233
+ const nextConfigPath = join(process.cwd(), 'next.config.ts');
234
+ try {
235
+ let config = readFileSync(nextConfigPath, 'utf-8');
236
+ config = config.replace(
237
+ /remotePatterns:\s*\[[\s\S]*?\],/,
238
+ `remotePatterns: [
239
+ {
240
+ protocol: 'https',
241
+ hostname: 'a.storyblok.com',
242
+ port: '',
243
+ pathname: '/**',
244
+ },
245
+ ],`
246
+ );
247
+ writeFileSync(nextConfigPath, config);
248
+ console.log('Updated next.config.ts with Storyblok image domain.');
249
+ } catch {
250
+ console.log('Warning: Could not update next.config.ts image patterns.');
251
+ }
252
+
253
+ // Create src/lib/storyblok.ts
254
+ const libDir = join(process.cwd(), 'src', 'lib');
255
+ writeFileSync(join(libDir, 'storyblok.ts'), `import { apiPlugin, storyblokInit } from '@storyblok/react/rsc';
256
+
257
+ import Page from '@/components/storyblok/Page';
258
+ import Feature from '@/components/storyblok/Feature';
259
+ import Grid from '@/components/storyblok/Grid';
260
+ import Teaser from '@/components/storyblok/Teaser';
261
+
262
+ export const getStoryblokApi = storyblokInit({
263
+ accessToken: process.env.STORYBLOK_PREVIEW_TOKEN,
264
+ use: [apiPlugin],
265
+ components: {
266
+ page: Page,
267
+ feature: Feature,
268
+ grid: Grid,
269
+ teaser: Teaser,
270
+ },
271
+ apiOptions: {
272
+ region: 'eu',
273
+ },
274
+ });
275
+ `);
276
+ console.log('Created src/lib/storyblok.ts');
277
+
278
+ // Create StoryblokProvider
279
+ const componentsDir = join(process.cwd(), 'src', 'components');
280
+ mkdirSync(componentsDir, { recursive: true });
281
+ writeFileSync(join(componentsDir, 'StoryblokProvider.tsx'), `'use client';
282
+
283
+ import { getStoryblokApi } from '@/lib/storyblok';
284
+ import type { PropsWithChildren } from 'react';
285
+
286
+ export default function StoryblokProvider({ children }: PropsWithChildren) {
287
+ getStoryblokApi();
288
+ return children;
289
+ }
290
+ `);
291
+ console.log('Created src/components/StoryblokProvider.tsx');
292
+
293
+ // Create Storyblok component directory and base components
294
+ const sbComponentsDir = join(componentsDir, 'storyblok');
295
+ mkdirSync(sbComponentsDir, { recursive: true });
296
+
297
+ writeFileSync(join(sbComponentsDir, 'Page.tsx'), `import { storyblokEditable, StoryblokServerComponent } from '@storyblok/react/rsc';
298
+ import type { SbBlokData } from '@storyblok/react/rsc';
299
+
300
+ interface PageBlok extends SbBlokData {
301
+ body?: SbBlokData[];
302
+ }
303
+
304
+ export default function Page({ blok }: { blok: PageBlok }) {
305
+ return (
306
+ <main {...storyblokEditable(blok)}>
307
+ {blok.body?.map((nestedBlok) => (
308
+ <StoryblokServerComponent blok={nestedBlok} key={nestedBlok._uid} />
309
+ ))}
310
+ </main>
311
+ );
312
+ }
313
+ `);
314
+
315
+ writeFileSync(join(sbComponentsDir, 'Grid.tsx'), `import { storyblokEditable, StoryblokServerComponent } from '@storyblok/react/rsc';
316
+ import type { SbBlokData } from '@storyblok/react/rsc';
317
+
318
+ interface GridBlok extends SbBlokData {
319
+ columns?: SbBlokData[];
320
+ }
321
+
322
+ export default function Grid({ blok }: { blok: GridBlok }) {
323
+ return (
324
+ <div {...storyblokEditable(blok)} className="grid grid-cols-3 gap-6">
325
+ {blok.columns?.map((nestedBlok) => (
326
+ <StoryblokServerComponent blok={nestedBlok} key={nestedBlok._uid} />
327
+ ))}
328
+ </div>
329
+ );
330
+ }
331
+ `);
332
+
333
+ writeFileSync(join(sbComponentsDir, 'Feature.tsx'), `import { storyblokEditable } from '@storyblok/react/rsc';
334
+ import type { SbBlokData } from '@storyblok/react/rsc';
335
+
336
+ interface FeatureBlok extends SbBlokData {
337
+ name?: string;
338
+ }
339
+
340
+ export default function Feature({ blok }: { blok: FeatureBlok }) {
341
+ return (
342
+ <div {...storyblokEditable(blok)} className="feature">
343
+ <span>{blok.name}</span>
344
+ </div>
345
+ );
346
+ }
347
+ `);
348
+
349
+ writeFileSync(join(sbComponentsDir, 'Teaser.tsx'), `import { storyblokEditable } from '@storyblok/react/rsc';
350
+ import type { SbBlokData } from '@storyblok/react/rsc';
351
+
352
+ interface TeaserBlok extends SbBlokData {
353
+ headline?: string;
354
+ }
355
+
356
+ export default function Teaser({ blok }: { blok: TeaserBlok }) {
357
+ return (
358
+ <div {...storyblokEditable(blok)} className="teaser">
359
+ <h2>{blok.headline}</h2>
360
+ </div>
361
+ );
362
+ }
363
+ `);
364
+ console.log('Created Storyblok component boilerplate (Page, Grid, Feature, Teaser).');
365
+
366
+ // Update layout.tsx to wrap with StoryblokProvider
367
+ const layoutPath = join(process.cwd(), 'src', 'app', 'layout.tsx');
368
+ try {
369
+ let layout = readFileSync(layoutPath, 'utf-8');
370
+ // Add import
371
+ layout = layout.replace(
372
+ "import '@/styles/globals.css';",
373
+ "import '@/styles/globals.css';\n\nimport StoryblokProvider from '@/components/StoryblokProvider';"
374
+ );
375
+ // Wrap <html> with StoryblokProvider
376
+ layout = layout.replace(
377
+ '<html lang="en"',
378
+ '<StoryblokProvider>\n\t\t<html lang="en"'
379
+ );
380
+ layout = layout.replace(
381
+ '</html>',
382
+ '</html>\n\t\t</StoryblokProvider>'
383
+ );
384
+ writeFileSync(layoutPath, layout);
385
+ console.log('Updated layout.tsx with StoryblokProvider.');
386
+ } catch {
387
+ console.log('Warning: Could not update layout.tsx with StoryblokProvider.');
388
+ }
389
+
390
+ // Create a sample home page that fetches from Storyblok
391
+ writeFileSync(join(process.cwd(), 'src', 'app', 'page.tsx'), `import { getStoryblokApi } from '@/lib/storyblok';
392
+ import { StoryblokStory } from '@storyblok/react/rsc';
393
+
394
+ async function fetchData() {
395
+ const storyblokApi = getStoryblokApi();
396
+ return storyblokApi.get('cdn/stories/home', {
397
+ version: 'draft',
398
+ });
399
+ }
400
+
401
+ export default async function Home() {
402
+ const { data } = await fetchData();
403
+
404
+ return (
405
+ <div>
406
+ <StoryblokStory story={data.story} />
407
+ </div>
408
+ );
409
+ }
410
+ `);
411
+ console.log('Created sample home page with Storyblok data fetching.');
412
+
413
+ console.log('');
414
+ console.log('=== Storyblok Setup Complete ===');
415
+ console.log('');
416
+ console.log('=== Next Steps ===');
417
+ console.log('');
418
+ console.log(' 1. Create a Storyblok space at https://app.storyblok.com');
419
+ console.log('');
420
+ console.log(' 2. Copy your Preview token and add it to .env.local:');
421
+ console.log(' STORYBLOK_PREVIEW_TOKEN=your_preview_token_here');
422
+ console.log('');
423
+ console.log(' 3. Make sure you have a "home" story in your Storyblok space');
424
+ console.log(' with a content type of "page"');
425
+ console.log('');
426
+ console.log(' 4. Set the Visual Editor URL to https://localhost:3000/');
427
+ console.log('');
428
+ console.log(' 5. Start your dev server:');
429
+ console.log(' npm run dev');
430
+ console.log('');
431
+ }
432
+
223
433
  function generateReadme(projectName, cms) {
224
434
  const title = projectName || 'Project Title';
225
435
 
@@ -431,6 +641,8 @@ SITE_URL=
431
641
 
432
642
  if (selection.key === 'prismic') {
433
643
  await setupPrismic(projectDir);
644
+ } else if (selection.key === 'storyblok') {
645
+ await setupStoryblok(projectDir);
434
646
  } else {
435
647
  console.log(`CMS preset ${selection.label} scaffolding is coming next.`);
436
648
  console.log('This run only confirms selection for non-Prismic options.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majordigital/create-acorn",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Interactive scaffold for Acorn with Storyblok/Prismic/DatoCMS, TypeScript, and Tailwind.",
5
5
  "bin": {
6
6
  "create-acorn": "bin/create-acorn.mjs",