@majordigital/create-acorn 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/bin/create-acorn.mjs +50 -130
  2. package/package.json +2 -1
  3. package/storyblok/storyblok-api/package.json +15 -0
  4. package/storyblok/storyblok-api/src/lib/buildCache.ts +64 -0
  5. package/storyblok/storyblok-api/src/lib/storyblok/bloks.ts +24 -0
  6. package/storyblok/storyblok-api/src/lib/storyblok/fetch/config.ts +47 -0
  7. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchBreadcrumbs.ts +125 -0
  8. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchCv.ts +45 -0
  9. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchGlobal.ts +48 -0
  10. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchIcons.ts +108 -0
  11. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchPaths.ts +164 -0
  12. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchSitemap.ts +103 -0
  13. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchStories.ts +105 -0
  14. package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchStory.ts +95 -0
  15. package/storyblok/storyblok-api/src/lib/storyblok/helpers.ts +111 -0
  16. package/storyblok/storyblok-api/src/lib/storyblok/index.ts +16 -0
  17. package/storyblok/storyblok-api/src/lib/storyblok/mergedRelations.ts +22 -0
  18. package/storyblok/storyblok-api/src/lib/storyblok/redirects.js +50 -0
  19. package/storyblok/storyblok-api/src/lib/storyblok/seoMetadata.ts +124 -0
  20. package/storyblok/storyblok-api/src/lib/storyblok/types.ts +48 -0
  21. package/storyblok/storyblok-api/src/lib/storyblok/utils/previewTokenValidator.ts +14 -0
  22. package/storyblok/storyblok-api/src/ui/FallbackComponent.tsx +24 -0
@@ -224,9 +224,21 @@ async function setupStoryblok(projectName) {
224
224
  console.log('Setting up Storyblok...');
225
225
  console.log('');
226
226
 
227
- // Install Storyblok SDK
228
- console.log('Installing @storyblok/react...');
229
- await runCommand('npm', ['install', '--legacy-peer-deps', '@storyblok/react']);
227
+ const spaceId = await ask('Enter your Storyblok Space ID');
228
+ if (!spaceId || !/^\d+$/.test(spaceId)) {
229
+ console.error('Space ID must be a numeric value (e.g. 338885). You can find it in your Storyblok dashboard.');
230
+ exit(1);
231
+ }
232
+ console.log('');
233
+
234
+ // Install Storyblok dependencies
235
+ console.log('Installing Storyblok dependencies...');
236
+ await runCommand('npm', ['install', '--legacy-peer-deps',
237
+ '@storyblok/react', '@storyblok/js', 'storyblok-js-client'
238
+ ]);
239
+ await runCommand('npm', ['install', '--save-dev', '--legacy-peer-deps',
240
+ 'storyblok', 'storyblok-generate-ts'
241
+ ]);
230
242
  console.log('');
231
243
 
232
244
  // Update next.config.ts — replace Prismic image patterns with Storyblok
@@ -250,30 +262,24 @@ async function setupStoryblok(projectName) {
250
262
  console.log('Warning: Could not update next.config.ts image patterns.');
251
263
  }
252
264
 
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');
265
+ // Copy standardised Storyblok API layer from storyblok/storyblok-api/src/
266
+ const __dirname = dirname(fileURLToPath(import.meta.url));
267
+ const storyblokSrcDir = join(__dirname, '..', 'storyblok', 'storyblok-api', 'src');
268
+
269
+ // Copy src/lib/storyblok/ (init, bloks, fetch, helpers, types, etc.)
270
+ const targetLibStoryblok = join(process.cwd(), 'src', 'lib', 'storyblok');
271
+ cpSync(join(storyblokSrcDir, 'lib', 'storyblok'), targetLibStoryblok, { recursive: true });
272
+ console.log('Copied Storyblok API layer (lib/storyblok/).');
273
+
274
+ // Copy src/lib/buildCache.ts
275
+ cpSync(join(storyblokSrcDir, 'lib', 'buildCache.ts'), join(process.cwd(), 'src', 'lib', 'buildCache.ts'));
276
+ console.log('Copied build cache utility (lib/buildCache.ts).');
277
+
278
+ // Copy src/ui/FallbackComponent.tsx
279
+ const targetUiDir = join(process.cwd(), 'src', 'ui');
280
+ mkdirSync(targetUiDir, { recursive: true });
281
+ cpSync(join(storyblokSrcDir, 'ui', 'FallbackComponent.tsx'), join(targetUiDir, 'FallbackComponent.tsx'));
282
+ console.log('Copied FallbackComponent (ui/FallbackComponent.tsx).');
277
283
 
278
284
  // Create StoryblokProvider
279
285
  const componentsDir = join(process.cwd(), 'src', 'components');
@@ -288,91 +294,16 @@ export default function StoryblokProvider({ children }: PropsWithChildren) {
288
294
  return children;
289
295
  }
290
296
  `);
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).');
297
+ console.log('Created StoryblokProvider component.');
365
298
 
366
299
  // Update layout.tsx to wrap with StoryblokProvider
367
300
  const layoutPath = join(process.cwd(), 'src', 'app', 'layout.tsx');
368
301
  try {
369
302
  let layout = readFileSync(layoutPath, 'utf-8');
370
- // Add import
371
303
  layout = layout.replace(
372
304
  "import '@/styles/globals.css';",
373
305
  "import '@/styles/globals.css';\n\nimport StoryblokProvider from '@/components/StoryblokProvider';"
374
306
  );
375
- // Wrap <html> with StoryblokProvider
376
307
  layout = layout.replace(
377
308
  '<html lang="en"',
378
309
  '<StoryblokProvider>\n\t\t<html lang="en"'
@@ -387,28 +318,17 @@ export default function Teaser({ blok }: { blok: TeaserBlok }) {
387
318
  console.log('Warning: Could not update layout.tsx with StoryblokProvider.');
388
319
  }
389
320
 
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.');
321
+ // Add generate-types script to package.json
322
+ const pkgPath = join(process.cwd(), 'package.json');
323
+ try {
324
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
325
+ if (!pkg.scripts) pkg.scripts = {};
326
+ pkg.scripts['generate-types'] = `storyblok components pull --space ${spaceId} && storyblok types generate --type-suffix Storyblok --space ${spaceId}`;
327
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
328
+ console.log('Added "generate-types" script to package.json.');
329
+ } catch {
330
+ console.log('Warning: Could not update package.json with generate-types script.');
331
+ }
412
332
 
413
333
  console.log('');
414
334
  console.log('=== Storyblok Setup Complete ===');
@@ -417,13 +337,13 @@ export default async function Home() {
417
337
  console.log('');
418
338
  console.log(' 1. Create a Storyblok space at https://app.storyblok.com');
419
339
  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');
340
+ console.log(' 2. Add your preview token to .env.local:');
341
+ console.log(' STORYBLOK_PREVIEW_TOKEN=your_preview_token');
422
342
  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"');
343
+ console.log(' 3. Register your blok components in src/lib/storyblok/bloks.ts');
425
344
  console.log('');
426
- console.log(' 4. Set the Visual Editor URL to https://localhost:3000/');
345
+ console.log(' 4. Generate TypeScript types from your Storyblok space:');
346
+ console.log(' npm run generate-types');
427
347
  console.log('');
428
348
  console.log(' 5. Start your dev server:');
429
349
  console.log(' npm run dev');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majordigital/create-acorn",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "bin",
22
22
  "template",
23
+ "storyblok",
23
24
  "README.md"
24
25
  ],
25
26
  "engines": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "storyblok-api",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "generate-types": "storyblok components pull --space <SPACEID> && storyblok types generate --type-suffix Storyblok --space <SPACEID>"
7
+ },
8
+ "dependencies": {
9
+ "@storyblok/js": "^4.2.8",
10
+ "@storyblok/react": "^5.4.11",
11
+ "storyblok": "^4.6.13",
12
+ "storyblok-generate-ts": "^2.2.0",
13
+ "storyblok-js-client": "^7.1.4",
14
+ }
15
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // File to store the CV (content version)
5
+ const BUILD_CV_FILE = path.join(process.cwd(), '.build-cv');
6
+
7
+ /**
8
+ * Clears the CV cache file
9
+ * Should be called at the start of each build
10
+ */
11
+ export function clearBuildCv(): void {
12
+ try {
13
+ if (fs.existsSync(BUILD_CV_FILE)) {
14
+ fs.unlinkSync(BUILD_CV_FILE);
15
+ }
16
+ } catch (_error) {
17
+ // biome-ignore lint/suspicious/noConsole: needs to warn about build CV clear issues
18
+ console.warn('Error clearing build CV:', _error);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Stores the CV (content version) for the build
24
+ */
25
+ export function storeBuildCv(cv: number): void {
26
+ try {
27
+ fs.writeFileSync(BUILD_CV_FILE, cv.toString(), 'utf8');
28
+ } catch (_error) {
29
+ // biome-ignore lint/suspicious/noConsole: needs to warn about build CV write issues
30
+ console.warn('Error writing build CV:', _error);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Returns the CV (content version) stored during the build
36
+ * Returns undefined if no CV file exists or if in draft mode
37
+ */
38
+ export function getBuildCv(): number | undefined {
39
+ if (fs.existsSync(BUILD_CV_FILE)) {
40
+ try {
41
+ const storedCv = fs.readFileSync(BUILD_CV_FILE, 'utf8').trim();
42
+
43
+ // If file is empty or being written, treat as if file doesn't exist
44
+ if (storedCv === '') {
45
+ return undefined;
46
+ }
47
+
48
+ const cv = parseInt(storedCv, 10);
49
+
50
+ if (Number.isNaN(cv)) {
51
+ // biome-ignore lint/suspicious/noConsole: needs to warn about invalid CV
52
+ console.warn('Stored CV is not a valid number, ignoring cache');
53
+ return undefined;
54
+ }
55
+
56
+ return cv;
57
+ } catch (_error) {
58
+ // If read fails (e.g., file being written), treat as cache miss
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ return undefined;
64
+ }
@@ -0,0 +1,24 @@
1
+ import type { SbReactComponentsMap } from '@storyblok/react/rsc';
2
+
3
+ const atoms = {
4
+ };
5
+
6
+ const elements = {
7
+ };
8
+
9
+ const components = {
10
+ };
11
+
12
+ const sections = {
13
+ };
14
+
15
+ const layouts = {
16
+ };
17
+
18
+ export const bloks: SbReactComponentsMap = {
19
+ ...atoms,
20
+ ...elements,
21
+ ...components,
22
+ ...sections,
23
+ ...layouts,
24
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Storyblok API config and constants for pagination, filtering, and relations.
3
+ */
4
+
5
+ // Pagination limits
6
+ export const MAX_STORIES_PER_PAGE = 100; // Storyblok getStories max
7
+ export const LINKS_PER_PAGE = 1000; // Storyblok getLinks max
8
+
9
+ // Default cache revalidation
10
+ export const DEFAULT_REVALIDATE = 60 * 60 * 8; // 8 hours
11
+
12
+ // Fields to exclude from listings for performance
13
+ export const DEFAULT_EXCLUDING_FIELDS = [
14
+ 'content.body',
15
+ 'content.blocks',
16
+ 'content.long_text',
17
+ 'content.richtext',
18
+ // Add/remove fields based on your schemas
19
+ ].join(',');
20
+
21
+ // Reserved slugs to exclude from sitemaps/routes
22
+ export const reservedSlugs = [
23
+ 'home',
24
+ 'error404',
25
+ '_not-found',
26
+ 'not-found',
27
+ 'favicon.ico',
28
+ 'robots.txt',
29
+ 'sitemap.xml',
30
+ ];
31
+
32
+ // Dynamic stories/routes to ignore in listings/sitemaps
33
+ export const ignoreDynamicStories: string[] = ['legal'];
34
+ export const ignoreDynamicRoutes: string[] = [
35
+ 'global',
36
+ 'global-settings',
37
+ 'preview',
38
+ ];
39
+ export const ignoreListingRoutes: string[][] = [];
40
+
41
+ // Relations to resolve globally for certain blocks/components
42
+ export const globalResolveRelations = [];
43
+
44
+ // Per-page settings for various content types
45
+ export const postPerPage = 20;
46
+ export const caseStudiesPerPage = 20;
47
+ export const resourcesPerPage = 20;
@@ -0,0 +1,125 @@
1
+ import { getStoryblokApi } from '@storyblok/react/rsc';
2
+ import type { ISbStoriesParams } from 'storyblok-js-client';
3
+
4
+ import { isDraftEnabled } from '@/lib/utils';
5
+
6
+ import type { ICustomSbStoryData, StoryblokServiceDefaults } from '../types';
7
+ import { DEFAULT_EXCLUDING_FIELDS, DEFAULT_REVALIDATE } from './config';
8
+
9
+ export interface BreadcrumbItemProps {
10
+ label: string;
11
+ href: string;
12
+ isActive?: boolean;
13
+ }
14
+
15
+ function labelFromSlug(slug: string): string {
16
+ const last = slug.split('/').filter(Boolean).pop() ?? slug;
17
+ return last.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
18
+ }
19
+
20
+ /**
21
+ * Build breadcrumbs for a Storyblok story (cv ancestor fetch).
22
+ */
23
+ export const fetchBreadcrumbs = async ({
24
+ story,
25
+ draftMode,
26
+ }: {
27
+ story: ICustomSbStoryData;
28
+ draftMode?: StoryblokServiceDefaults['draftMode'];
29
+ }): Promise<BreadcrumbItemProps[]> => {
30
+ // Guard: no story or no slug
31
+ if (!story || !story.full_slug) return [];
32
+
33
+ const isDraft = isDraftEnabled(draftMode);
34
+
35
+ // If there is no parent, return the single current crumb
36
+ if (!story.parent_id) {
37
+ return [
38
+ {
39
+ label:
40
+ story.content?.heading ??
41
+ story.name ??
42
+ labelFromSlug(story.full_slug),
43
+ href: `/${story.full_slug}`,
44
+ isActive: true,
45
+ },
46
+ ];
47
+ }
48
+
49
+ const parts = story.full_slug.split('/').filter(Boolean);
50
+
51
+ // Top-level: only itself
52
+ if (parts.length <= 1) {
53
+ return [
54
+ {
55
+ label:
56
+ story.content?.heading ??
57
+ story.name ??
58
+ labelFromSlug(story.full_slug),
59
+ href: `/${story.full_slug}`,
60
+ isActive: true,
61
+ },
62
+ ];
63
+ }
64
+
65
+ // Parents in order: "a", "a/b", ..., "a/b/.../y"
66
+ const parentSlugs = parts
67
+ .slice(0, -1)
68
+ .map((_, i) => parts.slice(0, i + 1).join('/'));
69
+
70
+ try {
71
+ const storyblokApi = getStoryblokApi();
72
+ // Build a lean, typed params object for getStories
73
+ const params: ISbStoriesParams = {
74
+ version: isDraft ? 'draft' : 'published',
75
+ ...(isDraft ? { cv: Date.now() } : {}),
76
+ by_slugs: parentSlugs.join(','),
77
+ per_page: parentSlugs.length,
78
+ excluding_fields: DEFAULT_EXCLUDING_FIELDS,
79
+ };
80
+
81
+ const storiesRes = await storyblokApi.get('cdn/stories', params, {
82
+ next: {
83
+ revalidate: DEFAULT_REVALIDATE,
84
+ },
85
+ });
86
+
87
+ type SbStoryLite = {
88
+ full_slug: string;
89
+ name?: string;
90
+ content?: { heading?: string };
91
+ };
92
+ const items = (storiesRes?.data?.stories ?? []) as SbStoryLite[];
93
+ const bySlug = new Map(items.map(s => [s.full_slug, s]));
94
+
95
+ // Ancestors in the exact same order as parentSlugs, with safe fallbacks
96
+ const ancestors: BreadcrumbItemProps[] = parentSlugs.map(slug => {
97
+ const s = bySlug.get(slug);
98
+ const label = s?.content?.heading ?? s?.name ?? labelFromSlug(slug);
99
+ return { label, href: `/${slug}`, isActive: false };
100
+ });
101
+
102
+ const current: BreadcrumbItemProps = {
103
+ label:
104
+ story.content?.heading ??
105
+ story.name ??
106
+ labelFromSlug(story.full_slug),
107
+ href: `/${story.full_slug}`,
108
+ isActive: true,
109
+ };
110
+
111
+ return [...ancestors, current];
112
+ } catch {
113
+ // Fallback: at least return the current item
114
+ return [
115
+ {
116
+ label:
117
+ story.content?.heading ??
118
+ story.name ??
119
+ labelFromSlug(story.full_slug),
120
+ href: `/${story.full_slug}`,
121
+ isActive: true,
122
+ },
123
+ ];
124
+ }
125
+ };
@@ -0,0 +1,45 @@
1
+ import { Storyblok } from 'storyblok-js-client';
2
+
3
+ import { getBuildCv, storeBuildCv } from '@/lib/buildCache';
4
+
5
+ // Store the in-flight promise to prevent concurrent API calls
6
+ let cvPromise: Promise<number> | null = null;
7
+
8
+ export const fetchCv = async (): Promise<number> => {
9
+ // Try to get cached CV first
10
+ const cachedCv = getBuildCv();
11
+
12
+ if (cachedCv !== undefined) {
13
+ return cachedCv;
14
+ }
15
+
16
+ // If there's already a fetch in progress, wait for it
17
+ if (cvPromise !== null) {
18
+ return cvPromise;
19
+ }
20
+
21
+ // Start a new fetch
22
+ cvPromise = (async () => {
23
+ try {
24
+ // Fetch CV from Storyblok
25
+ const storyblokClient = new Storyblok({
26
+ accessToken: process.env.STORYBLOK_PREVIEW_TOKEN,
27
+ region: 'eu',
28
+ });
29
+
30
+ const cv = await storyblokClient
31
+ .get('cdn/spaces/me', { version: 'published' })
32
+ .then(x => x.data.space.version as number);
33
+
34
+ // Store CV for subsequent calls
35
+ storeBuildCv(cv);
36
+
37
+ return cv;
38
+ } finally {
39
+ // Clear the promise after completion (success or failure)
40
+ cvPromise = null;
41
+ }
42
+ })();
43
+
44
+ return cvPromise;
45
+ };
@@ -0,0 +1,48 @@
1
+ import type { ISbStoryParams } from 'storyblok-js-client';
2
+
3
+ import { getStoryblokApi } from '@/lib/storyblok';
4
+ import { isDraftEnabled } from '@/lib/utils';
5
+
6
+ import type { StoryblokServiceDefaults } from '../types';
7
+ import { fetchCv } from './fetchCv';
8
+
9
+ import type { GlobalSettingsStoryblok } from '.storyblok/types/338885/storyblok-components';
10
+
11
+ /**
12
+ * Fetch Global Settings (single story) with caching + webhook-friendly tags.
13
+ * - Draft adds `cv` to bypass CDN.
14
+ * - Tags allow precise revalidation via `/api/storyblok/revalidate`.
15
+ */
16
+ export const fetchGlobal = async ({
17
+ draftMode,
18
+ revalidate = 900, // 15m; webhook will keep this fresh
19
+ }: {
20
+ draftMode?: StoryblokServiceDefaults['draftMode'];
21
+ revalidate?: number;
22
+ }): Promise<GlobalSettingsStoryblok | null> => {
23
+ const slug = 'global-settings';
24
+
25
+ const isDraft = isDraftEnabled(draftMode);
26
+ const storyblokApi = getStoryblokApi();
27
+ const cv = isDraft ? undefined : await fetchCv();
28
+
29
+ // 2) Narrow, typed Story params (no list fields)
30
+ const params: ISbStoryParams = {
31
+ version: isDraft ? 'draft' : 'published',
32
+ cv,
33
+ resolve_relations: 'global_settings.footer_legal',
34
+ resolve_links: 'url',
35
+ };
36
+
37
+ // 3) Direct API call with revalidate
38
+ const { data } = await storyblokApi.get(`cdn/stories/${slug}`, params, {
39
+ next: {
40
+ revalidate,
41
+ },
42
+ });
43
+ const story = data?.story;
44
+ if (!story) return null;
45
+
46
+ // 4) Return content only
47
+ return story.content as GlobalSettingsStoryblok;
48
+ };