@cobrastyle/adapter-storyblok 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cobrastyle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ import type { PartialStorefrontAdapter } from '@cobrastyle/shared-types';
2
+ export declare const storyblokAdapter: PartialStorefrontAdapter;
@@ -0,0 +1,25 @@
1
+ import { getConfig } from './config';
2
+ import { fetchStory, mapContext } from './client';
3
+ import { mapStory } from './mappers/story';
4
+ async function getByUrlKey(urlKey) {
5
+ const version = await getConfig().resolveVersion();
6
+ const story = await fetchStory(urlKey, version);
7
+ return story ? mapStory(story, mapContext(version)) : null;
8
+ }
9
+ export const storyblokAdapter = {
10
+ cms: {
11
+ // Storyblok identifies pages by slug, so getPage and getPageByUrlKey are the same lookup.
12
+ getPage: (identifier) => getByUrlKey(identifier),
13
+ getPageByUrlKey: (urlKey) => getByUrlKey(urlKey),
14
+ // Implemented for CmsAdapter contract completeness. Storefront routing keeps
15
+ // resolveUrl on the commerce adapter and falls through via getPageByUrlKey
16
+ // (see storefront.config.ts), so this is not called by the catch-all route.
17
+ resolveUrl: async (urlKey) => {
18
+ const version = await getConfig().resolveVersion();
19
+ const story = await fetchStory(urlKey, version);
20
+ return story
21
+ ? { type: 'CMS_PAGE', id: String(story.id), urlKey }
22
+ : { type: 'NOT_FOUND', id: '', urlKey };
23
+ },
24
+ },
25
+ };
@@ -0,0 +1,11 @@
1
+ export interface BlockDefinition {
2
+ /** Storyblok component key. */
3
+ name: string;
4
+ /** Schema version. */
5
+ version: string;
6
+ /** Required field keys (mappers downgrade to `unknown` if missing). */
7
+ requiredFields: string[];
8
+ /** Optional field keys. */
9
+ optionalFields: string[];
10
+ }
11
+ export declare const blockCatalog: BlockDefinition[];
@@ -0,0 +1,9 @@
1
+ export const blockCatalog = [
2
+ { name: 'hero', version: 'v1', requiredFields: ['headline'], optionalFields: ['subline', 'image', 'cta'] },
3
+ { name: 'rich_text', version: 'v1', requiredFields: ['body'], optionalFields: [] },
4
+ { name: 'image', version: 'v1', requiredFields: ['asset'], optionalFields: [] },
5
+ { name: 'cta_button', version: 'v1', requiredFields: ['label', 'link'], optionalFields: ['variant'] },
6
+ { name: 'section', version: 'v1', requiredFields: [], optionalFields: ['blocks'] },
7
+ { name: 'textBlock', version: 'v1', requiredFields: ['text'], optionalFields: ['buttons', 'textAlign'] },
8
+ { name: 'textColumnBlock', version: 'v1', requiredFields: [], optionalFields: ['title', 'text', 'text2', 'buttons', 'textAlign'] },
9
+ ];
@@ -0,0 +1,7 @@
1
+ import { type StoryblokVersion } from './config';
2
+ import type { StoryblokStory } from './types';
3
+ import type { MapContext } from './mappers/blocks';
4
+ /** Render context backed by the SDK's richtext resolver. */
5
+ export declare function mapContext(version: StoryblokVersion): MapContext;
6
+ /** Fetch a single story by slug. Returns null on 404. */
7
+ export declare function fetchStory(slug: string, version: StoryblokVersion): Promise<StoryblokStory | null>;
package/dist/client.js ADDED
@@ -0,0 +1,40 @@
1
+ import StoryblokClient from 'storyblok-js-client';
2
+ import { getConfig } from './config';
3
+ let clientForToken = null;
4
+ function getClient(token) {
5
+ if (clientForToken?.token === token)
6
+ return clientForToken.client;
7
+ const cfg = getConfig();
8
+ const client = new StoryblokClient({ accessToken: token, region: cfg.region });
9
+ clientForToken = { token, client };
10
+ return client;
11
+ }
12
+ function tokenFor(version) {
13
+ const cfg = getConfig();
14
+ return version === 'draft' ? cfg.previewToken : cfg.publicToken;
15
+ }
16
+ /** Render context backed by the SDK's richtext resolver. */
17
+ export function mapContext(version) {
18
+ const client = getClient(tokenFor(version));
19
+ return {
20
+ renderRichText: (doc) => doc ? client.richTextResolver.render(doc) : '',
21
+ };
22
+ }
23
+ /** Fetch a single story by slug. Returns null on 404. */
24
+ export async function fetchStory(slug, version) {
25
+ const client = getClient(tokenFor(version));
26
+ try {
27
+ const res = await client.get(`cdn/stories/${slug}`, { version });
28
+ return res.data.story;
29
+ }
30
+ catch (err) {
31
+ if (isNotFound(err))
32
+ return null;
33
+ throw err;
34
+ }
35
+ }
36
+ function isNotFound(err) {
37
+ const status = err?.status ??
38
+ err?.response?.status;
39
+ return status === 404;
40
+ }
@@ -0,0 +1,24 @@
1
+ export type StoryblokVersion = 'draft' | 'published';
2
+ export interface StoryblokConfig {
3
+ /** Public token — published content. */
4
+ publicToken: string;
5
+ /** Preview token — draft content. Server-only. */
6
+ previewToken: string;
7
+ /** Shared secret used to validate the publish webhook. Optional for read-only usage. */
8
+ webhookSecret?: string;
9
+ /** Storyblok space region. Defaults to 'eu'. */
10
+ region?: 'eu' | 'us' | 'ap' | 'ca' | 'cn';
11
+ /** Fallback version when no versionGetter is supplied. Defaults to 'published'. */
12
+ defaultVersion?: StoryblokVersion;
13
+ /**
14
+ * Per-request version resolver supplied by the app (reads Next.js draftMode).
15
+ * Kept here so the framework-agnostic package never imports next/headers.
16
+ */
17
+ versionGetter?: () => Promise<StoryblokVersion>;
18
+ }
19
+ interface ResolvedConfig extends StoryblokConfig {
20
+ resolveVersion: () => Promise<StoryblokVersion>;
21
+ }
22
+ export declare function setConfig(newConfig: StoryblokConfig): void;
23
+ export declare function getConfig(): ResolvedConfig;
24
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,19 @@
1
+ let config = null;
2
+ export function setConfig(newConfig) {
3
+ if (!newConfig) {
4
+ config = null;
5
+ return;
6
+ }
7
+ config = {
8
+ ...newConfig,
9
+ resolveVersion: async () => newConfig.versionGetter
10
+ ? newConfig.versionGetter()
11
+ : (newConfig.defaultVersion ?? 'published'),
12
+ };
13
+ }
14
+ export function getConfig() {
15
+ if (!config) {
16
+ throw new Error('Storyblok adapter not configured. Call setConfig() with publicToken and previewToken.');
17
+ }
18
+ return config;
19
+ }
@@ -0,0 +1,5 @@
1
+ export { storyblokAdapter } from './adapter';
2
+ export { setConfig, getConfig } from './config';
3
+ export type { StoryblokConfig, StoryblokVersion } from './config';
4
+ export { blockCatalog, type BlockDefinition } from './blocks/catalog';
5
+ export { inertStoryblokAdapter } from './inert';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { storyblokAdapter } from './adapter';
2
+ export { setConfig, getConfig } from './config';
3
+ export { blockCatalog } from './blocks/catalog';
4
+ export { inertStoryblokAdapter } from './inert';
@@ -0,0 +1,7 @@
1
+ import type { PartialStorefrontAdapter } from '@cobrastyle/shared-types';
2
+ /**
3
+ * Inert CMS adapter used when Storyblok is not configured. Never throws — every
4
+ * lookup resolves to "nothing here", so the storefront keeps working and Storyblok
5
+ * CMS simply stays inactive until tokens are provided.
6
+ */
7
+ export declare const inertStoryblokAdapter: PartialStorefrontAdapter;
package/dist/inert.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Inert CMS adapter used when Storyblok is not configured. Never throws — every
3
+ * lookup resolves to "nothing here", so the storefront keeps working and Storyblok
4
+ * CMS simply stays inactive until tokens are provided.
5
+ */
6
+ export const inertStoryblokAdapter = {
7
+ cms: {
8
+ getPage: async () => null,
9
+ getPageByUrlKey: async () => null,
10
+ resolveUrl: async (urlKey) => ({
11
+ type: 'NOT_FOUND',
12
+ id: '',
13
+ urlKey,
14
+ }),
15
+ },
16
+ };
@@ -0,0 +1,8 @@
1
+ import type { CmsBlockNode } from '@cobrastyle/shared-types';
2
+ import type { StoryblokBlok } from '../types';
3
+ export interface MapContext {
4
+ /** Renders a Storyblok richtext document to HTML. NOTE: output is trusted-editor HTML and is NOT sanitized — it is rendered via dangerouslySetInnerHTML downstream. */
5
+ renderRichText: (doc: unknown) => string;
6
+ }
7
+ export declare function mapBlok(blok: StoryblokBlok, ctx: MapContext): CmsBlockNode;
8
+ export declare function mapBloks(bloks: StoryblokBlok[] | undefined, ctx: MapContext): CmsBlockNode[];
@@ -0,0 +1,151 @@
1
+ function str(v) {
2
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
3
+ }
4
+ function asset(v) {
5
+ const a = v;
6
+ if (!a || !a.filename)
7
+ return undefined;
8
+ return { src: a.filename, alt: a.alt ?? '' };
9
+ }
10
+ function href(v) {
11
+ const link = v;
12
+ if (!link)
13
+ return undefined;
14
+ if (link.url)
15
+ return link.url;
16
+ if (link.cached_url) {
17
+ // story links arrive as a slug; make them root-relative.
18
+ return link.cached_url.startsWith('http') ? link.cached_url : `/${link.cached_url}`;
19
+ }
20
+ return undefined;
21
+ }
22
+ function firstBlok(v) {
23
+ return Array.isArray(v) ? v[0] : undefined;
24
+ }
25
+ function unknown(blok) {
26
+ const b = (blok && typeof blok === 'object' ? blok : {});
27
+ return {
28
+ _type: 'unknown',
29
+ id: typeof b._uid === 'string' ? b._uid : '',
30
+ component: typeof b.component === 'string' ? b.component : 'unknown',
31
+ raw: blok,
32
+ };
33
+ }
34
+ function mapCta(blok) {
35
+ const label = str(blok.label);
36
+ const linkHref = href(blok.link);
37
+ if (!label || !linkHref)
38
+ return unknown(blok);
39
+ const variant = blok.variant === 'secondary' ? 'secondary' : 'primary';
40
+ return { _type: 'cta_button', id: blok._uid, label, href: linkHref, variant };
41
+ }
42
+ function mapHero(blok, ctx) {
43
+ const headline = str(blok.headline);
44
+ if (!headline)
45
+ return unknown(blok);
46
+ const node = { _type: 'hero', id: blok._uid, headline };
47
+ const subline = str(blok.subline);
48
+ if (subline)
49
+ node.subline = subline;
50
+ const image = asset(blok.image);
51
+ if (image)
52
+ node.image = image;
53
+ const ctaBlok = firstBlok(blok.cta);
54
+ if (ctaBlok) {
55
+ const cta = mapCta(ctaBlok);
56
+ if (cta._type === 'cta_button')
57
+ node.cta = cta;
58
+ }
59
+ return node;
60
+ }
61
+ function mapRichText(blok, ctx) {
62
+ return { _type: 'rich_text', id: blok._uid, html: ctx.renderRichText(blok.body) };
63
+ }
64
+ function mapImage(blok) {
65
+ const img = asset(blok.asset);
66
+ if (!img)
67
+ return unknown(blok);
68
+ return { _type: 'image', id: blok._uid, src: img.src, alt: img.alt };
69
+ }
70
+ function mapSection(blok, ctx) {
71
+ const raw = Array.isArray(blok.blocks) ? blok.blocks : [];
72
+ return { _type: 'section', id: blok._uid, children: raw.map((b) => mapBlok(b, ctx)) };
73
+ }
74
+ function mapAlign(v) {
75
+ return v === 'left' || v === 'center' || v === 'right' ? v : undefined;
76
+ }
77
+ function mapButtons(v, _ctx) {
78
+ if (!Array.isArray(v))
79
+ return [];
80
+ const out = [];
81
+ for (const entry of v) {
82
+ if (!entry || typeof entry !== 'object')
83
+ continue;
84
+ const b = entry;
85
+ const label = str(b.label) ?? str(b.text) ?? str(b.title);
86
+ const linkHref = href(b.link) ?? href(b.url);
87
+ if (label && linkHref) {
88
+ out.push({
89
+ _type: 'cta_button',
90
+ id: typeof b._uid === 'string' ? b._uid : '',
91
+ label,
92
+ href: linkHref,
93
+ variant: b.variant === 'secondary' ? 'secondary' : 'primary',
94
+ });
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+ function mapText(blok, ctx) {
100
+ const node = {
101
+ _type: 'text',
102
+ id: blok._uid,
103
+ html: ctx.renderRichText(blok.text),
104
+ buttons: mapButtons(blok.buttons, ctx),
105
+ };
106
+ const align = mapAlign(blok.textAlign);
107
+ if (align)
108
+ node.align = align;
109
+ return node;
110
+ }
111
+ function mapTextColumn(blok, ctx) {
112
+ const node = {
113
+ _type: 'text_column',
114
+ id: blok._uid,
115
+ htmlLeft: ctx.renderRichText(blok.text),
116
+ htmlRight: ctx.renderRichText(blok.text2),
117
+ buttons: mapButtons(blok.buttons, ctx),
118
+ };
119
+ const titleHtml = ctx.renderRichText(blok.title);
120
+ if (titleHtml)
121
+ node.titleHtml = titleHtml;
122
+ const align = mapAlign(blok.textAlign);
123
+ if (align)
124
+ node.align = align;
125
+ return node;
126
+ }
127
+ export function mapBlok(blok, ctx) {
128
+ if (!blok || typeof blok !== 'object')
129
+ return unknown(blok);
130
+ switch (blok.component) {
131
+ case 'hero':
132
+ return mapHero(blok, ctx);
133
+ case 'rich_text':
134
+ return mapRichText(blok, ctx);
135
+ case 'image':
136
+ return mapImage(blok);
137
+ case 'cta_button':
138
+ return mapCta(blok);
139
+ case 'section':
140
+ return mapSection(blok, ctx);
141
+ case 'textBlock':
142
+ return mapText(blok, ctx);
143
+ case 'textColumnBlock':
144
+ return mapTextColumn(blok, ctx);
145
+ default:
146
+ return unknown(blok);
147
+ }
148
+ }
149
+ export function mapBloks(bloks, ctx) {
150
+ return (bloks ?? []).map((b) => mapBlok(b, ctx));
151
+ }
@@ -0,0 +1,4 @@
1
+ import type { CmsPage } from '@cobrastyle/shared-types';
2
+ import type { StoryblokStory } from '../types';
3
+ import { type MapContext } from './blocks';
4
+ export declare function mapStory(story: StoryblokStory, ctx: MapContext): CmsPage;
@@ -0,0 +1,14 @@
1
+ import { mapBloks } from './blocks';
2
+ export function mapStory(story, ctx) {
3
+ const content = story.content;
4
+ return {
5
+ id: String(story.id),
6
+ identifier: story.full_slug,
7
+ title: story.name,
8
+ content: '', // block-based; HTML field intentionally empty
9
+ metaTitle: content.meta_title || undefined,
10
+ metaDescription: content.meta_description || undefined,
11
+ urlKey: story.full_slug,
12
+ blocks: mapBloks(content.body, ctx),
13
+ };
14
+ }
@@ -0,0 +1,37 @@
1
+ /** A Storyblok content block ("blok"). Fields vary by component. */
2
+ export interface StoryblokBlok {
3
+ _uid: string;
4
+ component: string;
5
+ [field: string]: unknown;
6
+ }
7
+ /** Storyblok asset field. */
8
+ export interface StoryblokAsset {
9
+ filename: string | null;
10
+ alt?: string | null;
11
+ title?: string | null;
12
+ }
13
+ /** Storyblok multilink field. */
14
+ export interface StoryblokLink {
15
+ url?: string;
16
+ cached_url?: string;
17
+ linktype?: 'url' | 'story' | 'asset' | 'email';
18
+ }
19
+ /** The body of a Storyblok story for a CMS page. */
20
+ export interface StoryblokPageContent {
21
+ _uid: string;
22
+ component: string;
23
+ body?: StoryblokBlok[];
24
+ meta_title?: string;
25
+ meta_description?: string;
26
+ }
27
+ export interface StoryblokStory {
28
+ id: number;
29
+ uuid: string;
30
+ name: string;
31
+ slug: string;
32
+ full_slug: string;
33
+ content: StoryblokPageContent;
34
+ }
35
+ export interface StoryblokStoryResponse {
36
+ story: StoryblokStory;
37
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@cobrastyle/adapter-storyblok",
3
+ "version": "1.0.1",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "storyblok-js-client": "^6.10.4",
17
+ "@cobrastyle/shared-types": "1.0.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20.0.0",
21
+ "typescript": "^5.3.0"
22
+ },
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/eComero/cobrastyle-express-next.git",
27
+ "directory": "packages/adapters/storyblok"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc --project tsconfig.build.json",
34
+ "type-check": "tsc --noEmit"
35
+ }
36
+ }