@riverbankcms/sdk 0.1.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 +1892 -0
- package/dist/cli/index.js +327 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/analytics.d.mts +103 -0
- package/dist/client/analytics.d.ts +103 -0
- package/dist/client/analytics.js +197 -0
- package/dist/client/analytics.js.map +1 -0
- package/dist/client/analytics.mjs +169 -0
- package/dist/client/analytics.mjs.map +1 -0
- package/dist/client/bookings.d.mts +89 -0
- package/dist/client/bookings.d.ts +89 -0
- package/dist/client/bookings.js +34 -0
- package/dist/client/bookings.js.map +1 -0
- package/dist/client/bookings.mjs +11 -0
- package/dist/client/bookings.mjs.map +1 -0
- package/dist/client/client.d.mts +195 -0
- package/dist/client/client.d.ts +195 -0
- package/dist/client/client.js +606 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/client.mjs +572 -0
- package/dist/client/client.mjs.map +1 -0
- package/dist/client/hooks.d.mts +71 -0
- package/dist/client/hooks.d.ts +71 -0
- package/dist/client/hooks.js +264 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/client/hooks.mjs +235 -0
- package/dist/client/hooks.mjs.map +1 -0
- package/dist/client/rendering/client.d.mts +1 -0
- package/dist/client/rendering/client.d.ts +1 -0
- package/dist/client/rendering/client.js +33 -0
- package/dist/client/rendering/client.js.map +1 -0
- package/dist/client/rendering/client.mjs +8 -0
- package/dist/client/rendering/client.mjs.map +1 -0
- package/dist/client/usePage-BvKAa3Zw.d.mts +366 -0
- package/dist/client/usePage-BvKAa3Zw.d.ts +366 -0
- package/dist/server/chunk-2RW5HAQQ.mjs +86 -0
- package/dist/server/chunk-2RW5HAQQ.mjs.map +1 -0
- package/dist/server/chunk-3KKZVGH4.mjs +179 -0
- package/dist/server/chunk-3KKZVGH4.mjs.map +1 -0
- package/dist/server/chunk-4Z3GPTCS.js +179 -0
- package/dist/server/chunk-4Z3GPTCS.js.map +1 -0
- package/dist/server/chunk-4Z5FBFRL.mjs +211 -0
- package/dist/server/chunk-4Z5FBFRL.mjs.map +1 -0
- package/dist/server/chunk-ADREPXFU.js +86 -0
- package/dist/server/chunk-ADREPXFU.js.map +1 -0
- package/dist/server/chunk-F472SMKX.js +140 -0
- package/dist/server/chunk-F472SMKX.js.map +1 -0
- package/dist/server/chunk-GWBMJPLH.mjs +57 -0
- package/dist/server/chunk-GWBMJPLH.mjs.map +1 -0
- package/dist/server/chunk-JB4LIEFS.js +85 -0
- package/dist/server/chunk-JB4LIEFS.js.map +1 -0
- package/dist/server/chunk-PEAXKTDU.mjs +140 -0
- package/dist/server/chunk-PEAXKTDU.mjs.map +1 -0
- package/dist/server/chunk-QQ6U4QX6.js +120 -0
- package/dist/server/chunk-QQ6U4QX6.js.map +1 -0
- package/dist/server/chunk-R5YGLRUG.mjs +122 -0
- package/dist/server/chunk-R5YGLRUG.mjs.map +1 -0
- package/dist/server/chunk-SW7LE4M3.js +211 -0
- package/dist/server/chunk-SW7LE4M3.js.map +1 -0
- package/dist/server/chunk-W3K7LVPS.mjs +120 -0
- package/dist/server/chunk-W3K7LVPS.mjs.map +1 -0
- package/dist/server/chunk-WKG57P2H.mjs +85 -0
- package/dist/server/chunk-WKG57P2H.mjs.map +1 -0
- package/dist/server/chunk-YHEZMVTS.js +122 -0
- package/dist/server/chunk-YHEZMVTS.js.map +1 -0
- package/dist/server/chunk-YXDDFG3N.js +57 -0
- package/dist/server/chunk-YXDDFG3N.js.map +1 -0
- package/dist/server/components.d.mts +49 -0
- package/dist/server/components.d.ts +49 -0
- package/dist/server/components.js +22 -0
- package/dist/server/components.js.map +1 -0
- package/dist/server/components.mjs +22 -0
- package/dist/server/components.mjs.map +1 -0
- package/dist/server/config-validation.d.mts +300 -0
- package/dist/server/config-validation.d.ts +300 -0
- package/dist/server/config-validation.js +50 -0
- package/dist/server/config-validation.js.map +1 -0
- package/dist/server/config-validation.mjs +50 -0
- package/dist/server/config-validation.mjs.map +1 -0
- package/dist/server/config.d.mts +38 -0
- package/dist/server/config.d.ts +38 -0
- package/dist/server/config.js +44 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/config.mjs +44 -0
- package/dist/server/config.mjs.map +1 -0
- package/dist/server/data.d.mts +108 -0
- package/dist/server/data.d.ts +108 -0
- package/dist/server/data.js +15 -0
- package/dist/server/data.js.map +1 -0
- package/dist/server/data.mjs +15 -0
- package/dist/server/data.mjs.map +1 -0
- package/dist/server/index-B0yI_V6Z.d.mts +18 -0
- package/dist/server/index-C6M0Wfjq.d.ts +18 -0
- package/dist/server/index.d.mts +5 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +12 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/loadContent-CJcbYF3J.d.ts +152 -0
- package/dist/server/loadContent-zhlL4YSE.d.mts +152 -0
- package/dist/server/loadPage-BYmVMk0V.d.ts +216 -0
- package/dist/server/loadPage-CCf15nt8.d.mts +216 -0
- package/dist/server/loadPage-DVH3DW6E.js +9 -0
- package/dist/server/loadPage-DVH3DW6E.js.map +1 -0
- package/dist/server/loadPage-PHQZ6XQZ.mjs +9 -0
- package/dist/server/loadPage-PHQZ6XQZ.mjs.map +1 -0
- package/dist/server/metadata.d.mts +135 -0
- package/dist/server/metadata.d.ts +135 -0
- package/dist/server/metadata.js +68 -0
- package/dist/server/metadata.js.map +1 -0
- package/dist/server/metadata.mjs +68 -0
- package/dist/server/metadata.mjs.map +1 -0
- package/dist/server/rendering/server.d.mts +83 -0
- package/dist/server/rendering/server.d.ts +83 -0
- package/dist/server/rendering/server.js +14 -0
- package/dist/server/rendering/server.js.map +1 -0
- package/dist/server/rendering/server.mjs +14 -0
- package/dist/server/rendering/server.mjs.map +1 -0
- package/dist/server/rendering.d.mts +12 -0
- package/dist/server/rendering.d.ts +12 -0
- package/dist/server/rendering.js +40 -0
- package/dist/server/rendering.js.map +1 -0
- package/dist/server/rendering.mjs +40 -0
- package/dist/server/rendering.mjs.map +1 -0
- package/dist/server/routing.d.mts +115 -0
- package/dist/server/routing.d.ts +115 -0
- package/dist/server/routing.js +57 -0
- package/dist/server/routing.js.map +1 -0
- package/dist/server/routing.mjs +57 -0
- package/dist/server/routing.mjs.map +1 -0
- package/dist/server/server.d.mts +9 -0
- package/dist/server/server.d.ts +9 -0
- package/dist/server/server.js +21 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/server.mjs +21 -0
- package/dist/server/server.mjs.map +1 -0
- package/dist/server/theme-bridge.d.mts +232 -0
- package/dist/server/theme-bridge.d.ts +232 -0
- package/dist/server/theme-bridge.js +231 -0
- package/dist/server/theme-bridge.js.map +1 -0
- package/dist/server/theme-bridge.mjs +231 -0
- package/dist/server/theme-bridge.mjs.map +1 -0
- package/dist/server/theme.d.mts +40 -0
- package/dist/server/theme.d.ts +40 -0
- package/dist/server/theme.js +17 -0
- package/dist/server/theme.js.map +1 -0
- package/dist/server/theme.mjs +17 -0
- package/dist/server/theme.mjs.map +1 -0
- package/dist/server/types-BCeqWtI2.d.mts +333 -0
- package/dist/server/types-BCeqWtI2.d.ts +333 -0
- package/dist/server/types-Bbo01M7P.d.mts +76 -0
- package/dist/server/types-Bbo01M7P.d.ts +76 -0
- package/dist/server/types-C6gmRHLe.d.mts +150 -0
- package/dist/server/types-C6gmRHLe.d.ts +150 -0
- package/package.json +147 -0
- package/src/styles/index.css +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,1892 @@
|
|
|
1
|
+
# @riverbankcms/sdk
|
|
2
|
+
|
|
3
|
+
A lightweight TypeScript SDK for consuming Riverbank CMS content in React Server Components, Next.js, and client-side React applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Server-side rendering** with `loadPage()` helper
|
|
8
|
+
- ✅ **Unified content fetching** with `loadContent()` for pages and entries
|
|
9
|
+
- ✅ **Client-side data fetching** with `usePage()` and `useContent()` hooks
|
|
10
|
+
- ✅ **Automatic data prefetching** for blocks with loaders
|
|
11
|
+
- ✅ **Custom block data loaders** - Config-based (CMS endpoints) and code-based (external APIs)
|
|
12
|
+
- ✅ **Type-safe API client** with caching
|
|
13
|
+
- ✅ **React Server Components** compatible
|
|
14
|
+
- ✅ **Custom blocks** - Define site-specific blocks with full CMS editing support
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @riverbankcms/sdk
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @riverbankcms/sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
> **Important:** The `baseUrl` parameter must be the complete API URL including the `/api` path. For example: `https://dashboard.example.com/api`
|
|
27
|
+
|
|
28
|
+
### Server-Side Rendering (Recommended)
|
|
29
|
+
|
|
30
|
+
Use in Next.js App Router Server Components or getServerSideProps:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { createRiverbankClient, loadPage, Page } from '@riverbankcms/sdk';
|
|
34
|
+
|
|
35
|
+
// Note: baseUrl must include the /api path
|
|
36
|
+
const client = createRiverbankClient({
|
|
37
|
+
apiKey: process.env.RIVERBANK_API_KEY!,
|
|
38
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default async function HomePage() {
|
|
42
|
+
// Fetch all page data (site, page, block data) in parallel
|
|
43
|
+
const pageData = await loadPage({
|
|
44
|
+
client,
|
|
45
|
+
siteId: 'your-site-id',
|
|
46
|
+
path: '/',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return <Page {...pageData} />;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Client-Side Rendering
|
|
54
|
+
|
|
55
|
+
For client-only React apps or Client Components:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
"use client";
|
|
59
|
+
|
|
60
|
+
import { createRiverbankClient } from '@riverbankcms/sdk';
|
|
61
|
+
import { usePage, Page } from '@riverbankcms/sdk/client';
|
|
62
|
+
|
|
63
|
+
const client = createRiverbankClient({
|
|
64
|
+
apiKey: process.env.NEXT_PUBLIC_RIVERBANK_API_KEY!,
|
|
65
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export function DynamicPage({ path }: { path: string }) {
|
|
69
|
+
const pageData = usePage({ client, siteId: 'your-site-id', path });
|
|
70
|
+
|
|
71
|
+
if (pageData.loading) return <div>Loading...</div>;
|
|
72
|
+
if (pageData.error) return <div>Error: {pageData.error.message}</div>;
|
|
73
|
+
|
|
74
|
+
return <Page {...pageData} />;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API Reference
|
|
79
|
+
|
|
80
|
+
### Configuration
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const client = createRiverbankClient({
|
|
84
|
+
apiKey: string; // Required: Builder API key
|
|
85
|
+
baseUrl: string; // Required: Full API URL (e.g., 'https://dashboard.example.com/api')
|
|
86
|
+
cache?: {
|
|
87
|
+
enabled?: boolean; // Default: true
|
|
88
|
+
ttl?: number; // Default: 300 (seconds)
|
|
89
|
+
maxSize?: number; // Default: 100
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Server-Side API
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { loadPage, Page } from '@riverbankcms/sdk';
|
|
98
|
+
|
|
99
|
+
// Fetch all page data
|
|
100
|
+
const pageData = await loadPage({
|
|
101
|
+
client,
|
|
102
|
+
siteId: string,
|
|
103
|
+
path: string,
|
|
104
|
+
preview?: boolean, // Default: false - set true for draft content
|
|
105
|
+
pageId?: string, // Optional: explicit page ID
|
|
106
|
+
dataLoaderOverrides?: { // Optional: code-based data loaders for custom blocks
|
|
107
|
+
'custom.block-id': {
|
|
108
|
+
loaderKey: async (ctx) => fetchData(ctx.content.fieldValue),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Render the page
|
|
114
|
+
<Page {...pageData} />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Preview Mode (Draft Content)
|
|
118
|
+
|
|
119
|
+
The SDK supports fetching draft/unpublished content by passing `preview: true`. This requires an API key with site access.
|
|
120
|
+
|
|
121
|
+
**Server-Side Preview:**
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// app/preview/[[...slug]]/page.tsx
|
|
125
|
+
import { createRiverbankClient, loadPage, Page } from '@riverbankcms/sdk';
|
|
126
|
+
|
|
127
|
+
const client = createRiverbankClient({
|
|
128
|
+
apiKey: process.env.RIVERBANK_API_KEY!,
|
|
129
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export default async function PreviewPage({ params, searchParams }) {
|
|
133
|
+
const pageData = await loadPage({
|
|
134
|
+
client,
|
|
135
|
+
siteId: searchParams.siteId,
|
|
136
|
+
path: `/${params.slug?.join('/') || ''}`,
|
|
137
|
+
preview: true, // 🔑 Fetch draft content
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<div className="preview-banner">Preview Mode - Draft Content</div>
|
|
143
|
+
<Page {...pageData} />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Client-Side Preview:**
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
"use client";
|
|
153
|
+
|
|
154
|
+
import { createRiverbankClient } from '@riverbankcms/sdk';
|
|
155
|
+
import { usePage, Page } from '@riverbankcms/sdk/client';
|
|
156
|
+
|
|
157
|
+
const client = createRiverbankClient({
|
|
158
|
+
apiKey: process.env.NEXT_PUBLIC_RIVERBANK_API_KEY!,
|
|
159
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export function PreviewPage({ siteId, path }: { siteId: string; path: string }) {
|
|
163
|
+
const pageData = usePage({
|
|
164
|
+
client,
|
|
165
|
+
siteId,
|
|
166
|
+
path,
|
|
167
|
+
preview: true, // 🔑 Fetch draft content
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (pageData.loading) return <div>Loading preview...</div>;
|
|
171
|
+
if (pageData.error) return <div>Error: {pageData.error.message}</div>;
|
|
172
|
+
|
|
173
|
+
return <Page {...pageData} />;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**How Preview Works:**
|
|
178
|
+
|
|
179
|
+
- ✅ Uses your existing API key (no separate preview tokens needed)
|
|
180
|
+
- ✅ Returns draft content for blocks (via `draftContent` field)
|
|
181
|
+
- ✅ Shows unpublished pages that are in draft state
|
|
182
|
+
- ✅ Published and preview content are cached separately
|
|
183
|
+
- ✅ Requires API key to be scoped to the site
|
|
184
|
+
|
|
185
|
+
**Use Cases:**
|
|
186
|
+
|
|
187
|
+
- Preview pages before publishing
|
|
188
|
+
- Share draft content with stakeholders
|
|
189
|
+
- Test content changes in staging environment
|
|
190
|
+
- Content editing workflows
|
|
191
|
+
|
|
192
|
+
### Client-Side API
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { usePage, Page } from '@riverbankcms/sdk/client';
|
|
196
|
+
|
|
197
|
+
const pageData = usePage({
|
|
198
|
+
client,
|
|
199
|
+
siteId: string,
|
|
200
|
+
path: string,
|
|
201
|
+
preview?: boolean, // Default: false - set true for draft content
|
|
202
|
+
pageId?: string,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// pageData is a discriminated union:
|
|
206
|
+
// { loading: true, error: null, ... } |
|
|
207
|
+
// { loading: false, error: Error, ... } |
|
|
208
|
+
// { loading: false, error: null, page, theme, ... }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Unified Content Loading (Pages + Entries)
|
|
212
|
+
|
|
213
|
+
When a path might resolve to either a page or a content entry (blog post, product, etc.), use `loadContent()` or `useContent()` instead of the page-specific functions.
|
|
214
|
+
|
|
215
|
+
### Server-Side: loadContent
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { loadContent, isPageContent, Page } from '@riverbankcms/sdk';
|
|
219
|
+
|
|
220
|
+
export default async function DynamicRoute({ params }) {
|
|
221
|
+
const path = `/${params.slug?.join('/') || ''}`;
|
|
222
|
+
|
|
223
|
+
const content = await loadContent({
|
|
224
|
+
client,
|
|
225
|
+
siteId: 'your-site-id',
|
|
226
|
+
path,
|
|
227
|
+
preview: false, // Set true for draft content
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Pages get rendered with the Page component
|
|
231
|
+
if (isPageContent(content)) {
|
|
232
|
+
return <Page {...content} />;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Entries get rendered with custom UI
|
|
236
|
+
return (
|
|
237
|
+
<article>
|
|
238
|
+
<h1>{content.entry.title}</h1>
|
|
239
|
+
<div>{JSON.stringify(content.entry.content)}</div>
|
|
240
|
+
</article>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Client-Side: useContent
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
"use client";
|
|
249
|
+
|
|
250
|
+
import { useContent, isPageContentResult, Page } from '@riverbankcms/sdk/client';
|
|
251
|
+
|
|
252
|
+
export function DynamicContent({ path }: { path: string }) {
|
|
253
|
+
const content = useContent({
|
|
254
|
+
client,
|
|
255
|
+
siteId: 'your-site-id',
|
|
256
|
+
path,
|
|
257
|
+
preview: false,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (content.loading) return <div>Loading...</div>;
|
|
261
|
+
if (content.error) return <div>Error: {content.error.message}</div>;
|
|
262
|
+
|
|
263
|
+
if (isPageContentResult(content)) {
|
|
264
|
+
return <Page page={content.page} theme={content.theme} siteId={content.siteId} resolvedData={content.resolvedData} />;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Render entry with custom UI
|
|
268
|
+
return (
|
|
269
|
+
<article>
|
|
270
|
+
<h1>{content.entry.title}</h1>
|
|
271
|
+
<p>Type: {content.entry.type}</p>
|
|
272
|
+
<div>{JSON.stringify(content.entry.content)}</div>
|
|
273
|
+
</article>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Entry-Specific Rendering
|
|
279
|
+
|
|
280
|
+
Route entries to different components based on their content type:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { loadContent, isPageContent, Page } from '@riverbankcms/sdk';
|
|
284
|
+
|
|
285
|
+
export default async function DynamicRoute({ params }) {
|
|
286
|
+
const content = await loadContent({ client, siteId, path: `/${params.slug?.join('/') || ''}` });
|
|
287
|
+
|
|
288
|
+
if (isPageContent(content)) {
|
|
289
|
+
return <Page {...content} />;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Route to type-specific components
|
|
293
|
+
switch (content.entry.type) {
|
|
294
|
+
case 'blog-post':
|
|
295
|
+
return <BlogPost entry={content.entry} theme={content.theme} />;
|
|
296
|
+
case 'product':
|
|
297
|
+
return <ProductPage entry={content.entry} theme={content.theme} />;
|
|
298
|
+
case 'event':
|
|
299
|
+
return <EventPage entry={content.entry} theme={content.theme} />;
|
|
300
|
+
default:
|
|
301
|
+
return <GenericEntry entry={content.entry} />;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function BlogPost({ entry, theme }) {
|
|
306
|
+
const { title, content, metaDescription } = entry;
|
|
307
|
+
return (
|
|
308
|
+
<article>
|
|
309
|
+
<h1>{title}</h1>
|
|
310
|
+
<p className="lead">{metaDescription}</p>
|
|
311
|
+
<div className="prose">{content.body}</div>
|
|
312
|
+
<p>By {content.author} on {new Date(entry.createdAt).toLocaleDateString()}</p>
|
|
313
|
+
</article>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### ContentEntryData Type
|
|
319
|
+
|
|
320
|
+
When working with entries, you receive raw content data:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
type ContentEntryData = {
|
|
324
|
+
id: string;
|
|
325
|
+
type: string | null; // Content type key (e.g., 'blog-post', 'product')
|
|
326
|
+
title: string;
|
|
327
|
+
slug: string | null;
|
|
328
|
+
path: string | null;
|
|
329
|
+
status: string;
|
|
330
|
+
publishAt: string | null;
|
|
331
|
+
content: Record<string, unknown>; // Raw content fields
|
|
332
|
+
metaTitle: string | null;
|
|
333
|
+
metaDescription: string | null;
|
|
334
|
+
createdAt: string;
|
|
335
|
+
updatedAt: string;
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### When to Use Which Function
|
|
340
|
+
|
|
341
|
+
| Function | Use Case |
|
|
342
|
+
|----------|----------|
|
|
343
|
+
| `loadPage()` / `usePage()` | Pages only - throws error on entries |
|
|
344
|
+
| `loadContent()` / `useContent()` | Both pages and entries - discriminated union |
|
|
345
|
+
|
|
346
|
+
## Fetching Multiple Entries
|
|
347
|
+
|
|
348
|
+
Use `client.getEntries()` to fetch lists of content entries for blog listing pages, product catalogs, etc.
|
|
349
|
+
|
|
350
|
+
### Basic Usage
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { createRiverbankClient } from '@riverbankcms/sdk';
|
|
354
|
+
|
|
355
|
+
const client = createRiverbankClient({
|
|
356
|
+
apiKey: process.env.RIVERBANK_API_KEY!,
|
|
357
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Fetch all published blog posts
|
|
361
|
+
const { entries } = await client.getEntries({
|
|
362
|
+
siteId: 'your-site-id',
|
|
363
|
+
contentType: 'blog-post',
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Pagination and Sorting
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
// Fetch latest 10 posts (newest first)
|
|
371
|
+
const { entries: latestPosts } = await client.getEntries({
|
|
372
|
+
siteId: 'your-site-id',
|
|
373
|
+
contentType: 'blog-post',
|
|
374
|
+
limit: 10,
|
|
375
|
+
order: 'newest',
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Fetch oldest posts first (for archives)
|
|
379
|
+
const { entries: archivedPosts } = await client.getEntries({
|
|
380
|
+
siteId: 'your-site-id',
|
|
381
|
+
contentType: 'blog-post',
|
|
382
|
+
order: 'oldest',
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Include draft entries for preview mode
|
|
386
|
+
const { entries: allPosts } = await client.getEntries({
|
|
387
|
+
siteId: 'your-site-id',
|
|
388
|
+
contentType: 'blog-post',
|
|
389
|
+
preview: true, // Includes unpublished drafts
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Parameters
|
|
394
|
+
|
|
395
|
+
| Parameter | Type | Required | Description |
|
|
396
|
+
|-----------|------|----------|-------------|
|
|
397
|
+
| `siteId` | string | Yes | Site ID |
|
|
398
|
+
| `contentType` | string | Yes | Content type key (e.g., 'blog-post') |
|
|
399
|
+
| `limit` | number | No | Maximum entries to return |
|
|
400
|
+
| `order` | 'newest' \| 'oldest' | No | Sort by publish date |
|
|
401
|
+
| `preview` | boolean | No | Include draft entries |
|
|
402
|
+
|
|
403
|
+
### Real-World Example: Blog Listing Page
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// app/blog/page.tsx
|
|
407
|
+
import { createRiverbankClient } from '@riverbankcms/sdk';
|
|
408
|
+
import Link from 'next/link';
|
|
409
|
+
|
|
410
|
+
const client = createRiverbankClient({
|
|
411
|
+
apiKey: process.env.RIVERBANK_API_KEY!,
|
|
412
|
+
baseUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL + '/api',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
export default async function BlogPage() {
|
|
416
|
+
const site = await client.getSite({ slug: 'my-site' });
|
|
417
|
+
|
|
418
|
+
const { entries: posts } = await client.getEntries({
|
|
419
|
+
siteId: site.site.id,
|
|
420
|
+
contentType: 'blog-post',
|
|
421
|
+
limit: 10,
|
|
422
|
+
order: 'newest',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<main>
|
|
427
|
+
<h1>Blog</h1>
|
|
428
|
+
<div className="grid gap-6">
|
|
429
|
+
{posts.map((post) => (
|
|
430
|
+
<article key={post.id}>
|
|
431
|
+
<Link href={`/blog/${post.slug}`}>
|
|
432
|
+
<h2>{post.title}</h2>
|
|
433
|
+
</Link>
|
|
434
|
+
<p>{post.metaDescription}</p>
|
|
435
|
+
<time>{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : 'Draft'}</time>
|
|
436
|
+
</article>
|
|
437
|
+
))}
|
|
438
|
+
</div>
|
|
439
|
+
</main>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Import Patterns
|
|
445
|
+
|
|
446
|
+
⚠️ **Important:** Use the correct import path for your environment:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
// ✅ Server Components (Next.js App Router)
|
|
450
|
+
import { createRiverbankClient, loadPage, Page } from '@riverbankcms/sdk';
|
|
451
|
+
|
|
452
|
+
// ✅ Client Components (use "use client" directive)
|
|
453
|
+
"use client";
|
|
454
|
+
import { usePage, Page } from '@riverbankcms/sdk/client';
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The `/client` subpath exports React hooks that require client-side React features (useState, useEffect). The main entry point only exports server-safe code.
|
|
458
|
+
|
|
459
|
+
## Layout Component
|
|
460
|
+
|
|
461
|
+
The `Layout` component renders the site header, footer, and wraps your page content. Use this when you want consistent site chrome across all pages.
|
|
462
|
+
|
|
463
|
+
### Using Layout with Pre-Fetched Site Data
|
|
464
|
+
|
|
465
|
+
When you already have site data (from `loadPage` or `client.getSite`):
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
import { Layout } from '@riverbankcms/sdk';
|
|
469
|
+
|
|
470
|
+
export default async function MyPage() {
|
|
471
|
+
const siteData = await client.getSite({ slug: 'my-site' });
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<Layout siteData={siteData}>
|
|
475
|
+
<main>
|
|
476
|
+
<h1>My Custom Page</h1>
|
|
477
|
+
<p>This content is wrapped with site header and footer.</p>
|
|
478
|
+
</main>
|
|
479
|
+
</Layout>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Automatic Site Fetching
|
|
485
|
+
|
|
486
|
+
Layout can fetch site data automatically if you provide a client and identifier:
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { Layout } from '@riverbankcms/sdk';
|
|
490
|
+
|
|
491
|
+
export default async function MyPage() {
|
|
492
|
+
return (
|
|
493
|
+
<Layout
|
|
494
|
+
client={client}
|
|
495
|
+
slug="my-site" // or siteId="..." or domain="example.com"
|
|
496
|
+
>
|
|
497
|
+
<main>
|
|
498
|
+
<h1>My Custom Page</h1>
|
|
499
|
+
</main>
|
|
500
|
+
</Layout>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Customizing Header and Footer
|
|
506
|
+
|
|
507
|
+
You can hide the header or footer:
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
<Layout
|
|
511
|
+
siteData={siteData}
|
|
512
|
+
header={false} // Hide header
|
|
513
|
+
footer={false} // Hide footer
|
|
514
|
+
>
|
|
515
|
+
<main>Landing page without navigation</main>
|
|
516
|
+
</Layout>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Choosing Header Variants
|
|
520
|
+
|
|
521
|
+
Override the header variant from your code:
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
<Layout
|
|
525
|
+
siteData={siteData}
|
|
526
|
+
headerVariant="centered" // Options: classic, centered, transparent, floating, editorial
|
|
527
|
+
>
|
|
528
|
+
<main>Content with centered header</main>
|
|
529
|
+
</Layout>
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Fully Custom Headers
|
|
533
|
+
|
|
534
|
+
Create your own header while using CMS navigation data:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import { Layout, type HeaderData } from '@riverbankcms/sdk';
|
|
538
|
+
|
|
539
|
+
function CustomHeader({ menu, logo, site }: HeaderData) {
|
|
540
|
+
return (
|
|
541
|
+
<header className="custom-header">
|
|
542
|
+
<a href="/">{logo?.url && <img src={logo.url} alt={site.title} />}</a>
|
|
543
|
+
<nav>
|
|
544
|
+
{menu.items.map(item => (
|
|
545
|
+
<a key={item.id} href={item.url.href}>{item.label}</a>
|
|
546
|
+
))}
|
|
547
|
+
</nav>
|
|
548
|
+
</header>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export default async function MyPage() {
|
|
553
|
+
const siteData = await client.getSite({ slug: 'my-site' });
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<Layout
|
|
557
|
+
siteData={siteData}
|
|
558
|
+
header={(data) => <CustomHeader {...data} />}
|
|
559
|
+
>
|
|
560
|
+
<main>Custom header with CMS navigation</main>
|
|
561
|
+
</Layout>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Block Component
|
|
567
|
+
|
|
568
|
+
The `Block` component renders individual CMS blocks. Use this when you want to mix CMS content with custom JSX, or render blocks outside of a full page context.
|
|
569
|
+
|
|
570
|
+
### Basic Block Rendering
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
import { Block } from '@riverbankcms/sdk';
|
|
574
|
+
|
|
575
|
+
export default async function CustomPage() {
|
|
576
|
+
const siteData = await client.getSite({ slug: 'my-site' });
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<div>
|
|
580
|
+
<h1>Custom Header</h1>
|
|
581
|
+
|
|
582
|
+
{/* Render a CMS block inline */}
|
|
583
|
+
<Block
|
|
584
|
+
blockKind="block.hero"
|
|
585
|
+
content={{
|
|
586
|
+
headline: 'Welcome',
|
|
587
|
+
subheadline: 'To our custom page',
|
|
588
|
+
cta: { label: 'Get Started', href: '/signup' }
|
|
589
|
+
}}
|
|
590
|
+
theme={siteData.theme}
|
|
591
|
+
siteId={siteData.site.id}
|
|
592
|
+
/>
|
|
593
|
+
|
|
594
|
+
<p>More custom content...</p>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Block with Data Loading
|
|
601
|
+
|
|
602
|
+
Blocks can automatically load their data (e.g., blog posts, products) if they have data loaders configured:
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
<Block
|
|
606
|
+
blockKind="block.blog-listing"
|
|
607
|
+
blockId="block-abc123" // Block ID enables data loading
|
|
608
|
+
content={{ maxPosts: 10 }}
|
|
609
|
+
theme={siteData.theme}
|
|
610
|
+
siteId={siteData.site.id}
|
|
611
|
+
pageId={page.id} // Optional: for context-aware loaders
|
|
612
|
+
client={client} // Required for data loading
|
|
613
|
+
/>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Advanced Block Options
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
<Block
|
|
620
|
+
blockKind="block.hero"
|
|
621
|
+
blockId="block-xyz"
|
|
622
|
+
content={{ heading: 'Welcome' }}
|
|
623
|
+
theme={theme}
|
|
624
|
+
siteId="site-123"
|
|
625
|
+
|
|
626
|
+
// Optional data loading context
|
|
627
|
+
pageId="page-456"
|
|
628
|
+
previewStage="preview" // or "published"
|
|
629
|
+
client={client}
|
|
630
|
+
|
|
631
|
+
// Show placeholder data for missing loaders
|
|
632
|
+
usePlaceholders={true}
|
|
633
|
+
/>
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Metadata Generation
|
|
637
|
+
|
|
638
|
+
Generate SEO-optimized metadata for Next.js pages from Riverbank CMS data.
|
|
639
|
+
|
|
640
|
+
### Basic Usage
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
import { generatePageMetadata } from '@riverbankcms/sdk/metadata';
|
|
644
|
+
import { loadPage } from '@riverbankcms/sdk';
|
|
645
|
+
|
|
646
|
+
export async function generateMetadata({ params }) {
|
|
647
|
+
const pageData = await loadPage({ client, siteId, path: params.slug });
|
|
648
|
+
const siteData = await client.getSite({ id: siteId });
|
|
649
|
+
|
|
650
|
+
return generatePageMetadata({
|
|
651
|
+
page: pageData.page,
|
|
652
|
+
site: siteData.site,
|
|
653
|
+
path: params.slug || '/',
|
|
654
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### With Custom Overrides
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
const metadata = generatePageMetadata({
|
|
663
|
+
page: pageData.page,
|
|
664
|
+
site: siteData.site,
|
|
665
|
+
path: '/',
|
|
666
|
+
siteUrl: 'https://example.com',
|
|
667
|
+
overrides: {
|
|
668
|
+
title: 'Custom SEO Title',
|
|
669
|
+
description: 'Custom SEO description with keywords',
|
|
670
|
+
ogImage: 'https://example.com/og-image.jpg',
|
|
671
|
+
canonicalUrl: 'https://example.com/canonical-path',
|
|
672
|
+
},
|
|
673
|
+
googleSiteVerification: 'verification-token',
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Preview Environment Metadata
|
|
678
|
+
|
|
679
|
+
```tsx
|
|
680
|
+
import { generatePreviewMetadata } from '@riverbankcms/sdk/metadata';
|
|
681
|
+
|
|
682
|
+
// Adds noindex/nofollow for staging environments
|
|
683
|
+
const metadata = generatePreviewMetadata({
|
|
684
|
+
page: pageData.page,
|
|
685
|
+
site: siteData.site,
|
|
686
|
+
path: '/',
|
|
687
|
+
siteUrl: 'https://preview.example.com',
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
## Route Resolution
|
|
692
|
+
|
|
693
|
+
Resolve URL paths to pages, redirects, or 404s for dynamic routing.
|
|
694
|
+
|
|
695
|
+
### Single Route Resolution
|
|
696
|
+
|
|
697
|
+
```tsx
|
|
698
|
+
import { resolveRoute } from '@riverbankcms/sdk/routing';
|
|
699
|
+
import { notFound, redirect } from 'next/navigation';
|
|
700
|
+
|
|
701
|
+
export default async function DynamicPage({ params }) {
|
|
702
|
+
const path = `/${params.slug?.join('/') || ''}`;
|
|
703
|
+
|
|
704
|
+
const resolution = await resolveRoute({
|
|
705
|
+
client,
|
|
706
|
+
siteId: 'your-site-id',
|
|
707
|
+
path,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
if (resolution.type === 'redirect') {
|
|
711
|
+
redirect(resolution.destination);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (resolution.type === 'not-found') {
|
|
715
|
+
notFound();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return <Page {...resolution.pageData} />;
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Batch Route Resolution
|
|
723
|
+
|
|
724
|
+
```tsx
|
|
725
|
+
import { resolveRoutes } from '@riverbankcms/sdk/routing';
|
|
726
|
+
|
|
727
|
+
// Useful for sitemap generation or validation
|
|
728
|
+
const resolutions = await resolveRoutes({
|
|
729
|
+
client,
|
|
730
|
+
siteId: 'your-site-id',
|
|
731
|
+
paths: ['/', '/about', '/services', '/contact'],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
resolutions.forEach(({ path, resolution }) => {
|
|
735
|
+
if (resolution.type === 'page') {
|
|
736
|
+
console.log(`${path} → Page: ${resolution.pageData.page.name}`);
|
|
737
|
+
} else if (resolution.type === 'redirect') {
|
|
738
|
+
console.log(`${path} → Redirect to ${resolution.destination}`);
|
|
739
|
+
} else {
|
|
740
|
+
console.log(`${path} → Not found`);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## Analytics
|
|
746
|
+
|
|
747
|
+
Track page views, CTA clicks, form submissions, and custom events.
|
|
748
|
+
|
|
749
|
+
### Add Analytics to Layout
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
import { AnalyticsBootstrap } from '@riverbankcms/sdk/analytics';
|
|
753
|
+
|
|
754
|
+
export default function RootLayout({ children }) {
|
|
755
|
+
return (
|
|
756
|
+
<html>
|
|
757
|
+
<body>
|
|
758
|
+
{children}
|
|
759
|
+
|
|
760
|
+
{/* Auto-tracks page views on navigation */}
|
|
761
|
+
<AnalyticsBootstrap
|
|
762
|
+
siteId="your-site-id"
|
|
763
|
+
siteSlug="your-site-slug"
|
|
764
|
+
endpoint="/api/analytics/collect"
|
|
765
|
+
/>
|
|
766
|
+
</body>
|
|
767
|
+
</html>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Track Events in Components
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
'use client';
|
|
776
|
+
|
|
777
|
+
import { useAnalytics } from '@riverbankcms/sdk/analytics';
|
|
778
|
+
|
|
779
|
+
export function MyComponent() {
|
|
780
|
+
const analytics = useAnalytics({
|
|
781
|
+
siteId: 'your-site-id',
|
|
782
|
+
siteSlug: 'your-site-slug',
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const handleClick = () => {
|
|
786
|
+
analytics.trackCtaClick({ buttonLabel: 'Sign Up' });
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const handleSubmit = () => {
|
|
790
|
+
analytics.trackFormSubmit({ formName: 'contact-form' });
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const handleCustomEvent = () => {
|
|
794
|
+
analytics.trackEvent({
|
|
795
|
+
eventType: 'video_play',
|
|
796
|
+
metadata: { videoId: '123', duration: 120 },
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
return (
|
|
801
|
+
<>
|
|
802
|
+
<button onClick={handleClick}>Sign Up</button>
|
|
803
|
+
<form onSubmit={handleSubmit}>...</form>
|
|
804
|
+
</>
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
## Theming
|
|
810
|
+
|
|
811
|
+
Style Builder blocks to match your brand using the Theme Bridge. This provides CSS variables and optional component styles without requiring the full CMS theme system.
|
|
812
|
+
|
|
813
|
+
### Quick Start
|
|
814
|
+
|
|
815
|
+
Wrap your app with `ThemeBridgeProvider` and define your color tokens:
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
import { ThemeBridgeProvider } from '@riverbankcms/sdk/theme-bridge';
|
|
819
|
+
|
|
820
|
+
export default function RootLayout({ children }) {
|
|
821
|
+
return (
|
|
822
|
+
<ThemeBridgeProvider
|
|
823
|
+
config={{
|
|
824
|
+
tokens: {
|
|
825
|
+
primary: '#6d28d9',
|
|
826
|
+
secondary: '#4c1d95',
|
|
827
|
+
background: '#ffffff',
|
|
828
|
+
text: '#1e293b',
|
|
829
|
+
},
|
|
830
|
+
}}
|
|
831
|
+
>
|
|
832
|
+
{children}
|
|
833
|
+
</ThemeBridgeProvider>
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Token System
|
|
839
|
+
|
|
840
|
+
Define color tokens as key-value pairs. Keys become CSS variables (`--color-{key}`):
|
|
841
|
+
|
|
842
|
+
```tsx
|
|
843
|
+
<ThemeBridgeProvider
|
|
844
|
+
config={{
|
|
845
|
+
tokens: {
|
|
846
|
+
// Brand colors
|
|
847
|
+
primary: '#6d28d9',
|
|
848
|
+
secondary: '#4c1d95',
|
|
849
|
+
|
|
850
|
+
// Backgrounds
|
|
851
|
+
background: '#ffffff',
|
|
852
|
+
surface: '#f8fafc',
|
|
853
|
+
|
|
854
|
+
// Text
|
|
855
|
+
text: '#1e293b',
|
|
856
|
+
mutedText: '#64748b',
|
|
857
|
+
|
|
858
|
+
// UI
|
|
859
|
+
border: '#e2e8f0',
|
|
860
|
+
white: '#ffffff',
|
|
861
|
+
|
|
862
|
+
// Status
|
|
863
|
+
success: '#22c55e',
|
|
864
|
+
warning: '#f59e0b',
|
|
865
|
+
danger: '#ef4444',
|
|
866
|
+
},
|
|
867
|
+
}}
|
|
868
|
+
>
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Token values can be:
|
|
872
|
+
- **Hex colors**: `'#6d28d9'` (converted to RGB for Tailwind alpha support)
|
|
873
|
+
- **CSS variable refs**: `'var(--brand-purple)'` (passed through)
|
|
874
|
+
- **RGB values**: `'109 40 217'` (used directly)
|
|
875
|
+
|
|
876
|
+
### Design Presets
|
|
877
|
+
|
|
878
|
+
Control typography, spacing, corners, and shadows:
|
|
879
|
+
|
|
880
|
+
```tsx
|
|
881
|
+
<ThemeBridgeProvider
|
|
882
|
+
config={{
|
|
883
|
+
tokens: { /* ... */ },
|
|
884
|
+
|
|
885
|
+
// Typography
|
|
886
|
+
typography: {
|
|
887
|
+
headingFamily: '"Inter", sans-serif',
|
|
888
|
+
bodyFamily: '"Inter", sans-serif',
|
|
889
|
+
headingWeight: 700,
|
|
890
|
+
bodyWeight: 400,
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
// Spacing density
|
|
894
|
+
spacing: 'standard', // 'comfortable' | 'standard' | 'dense'
|
|
895
|
+
|
|
896
|
+
// Corner radius
|
|
897
|
+
corners: 'rounded', // 'square' | 'soft' | 'rounded' | 'pill'
|
|
898
|
+
|
|
899
|
+
// Shadow intensity
|
|
900
|
+
shadows: 'medium', // 'none' | 'low' | 'medium' | 'high'
|
|
901
|
+
}}
|
|
902
|
+
>
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### Component CSS (Opt-in)
|
|
906
|
+
|
|
907
|
+
By default, only CSS variables are generated. Enable component CSS for buttons, cards, and inputs:
|
|
908
|
+
|
|
909
|
+
```tsx
|
|
910
|
+
<ThemeBridgeProvider
|
|
911
|
+
config={{
|
|
912
|
+
tokens: {
|
|
913
|
+
primary: '#6d28d9',
|
|
914
|
+
secondary: '#4c1d95',
|
|
915
|
+
white: '#ffffff',
|
|
916
|
+
surface: '#f8fafc',
|
|
917
|
+
text: '#1e293b',
|
|
918
|
+
border: '#e2e8f0',
|
|
919
|
+
},
|
|
920
|
+
corners: 'rounded',
|
|
921
|
+
shadows: 'medium',
|
|
922
|
+
components: {
|
|
923
|
+
buttons: true, // Generates .button-primary, .button-secondary, etc.
|
|
924
|
+
cards: true, // Generates .card-default, .card-elevated, etc.
|
|
925
|
+
inputs: true, // Generates .form-input, .form-label, etc.
|
|
926
|
+
},
|
|
927
|
+
}}
|
|
928
|
+
>
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
### Custom Component Variants
|
|
932
|
+
|
|
933
|
+
Specify which variants to generate:
|
|
934
|
+
|
|
935
|
+
```tsx
|
|
936
|
+
components: {
|
|
937
|
+
buttons: {
|
|
938
|
+
variants: ['primary', 'secondary'], // Only these variants
|
|
939
|
+
},
|
|
940
|
+
cards: {
|
|
941
|
+
variants: ['default', 'elevated'],
|
|
942
|
+
},
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### CSS Overrides
|
|
947
|
+
|
|
948
|
+
Add custom CSS rules scoped to theme components:
|
|
949
|
+
|
|
950
|
+
```tsx
|
|
951
|
+
<ThemeBridgeProvider
|
|
952
|
+
config={{
|
|
953
|
+
tokens: { /* ... */ },
|
|
954
|
+
components: { buttons: true },
|
|
955
|
+
overrides: {
|
|
956
|
+
'.button-primary': 'border-radius: 9999px; font-weight: 700;',
|
|
957
|
+
'.button-primary:hover': 'transform: translateY(-2px);',
|
|
958
|
+
},
|
|
959
|
+
}}
|
|
960
|
+
>
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Pass-Through to Design Systems
|
|
964
|
+
|
|
965
|
+
Reference existing CSS variables from your design system:
|
|
966
|
+
|
|
967
|
+
```tsx
|
|
968
|
+
<ThemeBridgeProvider
|
|
969
|
+
config={{
|
|
970
|
+
tokens: {
|
|
971
|
+
primary: 'var(--brand-purple)',
|
|
972
|
+
secondary: 'var(--brand-navy)',
|
|
973
|
+
background: 'var(--ds-bg)',
|
|
974
|
+
text: 'var(--ds-text)',
|
|
975
|
+
},
|
|
976
|
+
components: { buttons: true },
|
|
977
|
+
}}
|
|
978
|
+
>
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### Using Generated CSS Directly
|
|
982
|
+
|
|
983
|
+
For advanced use cases, generate CSS without the provider:
|
|
984
|
+
|
|
985
|
+
```tsx
|
|
986
|
+
import { generateThemeBridgeCss } from '@riverbankcms/sdk/theme-bridge';
|
|
987
|
+
|
|
988
|
+
const { css, cssVars } = generateThemeBridgeCss({
|
|
989
|
+
tokens: {
|
|
990
|
+
primary: '#6d28d9',
|
|
991
|
+
background: '#ffffff',
|
|
992
|
+
},
|
|
993
|
+
components: { buttons: true },
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// css: Full CSS string for injection
|
|
997
|
+
// cssVars: Object of CSS variable name-value pairs
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### Available Exports
|
|
1001
|
+
|
|
1002
|
+
```tsx
|
|
1003
|
+
import {
|
|
1004
|
+
// Provider component
|
|
1005
|
+
ThemeBridgeProvider,
|
|
1006
|
+
useThemeBridgeCss,
|
|
1007
|
+
|
|
1008
|
+
// CSS generator
|
|
1009
|
+
generateThemeBridgeCss,
|
|
1010
|
+
|
|
1011
|
+
// Types
|
|
1012
|
+
type ThemeBridgeConfig,
|
|
1013
|
+
type ThemeBridgeTypography,
|
|
1014
|
+
type ThemeBridgeSpacing,
|
|
1015
|
+
type ThemeBridgeCorners,
|
|
1016
|
+
type ThemeBridgeShadows,
|
|
1017
|
+
type ThemeBridgeComponents,
|
|
1018
|
+
type ThemeBridgeOutput,
|
|
1019
|
+
} from '@riverbankcms/sdk/theme-bridge';
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
## Block Field Options
|
|
1023
|
+
|
|
1024
|
+
Customize field options for specific blocks directly in your SDK configuration. This allows SDK sites to define site-specific choices for select fields in system blocks.
|
|
1025
|
+
|
|
1026
|
+
### Basic Configuration
|
|
1027
|
+
|
|
1028
|
+
Add `blockFieldOptions` to your `riverbank.config.ts`:
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
import { defineConfig } from '@riverbankcms/sdk/config';
|
|
1032
|
+
|
|
1033
|
+
export default defineConfig({
|
|
1034
|
+
siteId: 'your-site-id',
|
|
1035
|
+
blockFieldOptions: {
|
|
1036
|
+
'block.embed': {
|
|
1037
|
+
layout: {
|
|
1038
|
+
options: [
|
|
1039
|
+
{ value: 'showcase', label: 'Showcase Grid' },
|
|
1040
|
+
{ value: 'list', label: 'Simple List' },
|
|
1041
|
+
{ value: 'featured', label: 'Featured Hero' },
|
|
1042
|
+
]
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
### How It Works
|
|
1050
|
+
|
|
1051
|
+
1. **Block ID format**: Use `block.*` for system blocks or `custom.*` for custom blocks
|
|
1052
|
+
2. **Field ID**: The field within the block whose options you want to override
|
|
1053
|
+
3. **Options**: Array of `{ value, label }` objects that replace the default options
|
|
1054
|
+
|
|
1055
|
+
When a block field uses the `sdkSelect` widget (like the embed block's `layout` field), it will:
|
|
1056
|
+
- Use SDK-provided options when available in `blockFieldOptions`
|
|
1057
|
+
- Fall back to the field's default options if no SDK options are configured
|
|
1058
|
+
|
|
1059
|
+
### Types
|
|
1060
|
+
|
|
1061
|
+
```typescript
|
|
1062
|
+
import type {
|
|
1063
|
+
FieldSelectOption,
|
|
1064
|
+
BlockFieldConfig,
|
|
1065
|
+
BlockFieldOptionsMap,
|
|
1066
|
+
} from '@riverbankcms/sdk/config';
|
|
1067
|
+
|
|
1068
|
+
// A single select option
|
|
1069
|
+
type FieldSelectOption = {
|
|
1070
|
+
value: string; // Value stored when selected
|
|
1071
|
+
label: string; // Display text in dropdown
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// Configuration for a field
|
|
1075
|
+
type BlockFieldConfig = {
|
|
1076
|
+
options?: FieldSelectOption[]; // Override select options
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Map of block IDs to field configurations
|
|
1080
|
+
type BlockFieldOptionsMap = Record<string, Record<string, BlockFieldConfig>>;
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Current Use Cases
|
|
1084
|
+
|
|
1085
|
+
**Embed Block Layout**: The `block.embed` block uses `sdkSelect` for its `layout` field, allowing SDK sites to define custom layout options that match their site-specific renderers:
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
export default defineConfig({
|
|
1089
|
+
siteId: 'your-site-id',
|
|
1090
|
+
blockFieldOptions: {
|
|
1091
|
+
'block.embed': {
|
|
1092
|
+
layout: {
|
|
1093
|
+
options: [
|
|
1094
|
+
{ value: 'blog-grid', label: 'Blog Grid' },
|
|
1095
|
+
{ value: 'team-cards', label: 'Team Cards' },
|
|
1096
|
+
{ value: 'portfolio', label: 'Portfolio Gallery' },
|
|
1097
|
+
]
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
Then in your block override, handle each layout:
|
|
1105
|
+
|
|
1106
|
+
```tsx
|
|
1107
|
+
function EmbedRenderer({ content, data }) {
|
|
1108
|
+
switch (content.layout) {
|
|
1109
|
+
case 'blog-grid':
|
|
1110
|
+
return <BlogGrid entries={data.entries} />;
|
|
1111
|
+
case 'team-cards':
|
|
1112
|
+
return <TeamCards entries={data.entries} />;
|
|
1113
|
+
case 'portfolio':
|
|
1114
|
+
return <PortfolioGallery entries={data.entries} />;
|
|
1115
|
+
default:
|
|
1116
|
+
return <DefaultList entries={data.entries} />;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Validation Rules
|
|
1122
|
+
|
|
1123
|
+
- Block IDs must match pattern: `block.*` or `custom.*` (lowercase letters, numbers, hyphens)
|
|
1124
|
+
- Field IDs must be non-empty strings
|
|
1125
|
+
- Each option must have both `value` and `label` (non-empty strings)
|
|
1126
|
+
- At least one option is required when `options` is specified
|
|
1127
|
+
|
|
1128
|
+
## Block Field Extensions
|
|
1129
|
+
|
|
1130
|
+
Add custom fields to built-in block types without modifying the core CMS. This is different from `blockFieldOptions` (which overrides options for existing fields) – `blockFieldExtensions` lets you add entirely new fields.
|
|
1131
|
+
|
|
1132
|
+
### Basic Configuration
|
|
1133
|
+
|
|
1134
|
+
Add `blockFieldExtensions` to your `riverbank.config.ts`:
|
|
1135
|
+
|
|
1136
|
+
```typescript
|
|
1137
|
+
import { defineConfig } from '@riverbankcms/sdk/config';
|
|
1138
|
+
|
|
1139
|
+
export default defineConfig({
|
|
1140
|
+
siteId: 'your-site-id',
|
|
1141
|
+
blockFieldExtensions: {
|
|
1142
|
+
'block.body-text': {
|
|
1143
|
+
fields: [
|
|
1144
|
+
{
|
|
1145
|
+
id: 'layout',
|
|
1146
|
+
type: 'select',
|
|
1147
|
+
label: 'Layout',
|
|
1148
|
+
defaultValue: 'default',
|
|
1149
|
+
required: false,
|
|
1150
|
+
multiple: false,
|
|
1151
|
+
options: [
|
|
1152
|
+
{ value: 'default', label: 'Default' },
|
|
1153
|
+
{ value: 'wide', label: 'Wide' },
|
|
1154
|
+
{ value: 'narrow', label: 'Narrow' },
|
|
1155
|
+
{ value: 'pullquote', label: 'Pull Quote' },
|
|
1156
|
+
],
|
|
1157
|
+
},
|
|
1158
|
+
],
|
|
1159
|
+
},
|
|
1160
|
+
'block.hero': {
|
|
1161
|
+
fields: [
|
|
1162
|
+
{
|
|
1163
|
+
id: 'videoBackground',
|
|
1164
|
+
type: 'media',
|
|
1165
|
+
label: 'Video Background',
|
|
1166
|
+
description: 'Optional video to play behind the hero',
|
|
1167
|
+
mediaKinds: ['video'],
|
|
1168
|
+
required: false,
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
id: 'overlayOpacity',
|
|
1172
|
+
type: 'number',
|
|
1173
|
+
label: 'Overlay Opacity',
|
|
1174
|
+
description: 'Darkness of the overlay (0-100)',
|
|
1175
|
+
defaultValue: 50,
|
|
1176
|
+
required: false,
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
### How It Works
|
|
1185
|
+
|
|
1186
|
+
1. **Extended fields appear at the end** of the block's editing form in the CMS
|
|
1187
|
+
2. **Field values are stored** alongside normal block content
|
|
1188
|
+
3. **Values are accessible** in your `blockOverrides` via the `content` prop
|
|
1189
|
+
4. **All field types supported**: text, select, media, repeater, group, etc.
|
|
1190
|
+
|
|
1191
|
+
### Accessing Extended Fields in blockOverrides
|
|
1192
|
+
|
|
1193
|
+
```tsx
|
|
1194
|
+
import { loadPage, Page } from '@riverbankcms/sdk';
|
|
1195
|
+
|
|
1196
|
+
export default async function CMSPage({ params }) {
|
|
1197
|
+
const pageData = await loadPage({ client, siteId, path: '/' });
|
|
1198
|
+
|
|
1199
|
+
return (
|
|
1200
|
+
<Page
|
|
1201
|
+
{...pageData}
|
|
1202
|
+
blockOverrides={{
|
|
1203
|
+
bodyText: ({ content }) => {
|
|
1204
|
+
// Extended field values are available on content
|
|
1205
|
+
const layout = content.layout ?? 'default';
|
|
1206
|
+
|
|
1207
|
+
const layoutClass = {
|
|
1208
|
+
default: 'max-w-prose mx-auto',
|
|
1209
|
+
wide: 'max-w-4xl mx-auto',
|
|
1210
|
+
narrow: 'max-w-md mx-auto',
|
|
1211
|
+
pullquote: 'max-w-lg mx-auto text-xl italic border-l-4 pl-6',
|
|
1212
|
+
}[layout];
|
|
1213
|
+
|
|
1214
|
+
return (
|
|
1215
|
+
<div className={layoutClass}>
|
|
1216
|
+
<RichText content={content.body} />
|
|
1217
|
+
</div>
|
|
1218
|
+
);
|
|
1219
|
+
},
|
|
1220
|
+
hero: ({ content }) => {
|
|
1221
|
+
const { videoBackground, overlayOpacity = 50 } = content;
|
|
1222
|
+
|
|
1223
|
+
return (
|
|
1224
|
+
<div className="relative">
|
|
1225
|
+
{videoBackground && (
|
|
1226
|
+
<video
|
|
1227
|
+
src={videoBackground.url}
|
|
1228
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
1229
|
+
autoPlay
|
|
1230
|
+
muted
|
|
1231
|
+
loop
|
|
1232
|
+
/>
|
|
1233
|
+
)}
|
|
1234
|
+
<div
|
|
1235
|
+
className="absolute inset-0 bg-black"
|
|
1236
|
+
style={{ opacity: overlayOpacity / 100 }}
|
|
1237
|
+
/>
|
|
1238
|
+
<div className="relative z-10">
|
|
1239
|
+
<h1>{content.headline}</h1>
|
|
1240
|
+
</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
);
|
|
1243
|
+
},
|
|
1244
|
+
}}
|
|
1245
|
+
/>
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
### Types
|
|
1251
|
+
|
|
1252
|
+
```typescript
|
|
1253
|
+
import type {
|
|
1254
|
+
BlockFieldExtension,
|
|
1255
|
+
BlockFieldExtensionsMap,
|
|
1256
|
+
FieldDefinition,
|
|
1257
|
+
} from '@riverbankcms/sdk/config';
|
|
1258
|
+
|
|
1259
|
+
// Configuration for extending a single block
|
|
1260
|
+
type BlockFieldExtension = {
|
|
1261
|
+
fields: FieldDefinition[]; // Same field format as block manifests
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// Map of block IDs to their extensions
|
|
1265
|
+
type BlockFieldExtensionsMap = Record<string, BlockFieldExtension>;
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
### Validation Rules
|
|
1269
|
+
|
|
1270
|
+
- **Block IDs must be system blocks**: Only `block.*` format (e.g., `block.body-text`, `block.hero`). Custom blocks (`custom.*`) should define their fields directly in `customBlocks`.
|
|
1271
|
+
- **Field IDs must be unique**: Extended field IDs cannot conflict with existing fields in the block. The CLI will error if you try to add a field with an ID that already exists.
|
|
1272
|
+
- **Required fields need defaultValue**: If `required: true`, you must provide a `defaultValue`. This ensures existing blocks (created before the extension) can still be edited.
|
|
1273
|
+
- **At least one field required**: Each block extension must have at least one field.
|
|
1274
|
+
|
|
1275
|
+
### Use Cases
|
|
1276
|
+
|
|
1277
|
+
**Layout variations for text blocks:**
|
|
1278
|
+
```typescript
|
|
1279
|
+
'block.body-text': {
|
|
1280
|
+
fields: [
|
|
1281
|
+
{
|
|
1282
|
+
id: 'layout',
|
|
1283
|
+
type: 'select',
|
|
1284
|
+
label: 'Layout',
|
|
1285
|
+
defaultValue: 'default',
|
|
1286
|
+
required: false,
|
|
1287
|
+
multiple: false,
|
|
1288
|
+
options: [
|
|
1289
|
+
{ value: 'default', label: 'Default' },
|
|
1290
|
+
{ value: 'wide', label: 'Wide' },
|
|
1291
|
+
{ value: 'sidebar', label: 'With Sidebar' },
|
|
1292
|
+
],
|
|
1293
|
+
},
|
|
1294
|
+
],
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
**Custom styling options:**
|
|
1299
|
+
```typescript
|
|
1300
|
+
'block.hero': {
|
|
1301
|
+
fields: [
|
|
1302
|
+
{
|
|
1303
|
+
id: 'textAlignment',
|
|
1304
|
+
type: 'select',
|
|
1305
|
+
label: 'Text Alignment',
|
|
1306
|
+
defaultValue: 'center',
|
|
1307
|
+
required: false,
|
|
1308
|
+
multiple: false,
|
|
1309
|
+
options: [
|
|
1310
|
+
{ value: 'left', label: 'Left' },
|
|
1311
|
+
{ value: 'center', label: 'Center' },
|
|
1312
|
+
{ value: 'right', label: 'Right' },
|
|
1313
|
+
],
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
id: 'showBreadcrumbs',
|
|
1317
|
+
type: 'boolean',
|
|
1318
|
+
label: 'Show Breadcrumbs',
|
|
1319
|
+
defaultValue: false,
|
|
1320
|
+
required: false,
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
**Adding metadata fields:**
|
|
1327
|
+
```typescript
|
|
1328
|
+
'block.blog-listing': {
|
|
1329
|
+
fields: [
|
|
1330
|
+
{
|
|
1331
|
+
id: 'analyticsId',
|
|
1332
|
+
type: 'text',
|
|
1333
|
+
label: 'Analytics ID',
|
|
1334
|
+
description: 'Track this specific listing in analytics',
|
|
1335
|
+
required: false,
|
|
1336
|
+
multiline: false,
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
### Comparison: blockFieldOptions vs blockFieldExtensions
|
|
1343
|
+
|
|
1344
|
+
| Feature | `blockFieldOptions` | `blockFieldExtensions` |
|
|
1345
|
+
|---------|---------------------|------------------------|
|
|
1346
|
+
| Purpose | Override options for existing select fields | Add new fields to blocks |
|
|
1347
|
+
| Supports | Select field options only | All field types |
|
|
1348
|
+
| Use case | Customize dropdown choices | Add new data fields |
|
|
1349
|
+
| Block types | System and custom blocks | System blocks only |
|
|
1350
|
+
|
|
1351
|
+
Use **both together** for maximum customization:
|
|
1352
|
+
|
|
1353
|
+
```typescript
|
|
1354
|
+
export default defineConfig({
|
|
1355
|
+
siteId: 'your-site-id',
|
|
1356
|
+
|
|
1357
|
+
// Override options for existing fields
|
|
1358
|
+
blockFieldOptions: {
|
|
1359
|
+
'block.embed': {
|
|
1360
|
+
layout: {
|
|
1361
|
+
options: [
|
|
1362
|
+
{ value: 'blog-grid', label: 'Blog Grid' },
|
|
1363
|
+
{ value: 'team-cards', label: 'Team Cards' },
|
|
1364
|
+
],
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
},
|
|
1368
|
+
|
|
1369
|
+
// Add new fields to blocks
|
|
1370
|
+
blockFieldExtensions: {
|
|
1371
|
+
'block.body-text': {
|
|
1372
|
+
fields: [
|
|
1373
|
+
{
|
|
1374
|
+
id: 'layout',
|
|
1375
|
+
type: 'select',
|
|
1376
|
+
label: 'Layout',
|
|
1377
|
+
defaultValue: 'default',
|
|
1378
|
+
required: false,
|
|
1379
|
+
multiple: false,
|
|
1380
|
+
options: [
|
|
1381
|
+
{ value: 'default', label: 'Default' },
|
|
1382
|
+
{ value: 'wide', label: 'Wide' },
|
|
1383
|
+
],
|
|
1384
|
+
},
|
|
1385
|
+
],
|
|
1386
|
+
},
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
## Custom Blocks
|
|
1392
|
+
|
|
1393
|
+
Define site-specific blocks directly in your SDK configuration. Custom blocks appear in the CMS block picker alongside system blocks, are edited using CMS-generated forms, and are rendered by your own React components.
|
|
1394
|
+
|
|
1395
|
+
### Quick Start
|
|
1396
|
+
|
|
1397
|
+
1. **Define the block schema** in your `riverbank.config.ts`:
|
|
1398
|
+
|
|
1399
|
+
```typescript
|
|
1400
|
+
import { defineConfig } from '@riverbankcms/sdk/config';
|
|
1401
|
+
|
|
1402
|
+
export default defineConfig({
|
|
1403
|
+
siteId: 'your-site-id',
|
|
1404
|
+
customBlocks: [
|
|
1405
|
+
{
|
|
1406
|
+
id: 'custom.team-member',
|
|
1407
|
+
title: 'Team Member',
|
|
1408
|
+
titleSource: 'name',
|
|
1409
|
+
description: 'Display a team member with photo and bio',
|
|
1410
|
+
category: 'content',
|
|
1411
|
+
icon: 'User',
|
|
1412
|
+
tags: ['team', 'about'],
|
|
1413
|
+
fields: [
|
|
1414
|
+
{ id: 'name', type: 'text', label: 'Name', required: true, multiline: false },
|
|
1415
|
+
{ id: 'role', type: 'text', label: 'Role', required: false, multiline: false },
|
|
1416
|
+
{ id: 'photo', type: 'media', label: 'Photo', required: false, mediaKinds: ['image'] },
|
|
1417
|
+
{ id: 'bio', type: 'richText', label: 'Bio', required: false },
|
|
1418
|
+
],
|
|
1419
|
+
},
|
|
1420
|
+
],
|
|
1421
|
+
});
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
2. **Create your component** to render the block:
|
|
1425
|
+
|
|
1426
|
+
```tsx
|
|
1427
|
+
// components/blocks/TeamMember.tsx
|
|
1428
|
+
import type { SystemBlockComponentProps } from '@riverbankcms/blocks';
|
|
1429
|
+
|
|
1430
|
+
type TeamMemberContent = {
|
|
1431
|
+
name: string;
|
|
1432
|
+
role?: string;
|
|
1433
|
+
photo?: { url: string; alt?: string };
|
|
1434
|
+
bio?: string;
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
export function TeamMember({ content }: SystemBlockComponentProps<TeamMemberContent>) {
|
|
1438
|
+
return (
|
|
1439
|
+
<div className="flex items-start gap-6 p-6">
|
|
1440
|
+
{content.photo && (
|
|
1441
|
+
<img
|
|
1442
|
+
src={content.photo.url}
|
|
1443
|
+
alt={content.photo.alt || content.name}
|
|
1444
|
+
className="h-24 w-24 rounded-full object-cover"
|
|
1445
|
+
/>
|
|
1446
|
+
)}
|
|
1447
|
+
<div>
|
|
1448
|
+
<h3 className="text-xl font-bold">{content.name}</h3>
|
|
1449
|
+
{content.role && <p className="text-gray-600">{content.role}</p>}
|
|
1450
|
+
{content.bio && (
|
|
1451
|
+
<div className="mt-2 prose" dangerouslySetInnerHTML={{ __html: content.bio }} />
|
|
1452
|
+
)}
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
3. **Register the component** with `blockOverrides`:
|
|
1460
|
+
|
|
1461
|
+
```tsx
|
|
1462
|
+
// app/[...slug]/page.tsx
|
|
1463
|
+
import { loadPage, Page } from '@riverbankcms/sdk';
|
|
1464
|
+
import { TeamMember } from '@/components/blocks/TeamMember';
|
|
1465
|
+
|
|
1466
|
+
export default async function CMSPage({ params }) {
|
|
1467
|
+
const pageData = await loadPage({
|
|
1468
|
+
client,
|
|
1469
|
+
siteId: process.env.SITE_ID!,
|
|
1470
|
+
path: `/${params.slug?.join('/') || ''}`,
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
return (
|
|
1474
|
+
<Page
|
|
1475
|
+
{...pageData}
|
|
1476
|
+
blockOverrides={{
|
|
1477
|
+
'custom.team-member': TeamMember,
|
|
1478
|
+
}}
|
|
1479
|
+
/>
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
### Block Definition Schema
|
|
1485
|
+
|
|
1486
|
+
| Property | Type | Required | Description |
|
|
1487
|
+
|----------|------|----------|-------------|
|
|
1488
|
+
| `id` | `custom.${string}` | Yes | Unique block ID, must start with `custom.` |
|
|
1489
|
+
| `title` | `string` | Yes | Display name in block picker |
|
|
1490
|
+
| `titleSource` | `string` | No | Field ID to use as block title in lists |
|
|
1491
|
+
| `description` | `string` | No | Description shown in block picker |
|
|
1492
|
+
| `category` | `BlockCategory` | Yes | One of: `marketing`, `content`, `blog`, `media`, `layout`, `interactive` |
|
|
1493
|
+
| `icon` | `string` | No | Lucide icon name (e.g., `User`, `CreditCard`) |
|
|
1494
|
+
| `tags` | `string[]` | No | Search tags for discoverability |
|
|
1495
|
+
| `fields` | `FieldDefinition[]` | Yes | Field definitions for the block |
|
|
1496
|
+
|
|
1497
|
+
### Supported Field Types
|
|
1498
|
+
|
|
1499
|
+
All field types from the CMS are supported:
|
|
1500
|
+
|
|
1501
|
+
**Basic Types:**
|
|
1502
|
+
- `text` - Single or multiline text
|
|
1503
|
+
- `richText` - Rich text editor with formatting
|
|
1504
|
+
- `number` - Numeric input
|
|
1505
|
+
- `boolean` - Toggle/checkbox
|
|
1506
|
+
|
|
1507
|
+
**Media & Links:**
|
|
1508
|
+
- `media` - Image, video, or file upload
|
|
1509
|
+
- `url` - URL input with validation
|
|
1510
|
+
- `link` - Internal or external link picker
|
|
1511
|
+
|
|
1512
|
+
**Selection:**
|
|
1513
|
+
- `select` - Dropdown selection
|
|
1514
|
+
- `reference` - Reference to other content entries
|
|
1515
|
+
|
|
1516
|
+
**Date/Time:**
|
|
1517
|
+
- `date` - Date picker
|
|
1518
|
+
- `time` - Time picker
|
|
1519
|
+
- `datetime` - Combined date and time
|
|
1520
|
+
|
|
1521
|
+
**Complex Types:**
|
|
1522
|
+
- `group` - Group related fields together
|
|
1523
|
+
- `repeater` - Repeatable list of items
|
|
1524
|
+
- `modal` - Fields in a modal dialog
|
|
1525
|
+
- `tabGroup` - Tabbed field groups
|
|
1526
|
+
|
|
1527
|
+
### Examples
|
|
1528
|
+
|
|
1529
|
+
**Pricing Card:**
|
|
1530
|
+
|
|
1531
|
+
```typescript
|
|
1532
|
+
{
|
|
1533
|
+
id: 'custom.pricing-card',
|
|
1534
|
+
title: 'Pricing Card',
|
|
1535
|
+
titleSource: 'planName',
|
|
1536
|
+
category: 'marketing',
|
|
1537
|
+
icon: 'CreditCard',
|
|
1538
|
+
fields: [
|
|
1539
|
+
{ id: 'planName', type: 'text', label: 'Plan Name', required: true, multiline: false },
|
|
1540
|
+
{ id: 'price', type: 'text', label: 'Price', required: false, multiline: false, description: 'e.g., "$29/mo"' },
|
|
1541
|
+
{ id: 'featured', type: 'boolean', label: 'Featured Plan', required: false },
|
|
1542
|
+
{
|
|
1543
|
+
id: 'features',
|
|
1544
|
+
type: 'repeater',
|
|
1545
|
+
label: 'Features',
|
|
1546
|
+
required: false,
|
|
1547
|
+
itemLabel: 'Feature',
|
|
1548
|
+
maxItems: 10,
|
|
1549
|
+
fields: [
|
|
1550
|
+
{ id: 'text', type: 'text', label: 'Feature', required: true, multiline: false },
|
|
1551
|
+
{ id: 'included', type: 'boolean', label: 'Included', required: false },
|
|
1552
|
+
],
|
|
1553
|
+
},
|
|
1554
|
+
{ id: 'ctaLabel', type: 'text', label: 'Button Label', required: false, multiline: false },
|
|
1555
|
+
{ id: 'ctaLink', type: 'link', label: 'Button Link', required: false },
|
|
1556
|
+
],
|
|
1557
|
+
}
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
**Testimonial:**
|
|
1561
|
+
|
|
1562
|
+
```typescript
|
|
1563
|
+
{
|
|
1564
|
+
id: 'custom.testimonial',
|
|
1565
|
+
title: 'Testimonial',
|
|
1566
|
+
titleSource: 'authorName',
|
|
1567
|
+
category: 'content',
|
|
1568
|
+
icon: 'Quote',
|
|
1569
|
+
fields: [
|
|
1570
|
+
{ id: 'quote', type: 'richText', label: 'Quote', required: true },
|
|
1571
|
+
{ id: 'authorName', type: 'text', label: 'Author Name', required: true, multiline: false },
|
|
1572
|
+
{ id: 'authorTitle', type: 'text', label: 'Author Title', required: false, multiline: false },
|
|
1573
|
+
{ id: 'authorPhoto', type: 'media', label: 'Author Photo', required: false, mediaKinds: ['image'] },
|
|
1574
|
+
{ id: 'companyLogo', type: 'media', label: 'Company Logo', required: false, mediaKinds: ['image'] },
|
|
1575
|
+
{ id: 'rating', type: 'number', label: 'Rating (1-5)', required: false },
|
|
1576
|
+
],
|
|
1577
|
+
}
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
### Validation Rules
|
|
1581
|
+
|
|
1582
|
+
- Maximum 20 custom blocks per site
|
|
1583
|
+
- Block IDs must be unique within the site
|
|
1584
|
+
- Block IDs must start with `custom.` followed by lowercase letters, numbers, or hyphens
|
|
1585
|
+
- `titleSource` must reference a valid field ID if provided
|
|
1586
|
+
- At least one field is required per block
|
|
1587
|
+
- Maximum 5 data loaders per block
|
|
1588
|
+
|
|
1589
|
+
### Best Practices
|
|
1590
|
+
|
|
1591
|
+
1. **Use descriptive IDs**: `custom.team-member` is better than `custom.tm`
|
|
1592
|
+
2. **Set `titleSource`**: Helps editors identify blocks in the page structure
|
|
1593
|
+
3. **Add descriptions**: Help editors understand when to use each block
|
|
1594
|
+
4. **Group related fields**: Use `group` for related fields that should appear together
|
|
1595
|
+
5. **Validate required fields**: Mark essential fields as `required: true`
|
|
1596
|
+
6. **Provide defaults**: Use `defaultValue` for optional fields with sensible defaults
|
|
1597
|
+
|
|
1598
|
+
## Custom Block Data Loaders
|
|
1599
|
+
|
|
1600
|
+
Custom blocks can fetch data automatically using data loaders. There are two approaches:
|
|
1601
|
+
|
|
1602
|
+
1. **Config-based loaders** - Declarative loaders defined in `riverbank.config.ts` for whitelisted CMS endpoints
|
|
1603
|
+
2. **Code-based loaders** - Functions passed to `loadPage()` for external APIs
|
|
1604
|
+
|
|
1605
|
+
Both loader types run server-side during `loadPage()`. Data is passed to your component via the `data` prop.
|
|
1606
|
+
|
|
1607
|
+
### Config-Based Loaders (CMS Endpoints)
|
|
1608
|
+
|
|
1609
|
+
Define loaders declaratively in your block definition. These are restricted to whitelisted CMS endpoints for security.
|
|
1610
|
+
|
|
1611
|
+
**Supported Endpoints:**
|
|
1612
|
+
|
|
1613
|
+
| Endpoint | Description |
|
|
1614
|
+
|----------|-------------|
|
|
1615
|
+
| `listPublishedEntries` | Fetch published content entries |
|
|
1616
|
+
| `getPublishedEntryPreview` | Fetch a single entry by slug |
|
|
1617
|
+
| `listPublicEvents` | Fetch events |
|
|
1618
|
+
| `getPublicFormById` | Fetch form configuration |
|
|
1619
|
+
| `getPublicBookingServices` | Fetch booking services |
|
|
1620
|
+
|
|
1621
|
+
**Parameter Bindings:**
|
|
1622
|
+
|
|
1623
|
+
Loader params can be static values or dynamic bindings:
|
|
1624
|
+
|
|
1625
|
+
- `{ $bind: { from: 'content.fieldName' } }` - Bind to a block field value
|
|
1626
|
+
- `{ $bind: { from: 'content.fieldName', fallback: '10' } }` - With fallback
|
|
1627
|
+
- `{ $bind: { from: '$root.siteId' } }` - Bind to site ID from page context
|
|
1628
|
+
- `{ $bind: { from: '$root.pageId' } }` - Bind to page ID
|
|
1629
|
+
- `{ $bind: { from: '$root.previewStage' } }` - 'published' or 'preview'
|
|
1630
|
+
|
|
1631
|
+
**Example: Blog Listing Block**
|
|
1632
|
+
|
|
1633
|
+
```typescript
|
|
1634
|
+
// riverbank.config.ts
|
|
1635
|
+
import { defineConfig } from '@riverbankcms/sdk/config';
|
|
1636
|
+
|
|
1637
|
+
export default defineConfig({
|
|
1638
|
+
siteId: 'your-site-id',
|
|
1639
|
+
customBlocks: [
|
|
1640
|
+
{
|
|
1641
|
+
id: 'custom.featured-posts',
|
|
1642
|
+
title: 'Featured Posts',
|
|
1643
|
+
category: 'blog',
|
|
1644
|
+
icon: 'FileText',
|
|
1645
|
+
fields: [
|
|
1646
|
+
{ id: 'limit', type: 'number', label: 'Number of posts', required: false },
|
|
1647
|
+
{ id: 'category', type: 'text', label: 'Category filter', required: false, multiline: false },
|
|
1648
|
+
],
|
|
1649
|
+
dataLoaders: {
|
|
1650
|
+
posts: {
|
|
1651
|
+
endpoint: 'listPublishedEntries',
|
|
1652
|
+
params: {
|
|
1653
|
+
siteId: { $bind: { from: '$root.siteId' } },
|
|
1654
|
+
type: 'blog-post',
|
|
1655
|
+
limit: { $bind: { from: 'content.limit', fallback: '10' } },
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
],
|
|
1661
|
+
});
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
```tsx
|
|
1665
|
+
// components/blocks/FeaturedPosts.tsx
|
|
1666
|
+
import type { SystemBlockComponentProps } from '@riverbankcms/blocks';
|
|
1667
|
+
|
|
1668
|
+
type FeaturedPostsContent = {
|
|
1669
|
+
limit?: number;
|
|
1670
|
+
category?: string;
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
type FeaturedPostsData = {
|
|
1674
|
+
posts: { entries: Array<{ id: string; title: string; slug: string }> };
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
export function FeaturedPosts({
|
|
1678
|
+
content,
|
|
1679
|
+
data,
|
|
1680
|
+
}: SystemBlockComponentProps<FeaturedPostsContent, FeaturedPostsData>) {
|
|
1681
|
+
const posts = data?.posts?.entries ?? [];
|
|
1682
|
+
|
|
1683
|
+
return (
|
|
1684
|
+
<div className="grid gap-4">
|
|
1685
|
+
{posts.map((post) => (
|
|
1686
|
+
<article key={post.id}>
|
|
1687
|
+
<a href={`/blog/${post.slug}`}>
|
|
1688
|
+
<h3>{post.title}</h3>
|
|
1689
|
+
</a>
|
|
1690
|
+
</article>
|
|
1691
|
+
))}
|
|
1692
|
+
</div>
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
### Code-Based Loaders (External APIs)
|
|
1698
|
+
|
|
1699
|
+
For data from external APIs, pass loader functions to `loadPage()`. This gives you full control over the fetch logic.
|
|
1700
|
+
|
|
1701
|
+
**Example: E-commerce Product Block**
|
|
1702
|
+
|
|
1703
|
+
```typescript
|
|
1704
|
+
// app/[...slug]/page.tsx
|
|
1705
|
+
import { loadPage, Page } from '@riverbankcms/sdk';
|
|
1706
|
+
import { FeaturedProducts } from '@/components/blocks/FeaturedProducts';
|
|
1707
|
+
|
|
1708
|
+
export default async function CMSPage({ params }) {
|
|
1709
|
+
const pageData = await loadPage({
|
|
1710
|
+
client,
|
|
1711
|
+
siteId: process.env.SITE_ID!,
|
|
1712
|
+
path: `/${params.slug?.join('/') || ''}`,
|
|
1713
|
+
dataLoaderOverrides: {
|
|
1714
|
+
'custom.featured-products': {
|
|
1715
|
+
products: async (ctx) => {
|
|
1716
|
+
// Access block content for parameters
|
|
1717
|
+
const categoryId = ctx.content.categoryId as string;
|
|
1718
|
+
const limit = (ctx.content.limit as number) ?? 10;
|
|
1719
|
+
|
|
1720
|
+
const res = await fetch(
|
|
1721
|
+
`https://api.shop.com/products?category=${categoryId}&limit=${limit}`,
|
|
1722
|
+
{ headers: { 'Authorization': `Bearer ${process.env.SHOP_API_KEY}` } }
|
|
1723
|
+
);
|
|
1724
|
+
|
|
1725
|
+
return res.json();
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
},
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
return (
|
|
1732
|
+
<Page
|
|
1733
|
+
{...pageData}
|
|
1734
|
+
blockOverrides={{
|
|
1735
|
+
'custom.featured-products': FeaturedProducts,
|
|
1736
|
+
}}
|
|
1737
|
+
/>
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
```tsx
|
|
1743
|
+
// components/blocks/FeaturedProducts.tsx
|
|
1744
|
+
import type { SystemBlockComponentProps } from '@riverbankcms/blocks';
|
|
1745
|
+
|
|
1746
|
+
type ProductsContent = {
|
|
1747
|
+
categoryId: string;
|
|
1748
|
+
limit?: number;
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
type ProductsData = {
|
|
1752
|
+
products: Array<{ id: string; name: string; price: number; image: string }>;
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
export function FeaturedProducts({
|
|
1756
|
+
content,
|
|
1757
|
+
data,
|
|
1758
|
+
}: SystemBlockComponentProps<ProductsContent, ProductsData>) {
|
|
1759
|
+
const products = data?.products ?? [];
|
|
1760
|
+
|
|
1761
|
+
return (
|
|
1762
|
+
<div className="grid grid-cols-3 gap-6">
|
|
1763
|
+
{products.map((product) => (
|
|
1764
|
+
<div key={product.id} className="border rounded p-4">
|
|
1765
|
+
<img src={product.image} alt={product.name} />
|
|
1766
|
+
<h3>{product.name}</h3>
|
|
1767
|
+
<p>${product.price}</p>
|
|
1768
|
+
</div>
|
|
1769
|
+
))}
|
|
1770
|
+
</div>
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
### DataLoaderContext
|
|
1776
|
+
|
|
1777
|
+
Code-based loaders receive context about the block and page:
|
|
1778
|
+
|
|
1779
|
+
```typescript
|
|
1780
|
+
interface DataLoaderContext {
|
|
1781
|
+
/** Site ID from page context */
|
|
1782
|
+
siteId: string;
|
|
1783
|
+
|
|
1784
|
+
/** Page ID from page context */
|
|
1785
|
+
pageId: string;
|
|
1786
|
+
|
|
1787
|
+
/** Unique block instance ID */
|
|
1788
|
+
blockId: string;
|
|
1789
|
+
|
|
1790
|
+
/** Block kind (e.g., 'custom.featured-products') */
|
|
1791
|
+
blockKind: string;
|
|
1792
|
+
|
|
1793
|
+
/** The block's CMS content (field values) */
|
|
1794
|
+
content: Record<string, unknown>;
|
|
1795
|
+
|
|
1796
|
+
/** Whether fetching preview/draft content */
|
|
1797
|
+
previewStage: 'published' | 'preview';
|
|
1798
|
+
}
|
|
1799
|
+
```
|
|
1800
|
+
|
|
1801
|
+
### Combining Config and Code Loaders
|
|
1802
|
+
|
|
1803
|
+
You can use both loader types together. Config loaders run first, then code loaders. On key conflicts, code loaders take precedence.
|
|
1804
|
+
|
|
1805
|
+
```typescript
|
|
1806
|
+
// riverbank.config.ts - Config loader for CMS data
|
|
1807
|
+
customBlocks: [{
|
|
1808
|
+
id: 'custom.product-showcase',
|
|
1809
|
+
dataLoaders: {
|
|
1810
|
+
relatedPosts: {
|
|
1811
|
+
endpoint: 'listPublishedEntries',
|
|
1812
|
+
params: {
|
|
1813
|
+
siteId: { $bind: { from: '$root.siteId' } },
|
|
1814
|
+
type: 'blog-post',
|
|
1815
|
+
limit: '3',
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
},
|
|
1819
|
+
}]
|
|
1820
|
+
|
|
1821
|
+
// page.tsx - Code loader for external API
|
|
1822
|
+
const pageData = await loadPage({
|
|
1823
|
+
client,
|
|
1824
|
+
siteId,
|
|
1825
|
+
path: '/',
|
|
1826
|
+
dataLoaderOverrides: {
|
|
1827
|
+
'custom.product-showcase': {
|
|
1828
|
+
products: async (ctx) => fetchProductsFromShopify(ctx.content.collectionId),
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
// Component receives both: data.relatedPosts (CMS) and data.products (Shopify)
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
### Error Handling
|
|
1837
|
+
|
|
1838
|
+
Data loaders are best-effort. If a loader fails:
|
|
1839
|
+
|
|
1840
|
+
- The error is logged to console
|
|
1841
|
+
- The loader key is undefined in the `data` prop
|
|
1842
|
+
- Other loaders continue executing
|
|
1843
|
+
- Your component should handle missing data gracefully
|
|
1844
|
+
|
|
1845
|
+
```tsx
|
|
1846
|
+
function MyBlock({ data }: SystemBlockComponentProps<Content, Data>) {
|
|
1847
|
+
// Handle missing data
|
|
1848
|
+
const products = data?.products ?? [];
|
|
1849
|
+
|
|
1850
|
+
if (products.length === 0) {
|
|
1851
|
+
return <p>No products available</p>;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return <ProductGrid products={products} />;
|
|
1855
|
+
}
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1858
|
+
### Data Exports
|
|
1859
|
+
|
|
1860
|
+
Import data utilities from `@riverbankcms/sdk/data`:
|
|
1861
|
+
|
|
1862
|
+
```typescript
|
|
1863
|
+
import {
|
|
1864
|
+
// Core prefetch function
|
|
1865
|
+
prefetchBlockData,
|
|
1866
|
+
|
|
1867
|
+
// Code loader execution
|
|
1868
|
+
executeCodeLoaders,
|
|
1869
|
+
mergeLoaderResults,
|
|
1870
|
+
|
|
1871
|
+
// Types
|
|
1872
|
+
type DataLoaderContext,
|
|
1873
|
+
type DataLoaderFn,
|
|
1874
|
+
type BlockLoaderMap,
|
|
1875
|
+
type DataLoaderOverrides,
|
|
1876
|
+
type PrefetchContext,
|
|
1877
|
+
type ResolvedBlockData,
|
|
1878
|
+
type SdkPrefetchOptions,
|
|
1879
|
+
} from '@riverbankcms/sdk/data';
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
## Additional Exports
|
|
1883
|
+
|
|
1884
|
+
- `@riverbankcms/sdk/rendering` - Low-level rendering components
|
|
1885
|
+
- `@riverbankcms/sdk/theme` - Theme utilities
|
|
1886
|
+
- `@riverbankcms/sdk/theme-bridge` - Simplified theming for SDK sites
|
|
1887
|
+
- `@riverbankcms/sdk/hooks` - (Deprecated: use `/client` instead)
|
|
1888
|
+
- `@riverbankcms/sdk/data` - Data prefetching utilities
|
|
1889
|
+
- `@riverbankcms/sdk/metadata` - Metadata generation helpers
|
|
1890
|
+
- `@riverbankcms/sdk/routing` - Route resolution helpers
|
|
1891
|
+
- `@riverbankcms/sdk/analytics` - Analytics tracking helpers
|
|
1892
|
+
- `@riverbankcms/sdk/config` - Site configuration utilities (includes `defineConfig`, `RiverbankSiteConfig`, `BlockFieldOptionsMap`, `FieldSelectOption`, etc.)
|