storyloco 1.16.0 → 2.0.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 +81 -13
- package/package.json +26 -15
- package/packages/hubspot_form/types.ts +4 -0
- package/packages/shared/dist/components/ui/textarea/textarea.svelte +1 -2
- package/packages/vite/dist/hooks.d.ts +8 -0
- package/packages/vite/dist/hooks.js +11 -0
- package/packages/vite/dist/server.d.ts +51 -0
- package/packages/vite/dist/server.js +76 -0
- package/packages/vite/dist/storyblok.svelte.d.ts +125 -9
- package/packages/vite/dist/storyblok.svelte.js +133 -23
- package/packages/shared/dist/components/ui/dropdown-menu/index.d.ts +0 -25
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
|
|
|
24
24
|
### Storyblok Client
|
|
25
25
|
|
|
26
26
|
- **🎯 StoryblokClient** - Reactive wrapper for Storyblok with Svelte 5 integration
|
|
27
|
-
- **🔧 Utilities** -
|
|
27
|
+
- **🔧 Utilities** - `attrs` for wiring a block to the visual editor, plus re-exported `editable`, `Block`, and `richtext` functions
|
|
28
28
|
|
|
29
29
|
### Shared Components
|
|
30
30
|
|
|
@@ -68,18 +68,20 @@ import type { Video, Heading, Link, Input, Asset, SEO } from 'storyloco'
|
|
|
68
68
|
import { StoryblokClient } from 'storyloco'
|
|
69
69
|
import { PUBLIC_STORYBLOK_ACCESS_TOKEN } from '$env/static/public'
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
export const client = new StoryblokClient({
|
|
72
|
+
token: PUBLIC_STORYBLOK_ACCESS_TOKEN,
|
|
73
|
+
relations: ['shared.items'],
|
|
74
|
+
components: import.meta.glob(['$blocks/**/*.svelte', '$templates/*.svelte'])
|
|
75
|
+
})
|
|
76
|
+
```
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
78
|
+
Everything is configured here rather than by subclassing, and `init()` runs on its own — the
|
|
79
|
+
API methods await it, so you never call it on the server. `components` takes the result of
|
|
80
|
+
`import.meta.glob`: Vite has to see the literal pattern to rewrite it, so the glob has to stay
|
|
81
|
+
at your call site rather than move into the client.
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
+
`client.handle_error` is on the instance, so `.catch(client.handle_error)` works without
|
|
84
|
+
wrapping anything.
|
|
83
85
|
|
|
84
86
|
#### 2. Use in your Svelte components
|
|
85
87
|
|
|
@@ -108,7 +110,7 @@ export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN, import.
|
|
|
108
110
|
|
|
109
111
|
```svelte
|
|
110
112
|
<script lang="ts">
|
|
111
|
-
import {
|
|
113
|
+
import { attrs } from 'storyloco'
|
|
112
114
|
|
|
113
115
|
// This is auto-generated by the Storyblok Schema plugin
|
|
114
116
|
import type { Blok, Hero } from '$lib/components.schema.js'
|
|
@@ -120,12 +122,78 @@ export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN, import.
|
|
|
120
122
|
let { blok } = $props()
|
|
121
123
|
</script>
|
|
122
124
|
|
|
123
|
-
<section
|
|
125
|
+
<section {...attrs(blok)}>
|
|
124
126
|
<h1>{blok.headline}</h1>
|
|
125
127
|
<p>{blok.subheadline}</p>
|
|
126
128
|
</section>
|
|
127
129
|
```
|
|
128
130
|
|
|
131
|
+
`attrs` makes the block editable in the visual editor and gives it a stable `id` — the
|
|
132
|
+
block's `anchor` field when it has one, so sections can be linked to, otherwise its `_uid`.
|
|
133
|
+
|
|
134
|
+
Prefer it to `use:editable`. Being an attachment rather than an action it spreads, so the
|
|
135
|
+
element keeps its own attributes, and you can wrap it to add project-specific ones in one
|
|
136
|
+
place rather than repeating them in every block:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Mapping section fields onto CSS custom properties is left to you — the field names and
|
|
140
|
+
// the design tokens are yours. Wrap `attrs` rather than reaching past it:
|
|
141
|
+
//
|
|
142
|
+
// export function section<T extends SectionFields>(blok: Blok<T>) {
|
|
143
|
+
// let style = ''
|
|
144
|
+
// if (blok.theme) style += `--theme: var(--c-${blok.theme});`
|
|
145
|
+
// if (blok.pad_top) style += `--pt: var(--s-${blok.pad_top});`
|
|
146
|
+
// return { ...attrs(blok), style }
|
|
147
|
+
// }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`editable` is still exported for anything `attrs` doesn't cover.
|
|
151
|
+
|
|
152
|
+
### Read Content on the Server
|
|
153
|
+
|
|
154
|
+
`storyloco/server` binds a client to the current request, so reads carry the version from
|
|
155
|
+
the `version` hook and the resolve options you'd otherwise repeat everywhere. Server-only,
|
|
156
|
+
since it reads the active request.
|
|
157
|
+
|
|
158
|
+
**`$lib/storyblok.server.ts`:**
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { setup } from 'storyloco/server'
|
|
162
|
+
import { client } from './storyblok.js'
|
|
163
|
+
|
|
164
|
+
export const storyblok = setup(client)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then anywhere on the server:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { storyblok } from '$lib/storyblok.server.js'
|
|
171
|
+
|
|
172
|
+
// A single story, with the client's `relations` resolved
|
|
173
|
+
await storyblok.story('layout')
|
|
174
|
+
|
|
175
|
+
// A list. `total` is the unpaged count, for "showing n of m"
|
|
176
|
+
const { stories, total } = await storyblok.stories({ content_type: 'blog', per_page: 2 })
|
|
177
|
+
|
|
178
|
+
// The link tree, flattened — Storyblok returns it keyed by uuid
|
|
179
|
+
const links = await storyblok.links({ starts_with: 'blog/' })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Your params are spread last, so any default can be overridden:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
await storyblok.story('layout', { resolve_assets: 0 })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
It's callable too, for anything the methods don't cover:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const { client, version } = storyblok()
|
|
192
|
+
await client.get('cdn/datasource_entries', { version })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Errors go through `handle_error`, so a missing story is a 404 and anything else a 500.
|
|
196
|
+
|
|
129
197
|
### Use Shared Components
|
|
130
198
|
|
|
131
199
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyloco",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storyblok",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"packages/ticketsource/types.ts",
|
|
33
33
|
"packages/input/types.ts",
|
|
34
34
|
"packages/time/types.ts",
|
|
35
|
-
"packages/seo/types.ts"
|
|
35
|
+
"packages/seo/types.ts",
|
|
36
|
+
"packages/hubspot_form/types.ts"
|
|
36
37
|
],
|
|
37
38
|
"exports": {
|
|
38
39
|
".": {
|
|
@@ -78,28 +79,38 @@
|
|
|
78
79
|
"types": "./packages/vite/dist/hooks.d.ts",
|
|
79
80
|
"import": "./packages/vite/dist/hooks.js"
|
|
80
81
|
},
|
|
82
|
+
"./server": {
|
|
83
|
+
"types": "./packages/vite/dist/server.d.ts",
|
|
84
|
+
"import": "./packages/vite/dist/server.js"
|
|
85
|
+
},
|
|
81
86
|
"./time": {
|
|
82
87
|
"types": "./packages/time/types.ts",
|
|
83
88
|
"svelte": "./packages/time/src/app.svelte.ts"
|
|
84
89
|
},
|
|
85
90
|
"./seo": {
|
|
86
91
|
"types": "./packages/seo/types.ts"
|
|
92
|
+
},
|
|
93
|
+
"./hubspot_form": {
|
|
94
|
+
"types": "./packages/hubspot_form/types.ts",
|
|
95
|
+
"svelte": "./packages/hubspot_form/src/app.svelte.ts"
|
|
87
96
|
}
|
|
88
97
|
},
|
|
89
98
|
"devDependencies": {
|
|
90
|
-
"@types/node": "
|
|
91
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
92
|
-
"@typescript-eslint/parser": "8.
|
|
93
|
-
"eslint": "
|
|
94
|
-
"kitto": "1.
|
|
95
|
-
"oxfmt": "0.
|
|
96
|
-
"typescript": "
|
|
97
|
-
"vite": "
|
|
99
|
+
"@types/node": "26.1.1",
|
|
100
|
+
"@typescript-eslint/eslint-plugin": "8.65.0",
|
|
101
|
+
"@typescript-eslint/parser": "8.65.0",
|
|
102
|
+
"eslint": "10.7.0",
|
|
103
|
+
"kitto": "1.6.0",
|
|
104
|
+
"oxfmt": "0.59.0",
|
|
105
|
+
"typescript": "6.0.3",
|
|
106
|
+
"vite": "8.1.5"
|
|
98
107
|
},
|
|
99
108
|
"peerDependencies": {
|
|
100
|
-
"@storyblok/svelte": "
|
|
101
|
-
"@storyblok/field-plugin": "^1.
|
|
102
|
-
"svelte": "^5.0.0"
|
|
109
|
+
"@storyblok/svelte": "7.2.2",
|
|
110
|
+
"@storyblok/field-plugin": "^1.7.1",
|
|
111
|
+
"svelte": "^5.0.0",
|
|
112
|
+
"@sveltejs/kit": "^2.0.0",
|
|
113
|
+
"storyblok-js-client": "^7.0.0"
|
|
103
114
|
},
|
|
104
115
|
"scripts": {
|
|
105
116
|
"add-plugin": "npx @storyblok/field-plugin-cli@latest add --dir \"./packages\" --structure monorepo",
|
|
@@ -112,7 +123,7 @@
|
|
|
112
123
|
"packages/*"
|
|
113
124
|
],
|
|
114
125
|
"dependencies": {
|
|
115
|
-
"ora": "9.
|
|
116
|
-
"path-to-regexp": "8.
|
|
126
|
+
"ora": "9.4.1",
|
|
127
|
+
"path-to-regexp": "8.4.2"
|
|
117
128
|
}
|
|
118
129
|
}
|
|
@@ -1 +1,9 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit";
|
|
1
2
|
export { handle_redirects as redirects } from "./redirects/handle.js";
|
|
3
|
+
/**
|
|
4
|
+
* Put the Storyblok content version on `event.locals.version` — draft while developing and
|
|
5
|
+
* inside the visual editor (`?_storyblok`), published everywhere else.
|
|
6
|
+
*
|
|
7
|
+
* Declare it on `App.Locals` as `version: import("storyloco").Version`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const version: Handle;
|
|
@@ -1 +1,12 @@
|
|
|
1
|
+
import { dev } from "$app/environment";
|
|
1
2
|
export { handle_redirects as redirects } from "./redirects/handle.js";
|
|
3
|
+
/**
|
|
4
|
+
* Put the Storyblok content version on `event.locals.version` — draft while developing and
|
|
5
|
+
* inside the visual editor (`?_storyblok`), published everywhere else.
|
|
6
|
+
*
|
|
7
|
+
* Declare it on `App.Locals` as `version: import("storyloco").Version`.
|
|
8
|
+
*/
|
|
9
|
+
export const version = async ({ event, resolve }) => {
|
|
10
|
+
event.locals.version = dev || event.url.searchParams.has("_storyblok") ? "draft" : "published";
|
|
11
|
+
return await resolve(event);
|
|
12
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ISbStoriesParams, ISbStoryData, ISbStoryParams } from "@storyblok/svelte";
|
|
2
|
+
import type { ISbLink, ISbLinksParams } from "storyblok-js-client";
|
|
3
|
+
import { type StoryblokClient, type Version } from "./storyblok.svelte.js";
|
|
4
|
+
/** The content version for the current request, as set by the `version` hook. */
|
|
5
|
+
export declare function version(): Version;
|
|
6
|
+
/**
|
|
7
|
+
* Bind a client to the current request.
|
|
8
|
+
*
|
|
9
|
+
* Returns a callable: call it for the client and version to reach anything the methods
|
|
10
|
+
* don't cover, or use `.story` / `.stories` for the common reads. Those carry the
|
|
11
|
+
* request's version and the resolve options a Storyblok read almost always wants; the
|
|
12
|
+
* caller's params are spread last, so every default can be overridden. `resolve_links`
|
|
13
|
+
* is on because `href` depends on it.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // $lib/storyblok.server.ts
|
|
18
|
+
* export const storyblok = setup(client)
|
|
19
|
+
*
|
|
20
|
+
* // anywhere on the server
|
|
21
|
+
* await storyblok.story("layout")
|
|
22
|
+
* await storyblok.stories<Blog>({ content_type: "blog", per_page: 2 })
|
|
23
|
+
*
|
|
24
|
+
* const { client, version } = storyblok()
|
|
25
|
+
* await client.get("cdn/links", { version })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function setup(client: StoryblokClient): (() => {
|
|
29
|
+
client: StoryblokClient;
|
|
30
|
+
version: Version;
|
|
31
|
+
}) & {
|
|
32
|
+
/**
|
|
33
|
+
* A single story by full slug. Resolves the client's `relations`, unlike `stories`.
|
|
34
|
+
*
|
|
35
|
+
* `T` is the story's content type; it defaults to the SDK's permissive one, so an
|
|
36
|
+
* unparameterised call behaves as before.
|
|
37
|
+
*/
|
|
38
|
+
story<T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
39
|
+
[index: string]: any;
|
|
40
|
+
}>(slug: string, params?: ISbStoryParams): Promise<ISbStoryData<T>>;
|
|
41
|
+
/**
|
|
42
|
+
* The link tree as a flat array — Storyblok returns it keyed by uuid, which is
|
|
43
|
+
* almost never what you want. Filtering and ordering are left to the caller.
|
|
44
|
+
*/
|
|
45
|
+
links(params?: ISbLinksParams): Promise<ISbLink[]>;
|
|
46
|
+
/** A list of stories. `total` is the unpaged count, for "showing n of m". */
|
|
47
|
+
stories<T>(params?: ISbStoriesParams): Promise<{
|
|
48
|
+
stories: Array<ISbStoryData<T>>;
|
|
49
|
+
total: number;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getRequestEvent } from "$app/server";
|
|
2
|
+
import { handle_error } from "./storyblok.svelte.js";
|
|
3
|
+
/** The content version for the current request, as set by the `version` hook. */
|
|
4
|
+
export function version() {
|
|
5
|
+
return getRequestEvent().locals.version;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Bind a client to the current request.
|
|
9
|
+
*
|
|
10
|
+
* Returns a callable: call it for the client and version to reach anything the methods
|
|
11
|
+
* don't cover, or use `.story` / `.stories` for the common reads. Those carry the
|
|
12
|
+
* request's version and the resolve options a Storyblok read almost always wants; the
|
|
13
|
+
* caller's params are spread last, so every default can be overridden. `resolve_links`
|
|
14
|
+
* is on because `href` depends on it.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // $lib/storyblok.server.ts
|
|
19
|
+
* export const storyblok = setup(client)
|
|
20
|
+
*
|
|
21
|
+
* // anywhere on the server
|
|
22
|
+
* await storyblok.story("layout")
|
|
23
|
+
* await storyblok.stories<Blog>({ content_type: "blog", per_page: 2 })
|
|
24
|
+
*
|
|
25
|
+
* const { client, version } = storyblok()
|
|
26
|
+
* await client.get("cdn/links", { version })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function setup(client) {
|
|
30
|
+
const context = () => ({ client, version: version() });
|
|
31
|
+
return Object.assign(context, {
|
|
32
|
+
/**
|
|
33
|
+
* A single story by full slug. Resolves the client's `relations`, unlike `stories`.
|
|
34
|
+
*
|
|
35
|
+
* `T` is the story's content type; it defaults to the SDK's permissive one, so an
|
|
36
|
+
* unparameterised call behaves as before.
|
|
37
|
+
*/
|
|
38
|
+
async story(slug, params = {}) {
|
|
39
|
+
const response = await client
|
|
40
|
+
.get(`cdn/stories/${slug}`, {
|
|
41
|
+
version: version(),
|
|
42
|
+
resolve_links: "url",
|
|
43
|
+
resolve_relations: client.relations,
|
|
44
|
+
resolve_assets: 1,
|
|
45
|
+
...params,
|
|
46
|
+
})
|
|
47
|
+
.catch(handle_error);
|
|
48
|
+
return response.data.story;
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* The link tree as a flat array — Storyblok returns it keyed by uuid, which is
|
|
52
|
+
* almost never what you want. Filtering and ordering are left to the caller.
|
|
53
|
+
*/
|
|
54
|
+
async links(params = {}) {
|
|
55
|
+
const response = await client
|
|
56
|
+
.get("cdn/links", { version: version(), per_page: 1000, ...params })
|
|
57
|
+
.catch(handle_error);
|
|
58
|
+
return Object.values(response.data.links ?? {});
|
|
59
|
+
},
|
|
60
|
+
/** A list of stories. `total` is the unpaged count, for "showing n of m". */
|
|
61
|
+
async stories(params = {}) {
|
|
62
|
+
const response = await client
|
|
63
|
+
.get("cdn/stories", {
|
|
64
|
+
version: version(),
|
|
65
|
+
resolve_links: "url",
|
|
66
|
+
resolve_assets: 1,
|
|
67
|
+
...params,
|
|
68
|
+
})
|
|
69
|
+
.catch(handle_error);
|
|
70
|
+
return {
|
|
71
|
+
stories: response.data.stories,
|
|
72
|
+
total: response.total ?? 0,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderRichText as richtext, StoryblokComponent as Block, storyblokEditable as editable, useStoryblokApi, type ISbStoryData } from "@storyblok/svelte";
|
|
1
|
+
import { renderRichText as richtext, StoryblokComponent as Block, storyblokEditable as editable, useStoryblokApi, type ISbStoryData, type SbBlokData } from "@storyblok/svelte";
|
|
2
2
|
import type { Component } from "svelte";
|
|
3
3
|
import { SvelteMap } from "svelte/reactivity";
|
|
4
4
|
/**
|
|
@@ -25,6 +25,25 @@ export interface Storyblok<T extends ISbStoryData> {
|
|
|
25
25
|
}
|
|
26
26
|
/** Type for dynamic component imports */
|
|
27
27
|
type Import<T = unknown> = () => Promise<T>;
|
|
28
|
+
/** Everything a client needs, so a project doesn't have to subclass to configure one. */
|
|
29
|
+
export interface ClientOptions {
|
|
30
|
+
/** Your Storyblok access token */
|
|
31
|
+
token: string;
|
|
32
|
+
/**
|
|
33
|
+
* The block and template components to register.
|
|
34
|
+
*
|
|
35
|
+
* Call `import.meta.glob` at your own call site and pass the result — Vite has to see
|
|
36
|
+
* the literal pattern to rewrite it, so the glob can't live in here.
|
|
37
|
+
*/
|
|
38
|
+
components?: Record<string, Import>;
|
|
39
|
+
/** Relation fields to resolve on every request, e.g. `["shared.items"]` */
|
|
40
|
+
relations?: Array<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Content types that render through another component, e.g. `{ news: "blog" }` when
|
|
43
|
+
* news posts reuse the blog template. Applied by `template`.
|
|
44
|
+
*/
|
|
45
|
+
aliases?: Record<string, string>;
|
|
46
|
+
}
|
|
28
47
|
/** Type alias for the storyblok api instance */
|
|
29
48
|
type API = ReturnType<typeof useStoryblokApi>;
|
|
30
49
|
/**
|
|
@@ -37,12 +56,21 @@ export declare class StoryblokClient {
|
|
|
37
56
|
components: SvelteMap<string, Component<{}, {}, string>>;
|
|
38
57
|
/** Array of relation field names to resolve when fetching stories */
|
|
39
58
|
relations: Array<string>;
|
|
59
|
+
/** Content types that render through another component's template */
|
|
60
|
+
aliases: Record<string, string>;
|
|
40
61
|
/**
|
|
41
62
|
* Check if the current URL is in Storyblok preview mode
|
|
42
63
|
* @param url - The URL to check
|
|
43
64
|
* @returns True if in preview mode
|
|
44
65
|
*/
|
|
45
66
|
is_preview(url: URL): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a story to its component by `component` name.
|
|
69
|
+
*
|
|
70
|
+
* Falls back when the name isn't registered, so a content type added in Storyblok
|
|
71
|
+
* before its component exists renders as a generic page rather than blanking.
|
|
72
|
+
*/
|
|
73
|
+
template(story: ISbStoryData | null, fallback?: string): Component | null | undefined;
|
|
46
74
|
/** Fetch a single story or collection of stories */
|
|
47
75
|
get: API["get"];
|
|
48
76
|
/** Fetch all stories matching criteria (handles pagination automatically) */
|
|
@@ -55,20 +83,33 @@ export declare class StoryblokClient {
|
|
|
55
83
|
put: API["put"];
|
|
56
84
|
/** Make a DELETE request to the Storyblok API */
|
|
57
85
|
delete: API["delete"];
|
|
86
|
+
/** The standalone `handle_error`, on the instance so `.catch(client.handle_error)` works */
|
|
87
|
+
handle_error: typeof handle_error;
|
|
58
88
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @
|
|
89
|
+
* Configured here rather than by subclassing, so setup is one expression.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* export const client = new StoryblokClient({
|
|
94
|
+
* token: PUBLIC_STORYBLOK_ACCESS_TOKEN,
|
|
95
|
+
* relations: ["shared.items"],
|
|
96
|
+
* components: import.meta.glob(["$blocks/**\/*.svelte", "$templates/*.svelte"]),
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
62
99
|
*/
|
|
63
|
-
constructor(
|
|
100
|
+
constructor({ token, components, relations, aliases }: ClientOptions);
|
|
64
101
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
102
|
+
* Load the components and set up the SDK.
|
|
103
|
+
*
|
|
104
|
+
* Safe to call any number of times, including concurrently — the first call starts the
|
|
105
|
+
* work and every later one awaits that same promise. Server callers never need it; the
|
|
106
|
+
* API methods await it themselves. The browser does, since `components` must be
|
|
107
|
+
* populated before the first render.
|
|
67
108
|
*/
|
|
68
109
|
init(): Promise<void>;
|
|
69
110
|
/** Storyblok editable function for making content editable in preview mode */
|
|
70
|
-
editable: (node: HTMLElement, value:
|
|
71
|
-
update: (value:
|
|
111
|
+
editable: (node: HTMLElement, value: SbBlokData) => {
|
|
112
|
+
update: (value: SbBlokData) => void;
|
|
72
113
|
};
|
|
73
114
|
/**
|
|
74
115
|
* Connect a story to Storyblok's live preview system for real-time updates
|
|
@@ -84,5 +125,80 @@ export declare class StoryblokClient {
|
|
|
84
125
|
*/
|
|
85
126
|
connect<T extends ISbStoryData>(current: T): Storyblok<T>;
|
|
86
127
|
}
|
|
128
|
+
/** The Storyblok content version to read. `event.locals.version` from `storyloco/hooks`. */
|
|
129
|
+
export type Version = "draft" | "published";
|
|
130
|
+
/**
|
|
131
|
+
* Richtext, with `attrs` typed so an embedded blok's fields are reachable — the CLI types
|
|
132
|
+
* it as `Record<string, unknown>`. `Blocks` is your generated block union.
|
|
133
|
+
*/
|
|
134
|
+
export interface Richtext<Blocks = unknown> {
|
|
135
|
+
type: string;
|
|
136
|
+
text?: string;
|
|
137
|
+
attrs?: {
|
|
138
|
+
body: Blocks;
|
|
139
|
+
};
|
|
140
|
+
content?: Array<Richtext<Blocks>>;
|
|
141
|
+
marks?: Array<Richtext<Blocks>>;
|
|
142
|
+
}
|
|
143
|
+
/** Adds the `story.url` that `resolve_links: "url"` injects but the CLI doesn't declare. */
|
|
144
|
+
export type Resolved<T extends {
|
|
145
|
+
story?: unknown;
|
|
146
|
+
}> = T & {
|
|
147
|
+
story?: NonNullable<T["story"]> & {
|
|
148
|
+
url?: string;
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/** Structural, so a generated `StoryblokMultilink` satisfies it. */
|
|
152
|
+
interface Link {
|
|
153
|
+
linktype?: string;
|
|
154
|
+
url?: string;
|
|
155
|
+
cached_url?: string;
|
|
156
|
+
story?: {
|
|
157
|
+
url?: string;
|
|
158
|
+
[key: string]: unknown;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a multilink to an href. Story links become root-relative, everything else passes
|
|
163
|
+
* through. Prefers `story.url` over `cached_url`, which goes stale when a target moves —
|
|
164
|
+
* so pair with `resolve_links: "url"`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function href(link?: Link): string | undefined;
|
|
167
|
+
/**
|
|
168
|
+
* Turn a Storyblok error into a SvelteKit one, for `.catch(handle_error)`.
|
|
169
|
+
*
|
|
170
|
+
* A missing story is a 404; anything else is a 500, since the CMS being unreachable isn't
|
|
171
|
+
* something to show a visitor a Storyblok status code for.
|
|
172
|
+
*/
|
|
173
|
+
export declare function handle_error(err: unknown): never;
|
|
174
|
+
/** Declaring these keeps `SbBlokData`'s index signature from widening every access. */
|
|
175
|
+
interface BlokFields {
|
|
176
|
+
anchor?: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Spread onto a block's root element to wire it up for the visual editor.
|
|
180
|
+
*
|
|
181
|
+
* Prefer this to `use:editable`: being an attachment it spreads, so the element keeps its
|
|
182
|
+
* own attributes and a project can wrap it. Also sets a stable `id` from the block's
|
|
183
|
+
* `anchor`, falling back to `_uid`.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```svelte
|
|
187
|
+
* <script lang="ts">
|
|
188
|
+
* import { attrs } from "storyloco"
|
|
189
|
+
* import type { Blok, Hero } from "./components.schema.js"
|
|
190
|
+
*
|
|
191
|
+
* let { blok }: { blok: Blok<Hero> } = $props()
|
|
192
|
+
* </script>
|
|
193
|
+
*
|
|
194
|
+
* <section {...attrs(blok)}>
|
|
195
|
+
* <h1>{blok.headline}</h1>
|
|
196
|
+
* </section>
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export declare function attrs<T extends BlokFields>(blok: SbBlokData & T): {
|
|
200
|
+
[x: symbol]: (node: HTMLElement) => void;
|
|
201
|
+
id: string | undefined;
|
|
202
|
+
};
|
|
87
203
|
/** Re-export commonly used Storyblok utilities */
|
|
88
204
|
export { editable, Block, richtext };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { storyblokInit, apiPlugin, renderRichText as richtext, useStoryblokBridge, StoryblokComponent as Block, storyblokEditable as editable, useStoryblokApi, } from "@storyblok/svelte";
|
|
2
2
|
import { SvelteMap } from "svelte/reactivity";
|
|
3
|
+
import { createAttachmentKey as attach } from "svelte/attachments";
|
|
4
|
+
import { error } from "@sveltejs/kit";
|
|
3
5
|
/**
|
|
4
6
|
* Main Storyblok client class that handles component registration, API calls, and story bridging
|
|
5
7
|
* Provides a unified interface for working with Storyblok in Svelte applications
|
|
@@ -7,10 +9,12 @@ import { SvelteMap } from "svelte/reactivity";
|
|
|
7
9
|
export class StoryblokClient {
|
|
8
10
|
/** Map of registered Svelte components by name */
|
|
9
11
|
components = new SvelteMap();
|
|
10
|
-
/**
|
|
11
|
-
#
|
|
12
|
+
/** The in-flight or settled setup, shared by every caller of `init` */
|
|
13
|
+
#ready;
|
|
12
14
|
/** Array of relation field names to resolve when fetching stories */
|
|
13
15
|
relations = [];
|
|
16
|
+
/** Content types that render through another component's template */
|
|
17
|
+
aliases = {};
|
|
14
18
|
/**
|
|
15
19
|
* Check if the current URL is in Storyblok preview mode
|
|
16
20
|
* @param url - The URL to check
|
|
@@ -19,27 +23,74 @@ export class StoryblokClient {
|
|
|
19
23
|
is_preview(url) {
|
|
20
24
|
return url.searchParams.has("_storyblok");
|
|
21
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a story to its component by `component` name.
|
|
28
|
+
*
|
|
29
|
+
* Falls back when the name isn't registered, so a content type added in Storyblok
|
|
30
|
+
* before its component exists renders as a generic page rather than blanking.
|
|
31
|
+
*/
|
|
32
|
+
template(story, fallback = "page") {
|
|
33
|
+
if (!story)
|
|
34
|
+
return null;
|
|
35
|
+
const name = story.content.component || fallback;
|
|
36
|
+
return this.components.get(this.aliases[name] ?? name) ?? this.components.get(fallback);
|
|
37
|
+
}
|
|
22
38
|
/** Storyblok access token for API authentication */
|
|
23
39
|
#access_token;
|
|
24
40
|
/** Map of component import functions */
|
|
25
41
|
#component_imports;
|
|
42
|
+
/** Every API method below goes through here, so callers never `await init()` first. */
|
|
43
|
+
async #api() {
|
|
44
|
+
await this.init();
|
|
45
|
+
return useStoryblokApi();
|
|
46
|
+
}
|
|
47
|
+
/** Fetch a single story or collection of stories */
|
|
48
|
+
get = async (...args) => (await this.#api()).get(...args);
|
|
49
|
+
/** Fetch all stories matching criteria (handles pagination automatically) */
|
|
50
|
+
getAll = async (...args) => (await this.#api()).getAll(...args);
|
|
51
|
+
/** Fetch a specific story by slug */
|
|
52
|
+
getStory = async (...args) => (await this.#api()).getStory(...args);
|
|
53
|
+
/** Make a POST request to the Storyblok API */
|
|
54
|
+
post = async (...args) => (await this.#api()).post(...args);
|
|
55
|
+
/** Make a PUT request to the Storyblok API */
|
|
56
|
+
put = async (...args) => (await this.#api()).put(...args);
|
|
57
|
+
/** Make a DELETE request to the Storyblok API */
|
|
58
|
+
delete = async (...args) => (await this.#api()).delete(...args);
|
|
59
|
+
/** The standalone `handle_error`, on the instance so `.catch(client.handle_error)` works */
|
|
60
|
+
handle_error = handle_error;
|
|
26
61
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @
|
|
62
|
+
* Configured here rather than by subclassing, so setup is one expression.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* export const client = new StoryblokClient({
|
|
67
|
+
* token: PUBLIC_STORYBLOK_ACCESS_TOKEN,
|
|
68
|
+
* relations: ["shared.items"],
|
|
69
|
+
* components: import.meta.glob(["$blocks/**\/*.svelte", "$templates/*.svelte"]),
|
|
70
|
+
* })
|
|
71
|
+
* ```
|
|
30
72
|
*/
|
|
31
|
-
constructor(
|
|
32
|
-
this.#access_token =
|
|
33
|
-
this.#component_imports =
|
|
34
|
-
this.
|
|
73
|
+
constructor({ token, components = {}, relations = [], aliases = {} }) {
|
|
74
|
+
this.#access_token = token;
|
|
75
|
+
this.#component_imports = components;
|
|
76
|
+
this.relations = relations;
|
|
77
|
+
this.aliases = aliases;
|
|
78
|
+
// Eager, but handled so a failure surfaces to whoever awaits `init` rather than
|
|
79
|
+
// as an unhandled rejection
|
|
80
|
+
this.init().catch(() => { });
|
|
35
81
|
}
|
|
36
82
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
83
|
+
* Load the components and set up the SDK.
|
|
84
|
+
*
|
|
85
|
+
* Safe to call any number of times, including concurrently — the first call starts the
|
|
86
|
+
* work and every later one awaits that same promise. Server callers never need it; the
|
|
87
|
+
* API methods await it themselves. The browser does, since `components` must be
|
|
88
|
+
* populated before the first render.
|
|
39
89
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
init() {
|
|
91
|
+
return (this.#ready ??= this.#setup());
|
|
92
|
+
}
|
|
93
|
+
async #setup() {
|
|
43
94
|
// Load all components from the provided imports
|
|
44
95
|
for (const [path, fn] of Object.entries(this.#component_imports)) {
|
|
45
96
|
const name = path.split("/").pop()?.replace(".svelte", "") ?? "";
|
|
@@ -54,15 +105,6 @@ export class StoryblokClient {
|
|
|
54
105
|
use: [apiPlugin],
|
|
55
106
|
components: Object.fromEntries(this.components),
|
|
56
107
|
});
|
|
57
|
-
// Bind API methods to this instance
|
|
58
|
-
const api = useStoryblokApi();
|
|
59
|
-
this.get = api.get.bind(api);
|
|
60
|
-
this.getAll = api.getAll.bind(api);
|
|
61
|
-
this.getStory = api.getStory.bind(api);
|
|
62
|
-
this.post = api.post?.bind(api);
|
|
63
|
-
this.put = api.put?.bind(api);
|
|
64
|
-
this.delete = api.delete?.bind(api);
|
|
65
|
-
this.#initialized = true;
|
|
66
108
|
}
|
|
67
109
|
/** Storyblok editable function for making content editable in preview mode */
|
|
68
110
|
editable = editable;
|
|
@@ -95,5 +137,73 @@ export class StoryblokClient {
|
|
|
95
137
|
};
|
|
96
138
|
}
|
|
97
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a multilink to an href. Story links become root-relative, everything else passes
|
|
142
|
+
* through. Prefers `story.url` over `cached_url`, which goes stale when a target moves —
|
|
143
|
+
* so pair with `resolve_links: "url"`.
|
|
144
|
+
*/
|
|
145
|
+
export function href(link) {
|
|
146
|
+
if (!link)
|
|
147
|
+
return undefined;
|
|
148
|
+
return link.linktype === "story"
|
|
149
|
+
? `/${link.story?.url || link.cached_url}`
|
|
150
|
+
: link.url || link.cached_url;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Turn a Storyblok error into a SvelteKit one, for `.catch(handle_error)`.
|
|
154
|
+
*
|
|
155
|
+
* A missing story is a 404; anything else is a 500, since the CMS being unreachable isn't
|
|
156
|
+
* something to show a visitor a Storyblok status code for.
|
|
157
|
+
*/
|
|
158
|
+
export function handle_error(err) {
|
|
159
|
+
console.error(err);
|
|
160
|
+
const status = typeof err === "object" && err !== null && "status" in err ? err.status : undefined;
|
|
161
|
+
if (status === 404)
|
|
162
|
+
error(404, "Story not found");
|
|
163
|
+
error(500, "Internal server error");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Spread onto a block's root element to wire it up for the visual editor.
|
|
167
|
+
*
|
|
168
|
+
* Prefer this to `use:editable`: being an attachment it spreads, so the element keeps its
|
|
169
|
+
* own attributes and a project can wrap it. Also sets a stable `id` from the block's
|
|
170
|
+
* `anchor`, falling back to `_uid`.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```svelte
|
|
174
|
+
* <script lang="ts">
|
|
175
|
+
* import { attrs } from "storyloco"
|
|
176
|
+
* import type { Blok, Hero } from "./components.schema.js"
|
|
177
|
+
*
|
|
178
|
+
* let { blok }: { blok: Blok<Hero> } = $props()
|
|
179
|
+
* </script>
|
|
180
|
+
*
|
|
181
|
+
* <section {...attrs(blok)}>
|
|
182
|
+
* <h1>{blok.headline}</h1>
|
|
183
|
+
* </section>
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function attrs(blok) {
|
|
187
|
+
return {
|
|
188
|
+
[attach()](node) {
|
|
189
|
+
editable(node, blok);
|
|
190
|
+
},
|
|
191
|
+
id: blok.anchor || blok._uid,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Map section fields onto CSS custom properties by wrapping `attrs`, rather than reaching
|
|
195
|
+
// past it — the field names and design tokens are yours:
|
|
196
|
+
//
|
|
197
|
+
// interface SectionFields extends BlokFields {
|
|
198
|
+
// theme?: number | string
|
|
199
|
+
// pad_top?: number | string
|
|
200
|
+
// }
|
|
201
|
+
//
|
|
202
|
+
// export function section<T extends SectionFields>(blok: Blok<T>) {
|
|
203
|
+
// let style = ""
|
|
204
|
+
// if (blok.theme) style += `--theme: var(--c-${blok.theme});`
|
|
205
|
+
// if (blok.pad_top) style += `--pt: var(--s-${blok.pad_top});`
|
|
206
|
+
// return { ...attrs(blok), style }
|
|
207
|
+
// }
|
|
98
208
|
/** Re-export commonly used Storyblok utilities */
|
|
99
209
|
export { editable, Block, richtext };
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
|
2
|
-
import Content from './dropdown-menu-content.svelte';
|
|
3
|
-
import Group from './dropdown-menu-group.svelte';
|
|
4
|
-
import Item from './dropdown-menu-item.svelte';
|
|
5
|
-
import Label from './dropdown-menu-label.svelte';
|
|
6
|
-
import RadioGroup from './dropdown-menu-radio-group.svelte';
|
|
7
|
-
import RadioItem from './dropdown-menu-radio-item.svelte';
|
|
8
|
-
import Separator from './dropdown-menu-separator.svelte';
|
|
9
|
-
import Shortcut from './dropdown-menu-shortcut.svelte';
|
|
10
|
-
import Trigger from './dropdown-menu-trigger.svelte';
|
|
11
|
-
import SubContent from './dropdown-menu-sub-content.svelte';
|
|
12
|
-
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
|
13
|
-
import GroupHeading from './dropdown-menu-group-heading.svelte';
|
|
14
|
-
declare const Sub: import("svelte").Component<import("bits-ui").ContextMenuSubPropsWithoutHTML, {}, "open">;
|
|
15
|
-
declare const Root: import("svelte").Component<{
|
|
16
|
-
open?: boolean;
|
|
17
|
-
onOpenChange?: import("bits-ui/dist/internal/types").OnChangeFn<boolean>;
|
|
18
|
-
onOpenChangeComplete?: import("bits-ui/dist/internal/types").OnChangeFn<boolean>;
|
|
19
|
-
dir?: import("bits-ui").Direction;
|
|
20
|
-
} & {
|
|
21
|
-
children?: import("svelte").Snippet | undefined;
|
|
22
|
-
} & {
|
|
23
|
-
_internal_variant?: "context-menu" | "dropdown-menu" | "menubar";
|
|
24
|
-
}, {}, "open">;
|
|
25
|
-
export { CheckboxItem, Content, Root as DropdownMenu, CheckboxItem as DropdownMenuCheckboxItem, Content as DropdownMenuContent, Group as DropdownMenuGroup, Item as DropdownMenuItem, Label as DropdownMenuLabel, RadioGroup as DropdownMenuRadioGroup, RadioItem as DropdownMenuRadioItem, Separator as DropdownMenuSeparator, Shortcut as DropdownMenuShortcut, Sub as DropdownMenuSub, SubContent as DropdownMenuSubContent, SubTrigger as DropdownMenuSubTrigger, Trigger as DropdownMenuTrigger, GroupHeading as DropdownMenuGroupHeading, Group, GroupHeading, Item, Label, RadioGroup, RadioItem, Root, Separator, Shortcut, Sub, SubContent, SubTrigger, Trigger, };
|