@riverbankcms/sdk 0.4.3 → 0.5.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.
Files changed (176) hide show
  1. package/README.md +84 -0
  2. package/dist/cli/index.js +3104 -120
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/client/analytics.js +1 -1
  5. package/dist/client/analytics.js.map +1 -1
  6. package/dist/client/analytics.mjs +1 -1
  7. package/dist/client/analytics.mjs.map +1 -1
  8. package/dist/client/bookings.js +6 -6
  9. package/dist/client/bookings.js.map +1 -1
  10. package/dist/client/bookings.mjs +6 -6
  11. package/dist/client/bookings.mjs.map +1 -1
  12. package/dist/client/client.d.mts +2 -2
  13. package/dist/client/client.d.ts +2 -2
  14. package/dist/client/client.js +1357 -519
  15. package/dist/client/client.js.map +1 -1
  16. package/dist/client/client.mjs +1357 -519
  17. package/dist/client/client.mjs.map +1 -1
  18. package/dist/client/hooks.d.mts +2 -2
  19. package/dist/client/hooks.d.ts +2 -2
  20. package/dist/client/hooks.js +26 -11
  21. package/dist/client/hooks.js.map +1 -1
  22. package/dist/client/hooks.mjs +26 -11
  23. package/dist/client/hooks.mjs.map +1 -1
  24. package/dist/client/rendering/client.js +20 -14
  25. package/dist/client/rendering/client.js.map +1 -1
  26. package/dist/client/rendering/client.mjs +20 -14
  27. package/dist/client/rendering/client.mjs.map +1 -1
  28. package/dist/client/usePage-BTPnCuWC.d.mts +6511 -0
  29. package/dist/client/usePage-BafOS9UT.d.mts +6512 -0
  30. package/dist/client/usePage-Bnx-kA6x.d.mts +6670 -0
  31. package/dist/client/usePage-DoPI6b8V.d.ts +6511 -0
  32. package/dist/client/usePage-QNWArrVO.d.ts +6670 -0
  33. package/dist/client/usePage-fBgPB6Oq.d.ts +6512 -0
  34. package/dist/server/{Layout-CXI_VkhN.d.ts → Layout-B-q2Py4v.d.ts} +4 -4
  35. package/dist/server/{Layout-p6f3TLw9.d.mts → Layout-Cc5HUXAH.d.mts} +4 -4
  36. package/dist/server/{chunk-6JBKKV3G.js → chunk-2KCF2DNK.js} +30 -10
  37. package/dist/server/chunk-2KCF2DNK.js.map +1 -0
  38. package/dist/server/{chunk-N3PX76AP.mjs → chunk-4HIRA33Z.mjs} +247 -135
  39. package/dist/server/chunk-4HIRA33Z.mjs.map +1 -0
  40. package/dist/server/chunk-5STV4MWD.js +189 -0
  41. package/dist/server/chunk-5STV4MWD.js.map +1 -0
  42. package/dist/server/{chunk-R5B6IOFQ.js → chunk-6OSNCH4F.js} +247 -135
  43. package/dist/server/chunk-6OSNCH4F.js.map +1 -0
  44. package/dist/server/{chunk-VHDDXCK6.js → chunk-7UPVCT3K.js} +1206 -496
  45. package/dist/server/chunk-7UPVCT3K.js.map +1 -0
  46. package/dist/server/{chunk-7DS4Q3GA.mjs → chunk-AEFWG657.mjs} +3 -3
  47. package/dist/server/chunk-AEFWG657.mjs.map +1 -0
  48. package/dist/server/{chunk-USQF2XTU.mjs → chunk-BYBJA6SP.mjs} +26 -11
  49. package/dist/server/chunk-BYBJA6SP.mjs.map +1 -0
  50. package/dist/server/{chunk-ES6QDZUX.mjs → chunk-C6FIJC7T.mjs} +2 -2
  51. package/dist/server/{chunk-TO7FD6TQ.js → chunk-I2D7KOEA.js} +4 -4
  52. package/dist/server/{chunk-TO7FD6TQ.js.map → chunk-I2D7KOEA.js.map} +1 -1
  53. package/dist/server/chunk-KFLZGNPO.mjs +189 -0
  54. package/dist/server/chunk-KFLZGNPO.mjs.map +1 -0
  55. package/dist/server/chunk-L5EA4FXU.mjs +134 -0
  56. package/dist/server/chunk-L5EA4FXU.mjs.map +1 -0
  57. package/dist/server/{chunk-U2NI3TS3.mjs → chunk-LNOUXALA.mjs} +1135 -425
  58. package/dist/server/chunk-LNOUXALA.mjs.map +1 -0
  59. package/dist/server/{chunk-24F6FTCI.mjs → chunk-OSF34JTQ.mjs} +4 -4
  60. package/dist/server/{chunk-G35R7N7B.js → chunk-P3NNN73G.js} +3 -3
  61. package/dist/server/{chunk-G35R7N7B.js.map → chunk-P3NNN73G.js.map} +1 -1
  62. package/dist/server/{chunk-I6K5REFT.mjs → chunk-P4K63SBZ.mjs} +24 -4
  63. package/dist/server/chunk-P4K63SBZ.mjs.map +1 -0
  64. package/dist/server/{chunk-HOY77YBF.js → chunk-RVDS7VSP.js} +5 -5
  65. package/dist/server/chunk-RVDS7VSP.js.map +1 -0
  66. package/dist/server/{chunk-2SSEBAHC.js → chunk-TT5JWA4X.js} +9 -9
  67. package/dist/server/{chunk-2SSEBAHC.js.map → chunk-TT5JWA4X.js.map} +1 -1
  68. package/dist/server/chunk-VSFQRHYZ.js +134 -0
  69. package/dist/server/chunk-VSFQRHYZ.js.map +1 -0
  70. package/dist/server/{chunk-EGTDJ4PL.js → chunk-YYO3RIFO.js} +26 -11
  71. package/dist/server/chunk-YYO3RIFO.js.map +1 -0
  72. package/dist/server/{chunk-OP2GHK27.mjs → chunk-Z5ZA6Q4D.mjs} +2 -2
  73. package/dist/server/{components-Dhiemsjd.d.ts → components-CU46ZkAv.d.mts} +20 -75
  74. package/dist/server/{components-C75e4poV.d.mts → components-DvozDwRN.d.ts} +20 -75
  75. package/dist/server/components.d.mts +11 -8
  76. package/dist/server/components.d.ts +11 -8
  77. package/dist/server/components.js +5 -4
  78. package/dist/server/components.js.map +1 -1
  79. package/dist/server/components.mjs +4 -3
  80. package/dist/server/config-validation.d.mts +3 -3
  81. package/dist/server/config-validation.d.ts +3 -3
  82. package/dist/server/config-validation.js +9 -5
  83. package/dist/server/config-validation.js.map +1 -1
  84. package/dist/server/config-validation.mjs +8 -4
  85. package/dist/server/config.d.mts +243 -5
  86. package/dist/server/config.d.ts +243 -5
  87. package/dist/server/config.js +72 -5
  88. package/dist/server/config.js.map +1 -1
  89. package/dist/server/config.mjs +72 -5
  90. package/dist/server/config.mjs.map +1 -1
  91. package/dist/server/core-DsNWrl3o.d.mts +44 -0
  92. package/dist/server/core-DsNWrl3o.d.ts +44 -0
  93. package/dist/server/data.d.mts +4 -3
  94. package/dist/server/data.d.ts +4 -3
  95. package/dist/server/data.js +3 -3
  96. package/dist/server/data.mjs +2 -2
  97. package/dist/server/{index-C6o9LPvq.d.mts → index-CJfMXZQr.d.ts} +2 -1
  98. package/dist/server/{index-CAwBj3-A.d.ts → index-Q7RLMAQ6.d.mts} +2 -1
  99. package/dist/server/index.d.mts +63 -6
  100. package/dist/server/index.d.ts +63 -6
  101. package/dist/server/index.js +91 -2
  102. package/dist/server/index.js.map +1 -1
  103. package/dist/server/index.mjs +90 -1
  104. package/dist/server/index.mjs.map +1 -1
  105. package/dist/server/link-DjxLyC82.d.mts +23 -0
  106. package/dist/server/link-DjxLyC82.d.ts +23 -0
  107. package/dist/server/{loadContent-CdXfuCuE.d.mts → loadContent-DgpSKWqY.d.mts} +4 -4
  108. package/dist/server/{loadContent-CsvQRoxb.d.ts → loadContent-GPvUI1bN.d.ts} +4 -4
  109. package/dist/server/{loadPage-p3AWwwrd.d.mts → loadPage-DGnIK7s4.d.mts} +5 -46
  110. package/dist/server/loadPage-DNQTTRHL.mjs +11 -0
  111. package/dist/server/{loadPage-BA0HiT-6.d.ts → loadPage-DW9WB-u9.d.ts} +5 -46
  112. package/dist/server/loadPage-IDGVDFBB.js +11 -0
  113. package/dist/server/{loadPage-DLC7DJZP.js.map → loadPage-IDGVDFBB.js.map} +1 -1
  114. package/dist/server/metadata.d.mts +6 -4
  115. package/dist/server/metadata.d.ts +6 -4
  116. package/dist/server/navigation.d.mts +199 -29
  117. package/dist/server/navigation.d.ts +199 -29
  118. package/dist/server/navigation.js +27 -43
  119. package/dist/server/navigation.js.map +1 -1
  120. package/dist/server/navigation.mjs +20 -36
  121. package/dist/server/navigation.mjs.map +1 -1
  122. package/dist/server/rendering/server.d.mts +8 -6
  123. package/dist/server/rendering/server.d.ts +8 -6
  124. package/dist/server/rendering/server.js +7 -6
  125. package/dist/server/rendering/server.js.map +1 -1
  126. package/dist/server/rendering/server.mjs +6 -5
  127. package/dist/server/rendering.d.mts +14 -10
  128. package/dist/server/rendering.d.ts +14 -10
  129. package/dist/server/rendering.js +9 -8
  130. package/dist/server/rendering.js.map +1 -1
  131. package/dist/server/rendering.mjs +8 -7
  132. package/dist/server/richTextSchema-DURiozvD.d.mts +62 -0
  133. package/dist/server/richTextSchema-DURiozvD.d.ts +62 -0
  134. package/dist/server/routing.d.mts +178 -11
  135. package/dist/server/routing.d.ts +178 -11
  136. package/dist/server/routing.js +95 -2
  137. package/dist/server/routing.js.map +1 -1
  138. package/dist/server/routing.mjs +94 -1
  139. package/dist/server/routing.mjs.map +1 -1
  140. package/dist/server/{schema-Bpy9N5ZI.d.mts → schema-Z6-afHJG.d.mts} +1 -1
  141. package/dist/server/{schema-Bpy9N5ZI.d.ts → schema-Z6-afHJG.d.ts} +1 -1
  142. package/dist/server/server.d.mts +9 -7
  143. package/dist/server/server.d.ts +9 -7
  144. package/dist/server/server.js +6 -6
  145. package/dist/server/server.mjs +5 -5
  146. package/dist/server/theme-bridge.js +8 -8
  147. package/dist/server/theme-bridge.mjs +2 -2
  148. package/dist/server/{types-Dj8B3QRb.d.ts → types-0f4PIlgx.d.mts} +55 -2
  149. package/dist/server/{types-txWsSxN7.d.mts → types-BjgZt8xJ.d.mts} +63 -2
  150. package/dist/server/{types-BWQ-TohG.d.ts → types-C28kMfa1.d.ts} +254 -82
  151. package/dist/server/{types-CL916r6x.d.ts → types-DLBhEPSt.d.ts} +63 -2
  152. package/dist/server/{types-BLf-hE50.d.mts → types-DuzJZKJI.d.mts} +254 -82
  153. package/dist/server/{types-CdhKJrB0.d.mts → types-kOQyCFXO.d.ts} +55 -2
  154. package/dist/server/{validation-DzvDwwRo.d.mts → validation-BGuRo8P1.d.mts} +18 -5
  155. package/dist/server/{validation-CoU8uAiu.d.ts → validation-DU2YE7u5.d.ts} +18 -5
  156. package/package.json +3 -1
  157. package/dist/server/chunk-6JBKKV3G.js.map +0 -1
  158. package/dist/server/chunk-7DS4Q3GA.mjs.map +0 -1
  159. package/dist/server/chunk-EGTDJ4PL.js.map +0 -1
  160. package/dist/server/chunk-HOY77YBF.js.map +0 -1
  161. package/dist/server/chunk-I6K5REFT.mjs.map +0 -1
  162. package/dist/server/chunk-LCYGQDAB.mjs +0 -835
  163. package/dist/server/chunk-LCYGQDAB.mjs.map +0 -1
  164. package/dist/server/chunk-N3PX76AP.mjs.map +0 -1
  165. package/dist/server/chunk-R5B6IOFQ.js.map +0 -1
  166. package/dist/server/chunk-TNYU5EIO.js +0 -835
  167. package/dist/server/chunk-TNYU5EIO.js.map +0 -1
  168. package/dist/server/chunk-U2NI3TS3.mjs.map +0 -1
  169. package/dist/server/chunk-USQF2XTU.mjs.map +0 -1
  170. package/dist/server/chunk-VHDDXCK6.js.map +0 -1
  171. package/dist/server/loadPage-DLC7DJZP.js +0 -11
  172. package/dist/server/loadPage-GEGN4UAL.mjs +0 -11
  173. /package/dist/server/{chunk-ES6QDZUX.mjs.map → chunk-C6FIJC7T.mjs.map} +0 -0
  174. /package/dist/server/{chunk-24F6FTCI.mjs.map → chunk-OSF34JTQ.mjs.map} +0 -0
  175. /package/dist/server/{chunk-OP2GHK27.mjs.map → chunk-Z5ZA6Q4D.mjs.map} +0 -0
  176. /package/dist/server/{loadPage-GEGN4UAL.mjs.map → loadPage-DNQTTRHL.mjs.map} +0 -0
@@ -1,14 +1,16 @@
1
- import { R as RiverbankClient } from './types-CdhKJrB0.mjs';
2
- import { a as LoadPageResult } from './loadPage-p3AWwwrd.mjs';
3
- import { R as RiverbankSiteConfig } from './types-txWsSxN7.mjs';
4
- import './schema-Bpy9N5ZI.mjs';
5
- import './types-BLf-hE50.mjs';
1
+ import { R as RiverbankClient, e as ResolveEventOccurrenceResponse } from './types-0f4PIlgx.mjs';
2
+ import { a as LoadPageResult } from './loadPage-DGnIK7s4.mjs';
3
+ import { R as RiverbankSiteConfig } from './types-BjgZt8xJ.mjs';
4
+ import './schema-Z6-afHJG.mjs';
5
+ import './types-DuzJZKJI.mjs';
6
6
  import '@riverbankcms/ai';
7
7
  import 'zod';
8
+ import './link-DjxLyC82.mjs';
8
9
  import '@riverbankcms/media-storage-supabase';
9
10
  import '@riverbankcms/db';
10
11
  import 'react/jsx-runtime';
11
12
  import 'react';
13
+ import './core-DsNWrl3o.mjs';
12
14
  import './types-CbagRQ_7.mjs';
13
15
  import './blockKinds-B6MWzNWp.mjs';
14
16
 
@@ -50,12 +52,13 @@ type ResolveRouteParams = {
50
52
  /**
51
53
  * Resolve a URL path to page data, redirect, or 404
52
54
  *
53
- * This helper attempts to fetch the page at the given path and returns
54
- * a discriminated union indicating whether the page was found or not.
55
+ * This helper attempts to fetch the page at the given path. If the page
56
+ * is not found, it checks for configured redirects before returning 404.
55
57
  *
56
- * **Note:** Redirect support is not yet implemented. The `redirect` type
57
- * exists in the return type for future compatibility, but this function
58
- * currently only returns `page` or `not-found` types.
58
+ * Returns a discriminated union indicating:
59
+ * - `page`: Page was found, includes full page data
60
+ * - `redirect`: Path has a redirect rule configured
61
+ * - `not-found`: No page or redirect exists for this path
59
62
  *
60
63
  * @example
61
64
  * ```tsx
@@ -190,4 +193,168 @@ type ContentEntryMatch = {
190
193
  */
191
194
  declare function isContentEntryPath(config: RiverbankSiteConfig, path: string | string[]): ContentEntryMatch;
192
195
 
193
- export { type ContentEntryMatch, type ResolveRouteParams, type RouteResolution, getContentEntryPrefixes, isContentEntryPath, resolveRoute, resolveRoutes };
196
+ /**
197
+ * Event occurrence resolution for dynamic URLs
198
+ *
199
+ * Resolves URL segments like "2025-01-15" or "abc123-uuid" to event occurrence data.
200
+ */
201
+
202
+ /**
203
+ * Data for a resolved event occurrence
204
+ */
205
+ type OccurrenceData = NonNullable<ResolveEventOccurrenceResponse['occurrence']>;
206
+ /**
207
+ * Result of occurrence resolution
208
+ */
209
+ type OccurrenceResolution = {
210
+ found: true;
211
+ occurrence: OccurrenceData;
212
+ } | {
213
+ found: false;
214
+ occurrence: null;
215
+ };
216
+ /**
217
+ * Parameters for resolving an event occurrence
218
+ */
219
+ type ResolveOccurrenceParams = {
220
+ /**
221
+ * SDK client instance
222
+ */
223
+ client: RiverbankClient;
224
+ /**
225
+ * Site ID
226
+ */
227
+ siteId: string;
228
+ /**
229
+ * Parent content entry ID (the event entry)
230
+ */
231
+ entryId: string;
232
+ /**
233
+ * URL segment to resolve - either a date (YYYY-MM-DD) or UUID
234
+ */
235
+ segment: string;
236
+ };
237
+ /**
238
+ * Check if a string is a valid ISO date (YYYY-MM-DD)
239
+ */
240
+ declare function isISODateString(str: string): boolean;
241
+ /**
242
+ * Check if a string is a valid UUID
243
+ */
244
+ declare function isUUID(str: string): boolean;
245
+ /**
246
+ * Validate URL segment format before making API call
247
+ */
248
+ declare function isValidSegment(segment: string): boolean;
249
+ /**
250
+ * Resolve an event occurrence from a URL segment
251
+ *
252
+ * Supports two segment formats:
253
+ * - Date format: "2025-01-15" → finds occurrence starting on that date
254
+ * - UUID format: "abc123..." → finds occurrence by ID
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * import { resolveOccurrence } from '@riverbankcms/sdk/routing';
259
+ *
260
+ * // In a Next.js dynamic route: /events/[slug]/[occurrence]
261
+ * export default async function EventOccurrencePage({ params }) {
262
+ * const result = await resolveOccurrence({
263
+ * client,
264
+ * siteId: 'your-site-id',
265
+ * entryId: eventEntry.id,
266
+ * segment: params.occurrence, // e.g., "2025-01-15"
267
+ * });
268
+ *
269
+ * if (!result.found) {
270
+ * notFound();
271
+ * }
272
+ *
273
+ * return <EventOccurrence occurrence={result.occurrence} />;
274
+ * }
275
+ * ```
276
+ */
277
+ declare function resolveOccurrence(params: ResolveOccurrenceParams): Promise<OccurrenceResolution>;
278
+
279
+ /**
280
+ * Redirect checking for 404 handling
281
+ *
282
+ * Check if a path has a configured redirect rule before returning 404.
283
+ */
284
+
285
+ /**
286
+ * Redirect rule data
287
+ */
288
+ type RedirectRule = {
289
+ /**
290
+ * Destination URL to redirect to
291
+ */
292
+ destination: string;
293
+ /**
294
+ * Whether this is a permanent redirect (301/308) or temporary (302/307)
295
+ */
296
+ permanent: boolean;
297
+ };
298
+ /**
299
+ * Result of redirect checking
300
+ */
301
+ type RedirectResult = {
302
+ hasRedirect: true;
303
+ redirect: RedirectRule;
304
+ } | {
305
+ hasRedirect: false;
306
+ redirect: null;
307
+ };
308
+ /**
309
+ * Parameters for checking redirects
310
+ */
311
+ type CheckRedirectParams = {
312
+ /**
313
+ * SDK client instance
314
+ */
315
+ client: RiverbankClient;
316
+ /**
317
+ * Site ID
318
+ */
319
+ siteId: string;
320
+ /**
321
+ * URL path to check (e.g., '/old-page')
322
+ */
323
+ path: string;
324
+ };
325
+ /**
326
+ * Check if a path has a configured redirect rule
327
+ *
328
+ * Use this when a page is not found to check if there's a redirect
329
+ * configured for the path before returning a 404.
330
+ *
331
+ * @example
332
+ * ```tsx
333
+ * import { checkRedirectForPath } from '@riverbankcms/sdk/routing';
334
+ * import { notFound, redirect } from 'next/navigation';
335
+ *
336
+ * export default async function Page({ params }) {
337
+ * const page = await getPage(params.path);
338
+ *
339
+ * if (!page) {
340
+ * // Check for redirect before 404
341
+ * const result = await checkRedirectForPath({
342
+ * client,
343
+ * siteId: 'your-site-id',
344
+ * path: params.path,
345
+ * });
346
+ *
347
+ * if (result.hasRedirect) {
348
+ * redirect(result.redirect.destination);
349
+ * }
350
+ *
351
+ * notFound();
352
+ * }
353
+ *
354
+ * return <PageContent page={page} />;
355
+ * }
356
+ * ```
357
+ */
358
+ declare function checkRedirectForPath(params: CheckRedirectParams): Promise<RedirectResult>;
359
+
360
+ export { type CheckRedirectParams, type ContentEntryMatch, type OccurrenceData, type OccurrenceResolution, type RedirectResult, type RedirectRule, type ResolveOccurrenceParams, type ResolveRouteParams, type RouteResolution, checkRedirectForPath, getContentEntryPrefixes, isContentEntryPath, isISODateString, isUUID, isValidSegment, resolveOccurrence, resolveRoute, resolveRoutes };
@@ -1,14 +1,16 @@
1
- import { R as RiverbankClient } from './types-Dj8B3QRb.js';
2
- import { a as LoadPageResult } from './loadPage-BA0HiT-6.js';
3
- import { R as RiverbankSiteConfig } from './types-CL916r6x.js';
4
- import './schema-Bpy9N5ZI.js';
5
- import './types-BWQ-TohG.js';
1
+ import { R as RiverbankClient, e as ResolveEventOccurrenceResponse } from './types-kOQyCFXO.js';
2
+ import { a as LoadPageResult } from './loadPage-DW9WB-u9.js';
3
+ import { R as RiverbankSiteConfig } from './types-DLBhEPSt.js';
4
+ import './schema-Z6-afHJG.js';
5
+ import './types-C28kMfa1.js';
6
6
  import '@riverbankcms/ai';
7
7
  import 'zod';
8
+ import './link-DjxLyC82.js';
8
9
  import '@riverbankcms/media-storage-supabase';
9
10
  import '@riverbankcms/db';
10
11
  import 'react/jsx-runtime';
11
12
  import 'react';
13
+ import './core-DsNWrl3o.js';
12
14
  import './types-DuQCNVV0.js';
13
15
  import './blockKinds-B6MWzNWp.js';
14
16
 
@@ -50,12 +52,13 @@ type ResolveRouteParams = {
50
52
  /**
51
53
  * Resolve a URL path to page data, redirect, or 404
52
54
  *
53
- * This helper attempts to fetch the page at the given path and returns
54
- * a discriminated union indicating whether the page was found or not.
55
+ * This helper attempts to fetch the page at the given path. If the page
56
+ * is not found, it checks for configured redirects before returning 404.
55
57
  *
56
- * **Note:** Redirect support is not yet implemented. The `redirect` type
57
- * exists in the return type for future compatibility, but this function
58
- * currently only returns `page` or `not-found` types.
58
+ * Returns a discriminated union indicating:
59
+ * - `page`: Page was found, includes full page data
60
+ * - `redirect`: Path has a redirect rule configured
61
+ * - `not-found`: No page or redirect exists for this path
59
62
  *
60
63
  * @example
61
64
  * ```tsx
@@ -190,4 +193,168 @@ type ContentEntryMatch = {
190
193
  */
191
194
  declare function isContentEntryPath(config: RiverbankSiteConfig, path: string | string[]): ContentEntryMatch;
192
195
 
193
- export { type ContentEntryMatch, type ResolveRouteParams, type RouteResolution, getContentEntryPrefixes, isContentEntryPath, resolveRoute, resolveRoutes };
196
+ /**
197
+ * Event occurrence resolution for dynamic URLs
198
+ *
199
+ * Resolves URL segments like "2025-01-15" or "abc123-uuid" to event occurrence data.
200
+ */
201
+
202
+ /**
203
+ * Data for a resolved event occurrence
204
+ */
205
+ type OccurrenceData = NonNullable<ResolveEventOccurrenceResponse['occurrence']>;
206
+ /**
207
+ * Result of occurrence resolution
208
+ */
209
+ type OccurrenceResolution = {
210
+ found: true;
211
+ occurrence: OccurrenceData;
212
+ } | {
213
+ found: false;
214
+ occurrence: null;
215
+ };
216
+ /**
217
+ * Parameters for resolving an event occurrence
218
+ */
219
+ type ResolveOccurrenceParams = {
220
+ /**
221
+ * SDK client instance
222
+ */
223
+ client: RiverbankClient;
224
+ /**
225
+ * Site ID
226
+ */
227
+ siteId: string;
228
+ /**
229
+ * Parent content entry ID (the event entry)
230
+ */
231
+ entryId: string;
232
+ /**
233
+ * URL segment to resolve - either a date (YYYY-MM-DD) or UUID
234
+ */
235
+ segment: string;
236
+ };
237
+ /**
238
+ * Check if a string is a valid ISO date (YYYY-MM-DD)
239
+ */
240
+ declare function isISODateString(str: string): boolean;
241
+ /**
242
+ * Check if a string is a valid UUID
243
+ */
244
+ declare function isUUID(str: string): boolean;
245
+ /**
246
+ * Validate URL segment format before making API call
247
+ */
248
+ declare function isValidSegment(segment: string): boolean;
249
+ /**
250
+ * Resolve an event occurrence from a URL segment
251
+ *
252
+ * Supports two segment formats:
253
+ * - Date format: "2025-01-15" → finds occurrence starting on that date
254
+ * - UUID format: "abc123..." → finds occurrence by ID
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * import { resolveOccurrence } from '@riverbankcms/sdk/routing';
259
+ *
260
+ * // In a Next.js dynamic route: /events/[slug]/[occurrence]
261
+ * export default async function EventOccurrencePage({ params }) {
262
+ * const result = await resolveOccurrence({
263
+ * client,
264
+ * siteId: 'your-site-id',
265
+ * entryId: eventEntry.id,
266
+ * segment: params.occurrence, // e.g., "2025-01-15"
267
+ * });
268
+ *
269
+ * if (!result.found) {
270
+ * notFound();
271
+ * }
272
+ *
273
+ * return <EventOccurrence occurrence={result.occurrence} />;
274
+ * }
275
+ * ```
276
+ */
277
+ declare function resolveOccurrence(params: ResolveOccurrenceParams): Promise<OccurrenceResolution>;
278
+
279
+ /**
280
+ * Redirect checking for 404 handling
281
+ *
282
+ * Check if a path has a configured redirect rule before returning 404.
283
+ */
284
+
285
+ /**
286
+ * Redirect rule data
287
+ */
288
+ type RedirectRule = {
289
+ /**
290
+ * Destination URL to redirect to
291
+ */
292
+ destination: string;
293
+ /**
294
+ * Whether this is a permanent redirect (301/308) or temporary (302/307)
295
+ */
296
+ permanent: boolean;
297
+ };
298
+ /**
299
+ * Result of redirect checking
300
+ */
301
+ type RedirectResult = {
302
+ hasRedirect: true;
303
+ redirect: RedirectRule;
304
+ } | {
305
+ hasRedirect: false;
306
+ redirect: null;
307
+ };
308
+ /**
309
+ * Parameters for checking redirects
310
+ */
311
+ type CheckRedirectParams = {
312
+ /**
313
+ * SDK client instance
314
+ */
315
+ client: RiverbankClient;
316
+ /**
317
+ * Site ID
318
+ */
319
+ siteId: string;
320
+ /**
321
+ * URL path to check (e.g., '/old-page')
322
+ */
323
+ path: string;
324
+ };
325
+ /**
326
+ * Check if a path has a configured redirect rule
327
+ *
328
+ * Use this when a page is not found to check if there's a redirect
329
+ * configured for the path before returning a 404.
330
+ *
331
+ * @example
332
+ * ```tsx
333
+ * import { checkRedirectForPath } from '@riverbankcms/sdk/routing';
334
+ * import { notFound, redirect } from 'next/navigation';
335
+ *
336
+ * export default async function Page({ params }) {
337
+ * const page = await getPage(params.path);
338
+ *
339
+ * if (!page) {
340
+ * // Check for redirect before 404
341
+ * const result = await checkRedirectForPath({
342
+ * client,
343
+ * siteId: 'your-site-id',
344
+ * path: params.path,
345
+ * });
346
+ *
347
+ * if (result.hasRedirect) {
348
+ * redirect(result.redirect.destination);
349
+ * }
350
+ *
351
+ * notFound();
352
+ * }
353
+ *
354
+ * return <PageContent page={page} />;
355
+ * }
356
+ * ```
357
+ */
358
+ declare function checkRedirectForPath(params: CheckRedirectParams): Promise<RedirectResult>;
359
+
360
+ export { type CheckRedirectParams, type ContentEntryMatch, type OccurrenceData, type OccurrenceResolution, type RedirectResult, type RedirectRule, type ResolveOccurrenceParams, type ResolveRouteParams, type RouteResolution, checkRedirectForPath, getContentEntryPrefixes, isContentEntryPath, isISODateString, isUUID, isValidSegment, resolveOccurrence, resolveRoute, resolveRoutes };
@@ -1,5 +1,34 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }require('./chunk-DGUM43GV.js');
2
2
 
3
+ // src/routing/checkRedirect.ts
4
+ async function checkRedirectForPath(params) {
5
+ const { client, siteId, path } = params;
6
+ try {
7
+ const response = await client.checkRedirect({
8
+ siteId,
9
+ path
10
+ });
11
+ if (response.redirectTo) {
12
+ const isPermanent = response.status === 301 || response.status === 308;
13
+ return {
14
+ hasRedirect: true,
15
+ redirect: {
16
+ destination: response.redirectTo,
17
+ permanent: isPermanent
18
+ }
19
+ };
20
+ }
21
+ return { hasRedirect: false, redirect: null };
22
+ } catch (error) {
23
+ console.warn("[checkRedirectForPath] Failed to check redirect", {
24
+ siteId,
25
+ path,
26
+ error: error instanceof Error ? error.message : String(error)
27
+ });
28
+ return { hasRedirect: false, redirect: null };
29
+ }
30
+ }
31
+
3
32
  // src/routing/resolveRoute.ts
4
33
  async function resolveRoute(params) {
5
34
  const { client, siteId, path, preview = false } = params;
@@ -10,7 +39,7 @@ async function resolveRoute(params) {
10
39
  preview
11
40
  });
12
41
  if (pageResponse) {
13
- const { loadPage } = await Promise.resolve().then(() => _interopRequireWildcard(require("./loadPage-DLC7DJZP.js")));
42
+ const { loadPage } = await Promise.resolve().then(() => _interopRequireWildcard(require("./loadPage-IDGVDFBB.js")));
14
43
  const pageData = await loadPage({
15
44
  client,
16
45
  siteId,
@@ -33,6 +62,25 @@ async function resolveRoute(params) {
33
62
  });
34
63
  }
35
64
  }
65
+ try {
66
+ const redirectResult = await checkRedirectForPath({
67
+ client,
68
+ siteId,
69
+ path
70
+ });
71
+ if (redirectResult.hasRedirect) {
72
+ return {
73
+ type: "redirect",
74
+ destination: redirectResult.redirect.destination,
75
+ permanent: redirectResult.redirect.permanent
76
+ };
77
+ }
78
+ } catch (redirectError) {
79
+ console.debug("[resolveRoute] Redirect check failed", {
80
+ path,
81
+ error: redirectError instanceof Error ? redirectError.message : String(redirectError)
82
+ });
83
+ }
36
84
  return {
37
85
  type: "not-found"
38
86
  };
@@ -105,9 +153,54 @@ function matchPattern(pathSegments, patternSegments) {
105
153
  return { matches: true, slug };
106
154
  }
107
155
 
156
+ // src/routing/resolveOccurrence.ts
157
+ function isISODateString(str) {
158
+ return /^\d{4}-\d{2}-\d{2}$/.test(str);
159
+ }
160
+ function isUUID(str) {
161
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
162
+ }
163
+ function isValidSegment(segment) {
164
+ return isISODateString(segment) || isUUID(segment);
165
+ }
166
+ async function resolveOccurrence(params) {
167
+ const { client, siteId, entryId, segment } = params;
168
+ if (!isValidSegment(segment)) {
169
+ console.debug("[resolveOccurrence] Invalid segment format", { segment });
170
+ return { found: false, occurrence: null };
171
+ }
172
+ try {
173
+ const response = await client.resolveEventOccurrence({
174
+ siteId,
175
+ entryId,
176
+ segment
177
+ });
178
+ if (response.occurrence) {
179
+ return {
180
+ found: true,
181
+ occurrence: response.occurrence
182
+ };
183
+ }
184
+ return { found: false, occurrence: null };
185
+ } catch (error) {
186
+ console.warn("[resolveOccurrence] Failed to resolve occurrence", {
187
+ siteId,
188
+ entryId,
189
+ segment,
190
+ error: error instanceof Error ? error.message : String(error)
191
+ });
192
+ return { found: false, occurrence: null };
193
+ }
194
+ }
195
+
196
+
197
+
198
+
199
+
200
+
108
201
 
109
202
 
110
203
 
111
204
 
112
- exports.getContentEntryPrefixes = getContentEntryPrefixes; exports.isContentEntryPath = isContentEntryPath; exports.resolveRoute = resolveRoute; exports.resolveRoutes = resolveRoutes;
205
+ exports.checkRedirectForPath = checkRedirectForPath; exports.getContentEntryPrefixes = getContentEntryPrefixes; exports.isContentEntryPath = isContentEntryPath; exports.isISODateString = isISODateString; exports.isUUID = isUUID; exports.isValidSegment = isValidSegment; exports.resolveOccurrence = resolveOccurrence; exports.resolveRoute = resolveRoute; exports.resolveRoutes = resolveRoutes;
113
206
  //# sourceMappingURL=routing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/will/Projects/Business/cms/builder/packages/sdk/dist/server/routing.js","../../src/routing/resolveRoute.ts","../../src/routing/contentRoutes.ts"],"names":[],"mappings":"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","file":"/Users/will/Projects/Business/cms/builder/packages/sdk/dist/server/routing.js","sourcesContent":[null,"/**\n * Route resolution helper for dynamic page routing\n *\n * Resolves URL paths to pages, entries, redirects, or 404s.\n */\n\nimport type { RiverbankClient } from '../client/types';\nimport type { LoadPageResult } from '../rendering/helpers/loadPage';\n\nexport type RouteResolution =\n | {\n type: 'page';\n pageData: LoadPageResult;\n }\n | {\n type: 'redirect';\n destination: string;\n permanent: boolean;\n }\n | {\n type: 'not-found';\n };\n\nexport type ResolveRouteParams = {\n /**\n * Builder client instance\n */\n client: RiverbankClient;\n\n /**\n * Site ID\n */\n siteId: string;\n\n /**\n * URL path to resolve (e.g., '/about', '/blog/post-1')\n */\n path: string;\n\n /**\n * If true, fetches draft/unpublished content instead of published content.\n * @default false\n */\n preview?: boolean;\n};\n\n/**\n * Resolve a URL path to page data, redirect, or 404\n *\n * This helper attempts to fetch the page at the given path and returns\n * a discriminated union indicating whether the page was found or not.\n *\n * **Note:** Redirect support is not yet implemented. The `redirect` type\n * exists in the return type for future compatibility, but this function\n * currently only returns `page` or `not-found` types.\n *\n * @example\n * ```tsx\n * import { resolveRoute } from '@riverbankcms/sdk/routing';\n * import { notFound, redirect } from 'next/navigation';\n *\n * export default async function DynamicPage({ params }) {\n * const path = `/${params.slug?.join('/') || ''}`;\n * const resolution = await resolveRoute({\n * client,\n * siteId: 'your-site-id',\n * path,\n * });\n *\n * if (resolution.type === 'redirect') {\n * redirect(resolution.destination);\n * }\n *\n * if (resolution.type === 'not-found') {\n * notFound();\n * }\n *\n * // resolution.type === 'page'\n * return <Page {...resolution.pageData} />;\n * }\n * ```\n */\nexport async function resolveRoute(\n params: ResolveRouteParams\n): Promise<RouteResolution> {\n const { client, siteId, path, preview = false } = params;\n\n try {\n // Attempt to fetch page data\n const pageResponse = await client.getPage({\n siteId,\n path,\n preview,\n });\n\n if (pageResponse) {\n // Successfully found page - load full page data\n const { loadPage } = await import('../rendering/helpers/loadPage');\n const pageData = await loadPage({\n client,\n siteId,\n path,\n preview,\n });\n\n return {\n type: 'page',\n pageData,\n };\n }\n } catch (error) {\n // Distinguish between expected 404s and unexpected errors\n const is404 = error instanceof Error &&\n (error.message.includes('404') ||\n error.message.includes('Not Found') ||\n error.message.includes('not found'));\n\n if (is404) {\n console.debug('[resolveRoute] Page not found', { path });\n } else {\n // Unexpected error - log as warning for visibility\n console.warn('[resolveRoute] Failed to fetch page', {\n path,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // No page found - return not-found\n // Note: Redirect support will be added when client.getRedirect() API is available\n return {\n type: 'not-found',\n };\n}\n\n/**\n * Batch resolve multiple routes in parallel\n *\n * Useful for pre-fetching multiple routes or validating a sitemap.\n *\n * @example\n * ```tsx\n * const resolutions = await resolveRoutes({\n * client,\n * siteId: 'your-site-id',\n * paths: ['/', '/about', '/blog', '/contact'],\n * });\n *\n * resolutions.forEach(({ path, resolution }) => {\n * if (resolution.type === 'page') {\n * console.log(`${path} → Page: ${resolution.pageData.page.name}`);\n * } else if (resolution.type === 'redirect') {\n * console.log(`${path} → Redirect to ${resolution.destination}`);\n * } else {\n * console.log(`${path} → Not found`);\n * }\n * });\n * ```\n */\nexport async function resolveRoutes(params: {\n client: RiverbankClient;\n siteId: string;\n paths: string[];\n preview?: boolean;\n}): Promise<Array<{ path: string; resolution: RouteResolution }>> {\n const { client, siteId, paths, preview } = params;\n\n const resolutions = await Promise.all(\n paths.map(async (path) => {\n const resolution = await resolveRoute({\n client,\n siteId,\n path,\n preview,\n });\n\n return { path, resolution };\n })\n );\n\n return resolutions;\n}\n","/**\n * Content route matching utilities.\n *\n * Derive route matching from SDK config to determine whether a URL path\n * is a CMS page or a content entry, without duplicating route patterns.\n */\n\nimport type { RiverbankSiteConfig, ContentTypeConfig } from '../config';\n\n/**\n * Extract the first path segment from each routable content type's routePattern.\n *\n * Useful for simple prefix-based routing decisions. For more precise matching\n * that handles nested patterns, use `isContentEntryPath` instead.\n *\n * @param config - The SDK config object from defineConfig()\n * @returns Array of first path segments from content type routePatterns\n *\n * @example\n * ```typescript\n * import { getContentEntryPrefixes } from '@riverbankcms/sdk/routing';\n * import config from '../riverbank.config';\n *\n * // Config with routePatterns: '/blog/{slug}', '/work/projects/{slug}'\n * const prefixes = getContentEntryPrefixes(config);\n * // Returns ['blog', 'work']\n * ```\n */\nexport function getContentEntryPrefixes(config: RiverbankSiteConfig): string[] {\n const contentTypes = config.content?.contentTypes ?? [];\n\n return contentTypes\n .filter((ct): ct is ContentTypeConfig & { routePattern: string } =>\n ct.hasPages && typeof ct.routePattern === 'string'\n )\n .map(ct => {\n // '/blog/{slug}' → 'blog'\n // '/work/projects/{slug}' → 'work'\n // '/{slug}' → undefined (no static prefix)\n const match = ct.routePattern.match(/^\\/([^/]+)/);\n const segment = match?.[1];\n // Skip dynamic segments (those containing {})\n if (segment?.includes('{')) return undefined;\n return segment;\n })\n .filter((prefix): prefix is string => typeof prefix === 'string');\n}\n\n/**\n * Result of checking if a path matches a content entry route.\n */\nexport type ContentEntryMatch = {\n /** Whether the path matches a content entry route pattern */\n isEntry: boolean;\n /** The content type key if matched (e.g., 'blog-post') */\n contentType?: string;\n /** The slug extracted from the path if matched */\n slug?: string;\n};\n\n/**\n * Check if a URL path matches any content entry route pattern.\n *\n * Supports nested patterns like '/work/projects/{slug}'. Returns the matched\n * content type key and extracted slug, useful for routing decisions.\n *\n * **Note:** Content types are checked in array order. If multiple patterns\n * could match a path, the first matching content type wins. Order more specific\n * patterns before generic ones in your config.\n *\n * @param config - The SDK config object from defineConfig()\n * @param path - URL path as string ('/blog/my-post') or segments ['blog', 'my-post']\n * @returns Match result with content type and slug if matched\n *\n * @example\n * ```typescript\n * import { isContentEntryPath } from '@riverbankcms/sdk/routing';\n * import config from '../riverbank.config';\n *\n * // Simple pattern: '/blog/{slug}'\n * isContentEntryPath(config, '/blog/my-post');\n * // Returns { isEntry: true, contentType: 'blog-post', slug: 'my-post' }\n *\n * // Nested pattern: '/work/projects/{slug}'\n * isContentEntryPath(config, '/work/projects/website-redesign');\n * // Returns { isEntry: true, contentType: 'project', slug: 'website-redesign' }\n *\n * // Non-matching path\n * isContentEntryPath(config, '/about');\n * // Returns { isEntry: false }\n * ```\n */\nexport function isContentEntryPath(\n config: RiverbankSiteConfig,\n path: string | string[]\n): ContentEntryMatch {\n const segments = typeof path === 'string'\n ? path.split('/').filter(Boolean)\n : path;\n\n const contentTypes = config.content?.contentTypes ?? [];\n\n for (const ct of contentTypes) {\n if (!ct.hasPages || !ct.routePattern) continue;\n\n // Parse pattern: '/blog/{slug}' → ['blog'], '/work/projects/{slug}' → ['work', 'projects']\n const patternSegments = parseRoutePattern(ct.routePattern);\n\n // Check if path segments match the pattern\n const match = matchPattern(segments, patternSegments);\n if (match.matches) {\n return {\n isEntry: true,\n contentType: ct.key,\n slug: match.slug,\n };\n }\n }\n\n return { isEntry: false };\n}\n\n/**\n * Parse a route pattern into static segments before {slug}.\n *\n * @example\n * parseRoutePattern('/blog/{slug}') // ['blog']\n * parseRoutePattern('/work/projects/{slug}') // ['work', 'projects']\n * parseRoutePattern('/{slug}') // []\n */\nfunction parseRoutePattern(pattern: string): string[] {\n const segments = pattern.split('/').filter(Boolean);\n const staticSegments: string[] = [];\n\n for (const segment of segments) {\n if (segment.includes('{')) break; // Stop at dynamic segment\n staticSegments.push(segment);\n }\n\n return staticSegments;\n}\n\n/**\n * Check if path segments match pattern segments.\n * Path must have at least one more segment than pattern (the slug).\n *\n * @returns Match result with extracted slug if matched\n */\nfunction matchPattern(\n pathSegments: string[],\n patternSegments: string[]\n): { matches: boolean; slug?: string } {\n // Path needs: pattern segments + at least 1 slug segment\n if (pathSegments.length < patternSegments.length + 1) {\n return { matches: false };\n }\n\n // All pattern segments must match exactly\n for (let i = 0; i < patternSegments.length; i++) {\n if (pathSegments[i] !== patternSegments[i]) {\n return { matches: false };\n }\n }\n\n // Extract slug (everything after the pattern segments)\n // For '/blog/{slug}' with path '/blog/my-post' → slug = 'my-post'\n // For nested slugs, join remaining segments (rare but supported)\n const slugSegments = pathSegments.slice(patternSegments.length);\n const slug = slugSegments.join('/');\n\n return { matches: true, slug };\n}\n"]}
1
+ {"version":3,"sources":["/Users/will/Projects/Business/cms/builder/packages/sdk/dist/server/routing.js","../../src/routing/checkRedirect.ts","../../src/routing/resolveRoute.ts","../../src/routing/contentRoutes.ts","../../src/routing/resolveOccurrence.ts"],"names":[],"mappings":"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","file":"/Users/will/Projects/Business/cms/builder/packages/sdk/dist/server/routing.js","sourcesContent":[null,"/**\n * Redirect checking for 404 handling\n *\n * Check if a path has a configured redirect rule before returning 404.\n */\n\nimport type { RiverbankClient } from '../client/types';\n\n/**\n * Redirect rule data\n */\nexport type RedirectRule = {\n /**\n * Destination URL to redirect to\n */\n destination: string;\n\n /**\n * Whether this is a permanent redirect (301/308) or temporary (302/307)\n */\n permanent: boolean;\n};\n\n/**\n * Result of redirect checking\n */\nexport type RedirectResult =\n | {\n hasRedirect: true;\n redirect: RedirectRule;\n }\n | {\n hasRedirect: false;\n redirect: null;\n };\n\n/**\n * Parameters for checking redirects\n */\nexport type CheckRedirectParams = {\n /**\n * SDK client instance\n */\n client: RiverbankClient;\n\n /**\n * Site ID\n */\n siteId: string;\n\n /**\n * URL path to check (e.g., '/old-page')\n */\n path: string;\n};\n\n/**\n * Check if a path has a configured redirect rule\n *\n * Use this when a page is not found to check if there's a redirect\n * configured for the path before returning a 404.\n *\n * @example\n * ```tsx\n * import { checkRedirectForPath } from '@riverbankcms/sdk/routing';\n * import { notFound, redirect } from 'next/navigation';\n *\n * export default async function Page({ params }) {\n * const page = await getPage(params.path);\n *\n * if (!page) {\n * // Check for redirect before 404\n * const result = await checkRedirectForPath({\n * client,\n * siteId: 'your-site-id',\n * path: params.path,\n * });\n *\n * if (result.hasRedirect) {\n * redirect(result.redirect.destination);\n * }\n *\n * notFound();\n * }\n *\n * return <PageContent page={page} />;\n * }\n * ```\n */\nexport async function checkRedirectForPath(\n params: CheckRedirectParams\n): Promise<RedirectResult> {\n const { client, siteId, path } = params;\n\n try {\n const response = await client.checkRedirect({\n siteId,\n path,\n });\n\n // API returns { redirectTo: string | null, status?: number }\n if (response.redirectTo) {\n // Status 301 and 308 are permanent, others are temporary\n const isPermanent = response.status === 301 || response.status === 308;\n\n return {\n hasRedirect: true,\n redirect: {\n destination: response.redirectTo,\n permanent: isPermanent,\n },\n };\n }\n\n return { hasRedirect: false, redirect: null };\n } catch (error) {\n console.warn('[checkRedirectForPath] Failed to check redirect', {\n siteId,\n path,\n error: error instanceof Error ? error.message : String(error),\n });\n\n return { hasRedirect: false, redirect: null };\n }\n}\n","/**\n * Route resolution helper for dynamic page routing\n *\n * Resolves URL paths to pages, entries, redirects, or 404s.\n */\n\nimport type { RiverbankClient } from '../client/types';\nimport type { LoadPageResult } from '../rendering/helpers/loadPage';\nimport { checkRedirectForPath } from './checkRedirect';\n\nexport type RouteResolution =\n | {\n type: 'page';\n pageData: LoadPageResult;\n }\n | {\n type: 'redirect';\n destination: string;\n permanent: boolean;\n }\n | {\n type: 'not-found';\n };\n\nexport type ResolveRouteParams = {\n /**\n * Builder client instance\n */\n client: RiverbankClient;\n\n /**\n * Site ID\n */\n siteId: string;\n\n /**\n * URL path to resolve (e.g., '/about', '/blog/post-1')\n */\n path: string;\n\n /**\n * If true, fetches draft/unpublished content instead of published content.\n * @default false\n */\n preview?: boolean;\n};\n\n/**\n * Resolve a URL path to page data, redirect, or 404\n *\n * This helper attempts to fetch the page at the given path. If the page\n * is not found, it checks for configured redirects before returning 404.\n *\n * Returns a discriminated union indicating:\n * - `page`: Page was found, includes full page data\n * - `redirect`: Path has a redirect rule configured\n * - `not-found`: No page or redirect exists for this path\n *\n * @example\n * ```tsx\n * import { resolveRoute } from '@riverbankcms/sdk/routing';\n * import { notFound, redirect } from 'next/navigation';\n *\n * export default async function DynamicPage({ params }) {\n * const path = `/${params.slug?.join('/') || ''}`;\n * const resolution = await resolveRoute({\n * client,\n * siteId: 'your-site-id',\n * path,\n * });\n *\n * if (resolution.type === 'redirect') {\n * redirect(resolution.destination);\n * }\n *\n * if (resolution.type === 'not-found') {\n * notFound();\n * }\n *\n * // resolution.type === 'page'\n * return <Page {...resolution.pageData} />;\n * }\n * ```\n */\nexport async function resolveRoute(\n params: ResolveRouteParams\n): Promise<RouteResolution> {\n const { client, siteId, path, preview = false } = params;\n\n try {\n // Attempt to fetch page data\n const pageResponse = await client.getPage({\n siteId,\n path,\n preview,\n });\n\n if (pageResponse) {\n // Successfully found page - load full page data\n const { loadPage } = await import('../rendering/helpers/loadPage');\n const pageData = await loadPage({\n client,\n siteId,\n path,\n preview,\n });\n\n return {\n type: 'page',\n pageData,\n };\n }\n } catch (error) {\n // Distinguish between expected 404s and unexpected errors\n const is404 = error instanceof Error &&\n (error.message.includes('404') ||\n error.message.includes('Not Found') ||\n error.message.includes('not found'));\n\n if (is404) {\n console.debug('[resolveRoute] Page not found', { path });\n } else {\n // Unexpected error - log as warning for visibility\n console.warn('[resolveRoute] Failed to fetch page', {\n path,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // No page found - check for redirect before returning 404\n try {\n const redirectResult = await checkRedirectForPath({\n client,\n siteId,\n path,\n });\n\n if (redirectResult.hasRedirect) {\n return {\n type: 'redirect',\n destination: redirectResult.redirect.destination,\n permanent: redirectResult.redirect.permanent,\n };\n }\n } catch (redirectError) {\n console.debug('[resolveRoute] Redirect check failed', {\n path,\n error: redirectError instanceof Error ? redirectError.message : String(redirectError),\n });\n }\n\n return {\n type: 'not-found',\n };\n}\n\n/**\n * Batch resolve multiple routes in parallel\n *\n * Useful for pre-fetching multiple routes or validating a sitemap.\n *\n * @example\n * ```tsx\n * const resolutions = await resolveRoutes({\n * client,\n * siteId: 'your-site-id',\n * paths: ['/', '/about', '/blog', '/contact'],\n * });\n *\n * resolutions.forEach(({ path, resolution }) => {\n * if (resolution.type === 'page') {\n * console.log(`${path} → Page: ${resolution.pageData.page.name}`);\n * } else if (resolution.type === 'redirect') {\n * console.log(`${path} → Redirect to ${resolution.destination}`);\n * } else {\n * console.log(`${path} → Not found`);\n * }\n * });\n * ```\n */\nexport async function resolveRoutes(params: {\n client: RiverbankClient;\n siteId: string;\n paths: string[];\n preview?: boolean;\n}): Promise<Array<{ path: string; resolution: RouteResolution }>> {\n const { client, siteId, paths, preview } = params;\n\n const resolutions = await Promise.all(\n paths.map(async (path) => {\n const resolution = await resolveRoute({\n client,\n siteId,\n path,\n preview,\n });\n\n return { path, resolution };\n })\n );\n\n return resolutions;\n}\n","/**\n * Content route matching utilities.\n *\n * Derive route matching from SDK config to determine whether a URL path\n * is a CMS page or a content entry, without duplicating route patterns.\n */\n\nimport type { RiverbankSiteConfig, ContentTypeConfig } from '../config';\n\n/**\n * Extract the first path segment from each routable content type's routePattern.\n *\n * Useful for simple prefix-based routing decisions. For more precise matching\n * that handles nested patterns, use `isContentEntryPath` instead.\n *\n * @param config - The SDK config object from defineConfig()\n * @returns Array of first path segments from content type routePatterns\n *\n * @example\n * ```typescript\n * import { getContentEntryPrefixes } from '@riverbankcms/sdk/routing';\n * import config from '../riverbank.config';\n *\n * // Config with routePatterns: '/blog/{slug}', '/work/projects/{slug}'\n * const prefixes = getContentEntryPrefixes(config);\n * // Returns ['blog', 'work']\n * ```\n */\nexport function getContentEntryPrefixes(config: RiverbankSiteConfig): string[] {\n const contentTypes = config.content?.contentTypes ?? [];\n\n return contentTypes\n .filter((ct): ct is ContentTypeConfig & { routePattern: string } =>\n ct.hasPages && typeof ct.routePattern === 'string'\n )\n .map(ct => {\n // '/blog/{slug}' → 'blog'\n // '/work/projects/{slug}' → 'work'\n // '/{slug}' → undefined (no static prefix)\n const match = ct.routePattern.match(/^\\/([^/]+)/);\n const segment = match?.[1];\n // Skip dynamic segments (those containing {})\n if (segment?.includes('{')) return undefined;\n return segment;\n })\n .filter((prefix): prefix is string => typeof prefix === 'string');\n}\n\n/**\n * Result of checking if a path matches a content entry route.\n */\nexport type ContentEntryMatch = {\n /** Whether the path matches a content entry route pattern */\n isEntry: boolean;\n /** The content type key if matched (e.g., 'blog-post') */\n contentType?: string;\n /** The slug extracted from the path if matched */\n slug?: string;\n};\n\n/**\n * Check if a URL path matches any content entry route pattern.\n *\n * Supports nested patterns like '/work/projects/{slug}'. Returns the matched\n * content type key and extracted slug, useful for routing decisions.\n *\n * **Note:** Content types are checked in array order. If multiple patterns\n * could match a path, the first matching content type wins. Order more specific\n * patterns before generic ones in your config.\n *\n * @param config - The SDK config object from defineConfig()\n * @param path - URL path as string ('/blog/my-post') or segments ['blog', 'my-post']\n * @returns Match result with content type and slug if matched\n *\n * @example\n * ```typescript\n * import { isContentEntryPath } from '@riverbankcms/sdk/routing';\n * import config from '../riverbank.config';\n *\n * // Simple pattern: '/blog/{slug}'\n * isContentEntryPath(config, '/blog/my-post');\n * // Returns { isEntry: true, contentType: 'blog-post', slug: 'my-post' }\n *\n * // Nested pattern: '/work/projects/{slug}'\n * isContentEntryPath(config, '/work/projects/website-redesign');\n * // Returns { isEntry: true, contentType: 'project', slug: 'website-redesign' }\n *\n * // Non-matching path\n * isContentEntryPath(config, '/about');\n * // Returns { isEntry: false }\n * ```\n */\nexport function isContentEntryPath(\n config: RiverbankSiteConfig,\n path: string | string[]\n): ContentEntryMatch {\n const segments = typeof path === 'string'\n ? path.split('/').filter(Boolean)\n : path;\n\n const contentTypes = config.content?.contentTypes ?? [];\n\n for (const ct of contentTypes) {\n if (!ct.hasPages || !ct.routePattern) continue;\n\n // Parse pattern: '/blog/{slug}' → ['blog'], '/work/projects/{slug}' → ['work', 'projects']\n const patternSegments = parseRoutePattern(ct.routePattern);\n\n // Check if path segments match the pattern\n const match = matchPattern(segments, patternSegments);\n if (match.matches) {\n return {\n isEntry: true,\n contentType: ct.key,\n slug: match.slug,\n };\n }\n }\n\n return { isEntry: false };\n}\n\n/**\n * Parse a route pattern into static segments before {slug}.\n *\n * @example\n * parseRoutePattern('/blog/{slug}') // ['blog']\n * parseRoutePattern('/work/projects/{slug}') // ['work', 'projects']\n * parseRoutePattern('/{slug}') // []\n */\nfunction parseRoutePattern(pattern: string): string[] {\n const segments = pattern.split('/').filter(Boolean);\n const staticSegments: string[] = [];\n\n for (const segment of segments) {\n if (segment.includes('{')) break; // Stop at dynamic segment\n staticSegments.push(segment);\n }\n\n return staticSegments;\n}\n\n/**\n * Check if path segments match pattern segments.\n * Path must have at least one more segment than pattern (the slug).\n *\n * @returns Match result with extracted slug if matched\n */\nfunction matchPattern(\n pathSegments: string[],\n patternSegments: string[]\n): { matches: boolean; slug?: string } {\n // Path needs: pattern segments + at least 1 slug segment\n if (pathSegments.length < patternSegments.length + 1) {\n return { matches: false };\n }\n\n // All pattern segments must match exactly\n for (let i = 0; i < patternSegments.length; i++) {\n if (pathSegments[i] !== patternSegments[i]) {\n return { matches: false };\n }\n }\n\n // Extract slug (everything after the pattern segments)\n // For '/blog/{slug}' with path '/blog/my-post' → slug = 'my-post'\n // For nested slugs, join remaining segments (rare but supported)\n const slugSegments = pathSegments.slice(patternSegments.length);\n const slug = slugSegments.join('/');\n\n return { matches: true, slug };\n}\n","/**\n * Event occurrence resolution for dynamic URLs\n *\n * Resolves URL segments like \"2025-01-15\" or \"abc123-uuid\" to event occurrence data.\n */\n\nimport type { RiverbankClient, ResolveEventOccurrenceResponse } from '../client/types';\n\n/**\n * Data for a resolved event occurrence\n */\nexport type OccurrenceData = NonNullable<ResolveEventOccurrenceResponse['occurrence']>;\n\n/**\n * Result of occurrence resolution\n */\nexport type OccurrenceResolution =\n | {\n found: true;\n occurrence: OccurrenceData;\n }\n | {\n found: false;\n occurrence: null;\n };\n\n/**\n * Parameters for resolving an event occurrence\n */\nexport type ResolveOccurrenceParams = {\n /**\n * SDK client instance\n */\n client: RiverbankClient;\n\n /**\n * Site ID\n */\n siteId: string;\n\n /**\n * Parent content entry ID (the event entry)\n */\n entryId: string;\n\n /**\n * URL segment to resolve - either a date (YYYY-MM-DD) or UUID\n */\n segment: string;\n};\n\n/**\n * Check if a string is a valid ISO date (YYYY-MM-DD)\n */\nfunction isISODateString(str: string): boolean {\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(str);\n}\n\n/**\n * Check if a string is a valid UUID\n */\nfunction isUUID(str: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);\n}\n\n/**\n * Validate URL segment format before making API call\n */\nfunction isValidSegment(segment: string): boolean {\n return isISODateString(segment) || isUUID(segment);\n}\n\n/**\n * Resolve an event occurrence from a URL segment\n *\n * Supports two segment formats:\n * - Date format: \"2025-01-15\" → finds occurrence starting on that date\n * - UUID format: \"abc123...\" → finds occurrence by ID\n *\n * @example\n * ```tsx\n * import { resolveOccurrence } from '@riverbankcms/sdk/routing';\n *\n * // In a Next.js dynamic route: /events/[slug]/[occurrence]\n * export default async function EventOccurrencePage({ params }) {\n * const result = await resolveOccurrence({\n * client,\n * siteId: 'your-site-id',\n * entryId: eventEntry.id,\n * segment: params.occurrence, // e.g., \"2025-01-15\"\n * });\n *\n * if (!result.found) {\n * notFound();\n * }\n *\n * return <EventOccurrence occurrence={result.occurrence} />;\n * }\n * ```\n */\nexport async function resolveOccurrence(\n params: ResolveOccurrenceParams\n): Promise<OccurrenceResolution> {\n const { client, siteId, entryId, segment } = params;\n\n // Validate segment format client-side to avoid unnecessary API calls\n if (!isValidSegment(segment)) {\n console.debug('[resolveOccurrence] Invalid segment format', { segment });\n return { found: false, occurrence: null };\n }\n\n try {\n const response = await client.resolveEventOccurrence({\n siteId,\n entryId,\n segment,\n });\n\n if (response.occurrence) {\n return {\n found: true,\n occurrence: response.occurrence,\n };\n }\n\n return { found: false, occurrence: null };\n } catch (error) {\n console.warn('[resolveOccurrence] Failed to resolve occurrence', {\n siteId,\n entryId,\n segment,\n error: error instanceof Error ? error.message : String(error),\n });\n\n return { found: false, occurrence: null };\n }\n}\n\nexport { isISODateString, isUUID, isValidSegment };\n"]}