@majordigital/create-acorn 1.3.0 → 1.3.2
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/bin/create-acorn.mjs +73 -129
- package/package.json +2 -1
- package/storyblok/storyblok-api/package.json +15 -0
- package/storyblok/storyblok-api/src/lib/buildCache.ts +64 -0
- package/storyblok/storyblok-api/src/lib/storyblok/bloks.ts +24 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/config.ts +47 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchBreadcrumbs.ts +125 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchCv.ts +45 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchGlobal.ts +48 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchIcons.ts +108 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchPaths.ts +164 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchSitemap.ts +103 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchStories.ts +105 -0
- package/storyblok/storyblok-api/src/lib/storyblok/fetch/fetchStory.ts +95 -0
- package/storyblok/storyblok-api/src/lib/storyblok/helpers.ts +111 -0
- package/storyblok/storyblok-api/src/lib/storyblok/index.ts +16 -0
- package/storyblok/storyblok-api/src/lib/storyblok/mergedRelations.ts +22 -0
- package/storyblok/storyblok-api/src/lib/storyblok/redirects.js +50 -0
- package/storyblok/storyblok-api/src/lib/storyblok/seoMetadata.ts +124 -0
- package/storyblok/storyblok-api/src/lib/storyblok/types.ts +48 -0
- package/storyblok/storyblok-api/src/lib/storyblok/utils/previewTokenValidator.ts +14 -0
- package/storyblok/storyblok-api/src/ui/FallbackComponent.tsx +24 -0
package/bin/create-acorn.mjs
CHANGED
|
@@ -224,9 +224,21 @@ async function setupStoryblok(projectName) {
|
|
|
224
224
|
console.log('Setting up Storyblok...');
|
|
225
225
|
console.log('');
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
//
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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,41 @@ export default function Teaser({ blok }: { blok: TeaserBlok }) {
|
|
|
387
318
|
console.log('Warning: Could not update layout.tsx with StoryblokProvider.');
|
|
388
319
|
}
|
|
389
320
|
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
321
|
+
// Update package.json scripts for Storyblok
|
|
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.dev = 'next dev --turbo --experimental-https';
|
|
327
|
+
pkg.scripts['build:snapshot'] = 'node src/scripts/generate-snapshot-entry.js';
|
|
328
|
+
pkg.scripts.build = 'npm run build:snapshot && next build';
|
|
329
|
+
pkg.scripts['build-stats'] = 'cross-env ANALYZE=true npm run build';
|
|
330
|
+
pkg.scripts.start = 'next start';
|
|
331
|
+
pkg.scripts.preview = 'next build && next start';
|
|
332
|
+
pkg.scripts.prepare = 'lefthook install || true';
|
|
333
|
+
pkg.scripts.check = 'biome lint . --skip suspicious/noConsole && biome format .';
|
|
334
|
+
pkg.scripts.fix = 'biome lint . --write --skip suspicious/noConsole && biome format . --write';
|
|
335
|
+
pkg.scripts['check:prod'] = 'biome check .';
|
|
336
|
+
pkg.scripts['fix:prod'] = 'biome check . --write';
|
|
337
|
+
pkg.scripts['generate-types'] = `storyblok components pull --space ${spaceId} && storyblok types generate --type-suffix Storyblok --space ${spaceId}`;
|
|
338
|
+
pkg.scripts.storybook = 'storybook dev -p 6006';
|
|
339
|
+
pkg.scripts['build-storybook'] = 'storybook build';
|
|
340
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
341
|
+
console.log('Updated package.json scripts for Storyblok.');
|
|
342
|
+
} catch {
|
|
343
|
+
console.log('Warning: Could not update package.json scripts.');
|
|
344
|
+
}
|
|
403
345
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
346
|
+
// Update .env.example with the space ID
|
|
347
|
+
const envExamplePath = join(process.cwd(), '.env.example');
|
|
348
|
+
try {
|
|
349
|
+
let envContent = readFileSync(envExamplePath, 'utf-8');
|
|
350
|
+
envContent = envContent.replace('STORYBLOK_SPACE_ID=', `STORYBLOK_SPACE_ID=${spaceId}`);
|
|
351
|
+
writeFileSync(envExamplePath, envContent);
|
|
352
|
+
console.log('Updated .env.example with Space ID.');
|
|
353
|
+
} catch {
|
|
354
|
+
console.log('Warning: Could not update .env.example with Space ID.');
|
|
355
|
+
}
|
|
412
356
|
|
|
413
357
|
console.log('');
|
|
414
358
|
console.log('=== Storyblok Setup Complete ===');
|
|
@@ -417,13 +361,13 @@ export default async function Home() {
|
|
|
417
361
|
console.log('');
|
|
418
362
|
console.log(' 1. Create a Storyblok space at https://app.storyblok.com');
|
|
419
363
|
console.log('');
|
|
420
|
-
console.log(' 2.
|
|
421
|
-
console.log(' STORYBLOK_PREVIEW_TOKEN=
|
|
364
|
+
console.log(' 2. Add your preview token to .env.local:');
|
|
365
|
+
console.log(' STORYBLOK_PREVIEW_TOKEN=your_preview_token');
|
|
422
366
|
console.log('');
|
|
423
|
-
console.log(' 3.
|
|
424
|
-
console.log(' with a content type of "page"');
|
|
367
|
+
console.log(' 3. Register your blok components in src/lib/storyblok/bloks.ts');
|
|
425
368
|
console.log('');
|
|
426
|
-
console.log(' 4.
|
|
369
|
+
console.log(' 4. Generate TypeScript types from your Storyblok space:');
|
|
370
|
+
console.log(' npm run generate-types');
|
|
427
371
|
console.log('');
|
|
428
372
|
console.log(' 5. Start your dev server:');
|
|
429
373
|
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.
|
|
3
|
+
"version": "1.3.2",
|
|
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
|
+
};
|