cms-renderer 0.0.0 → 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.
Files changed (51) hide show
  1. package/dist/chunk-G22U6UHQ.js +45 -0
  2. package/dist/chunk-G22U6UHQ.js.map +1 -0
  3. package/dist/chunk-HVKFEZBT.js +116 -0
  4. package/dist/chunk-HVKFEZBT.js.map +1 -0
  5. package/dist/chunk-RPM73PQZ.js +17 -0
  6. package/dist/chunk-RPM73PQZ.js.map +1 -0
  7. package/dist/lib/block-renderer.d.ts +32 -0
  8. package/dist/lib/block-renderer.js +7 -0
  9. package/dist/lib/block-renderer.js.map +1 -0
  10. package/dist/lib/cms-api.d.ts +24 -0
  11. package/dist/lib/cms-api.js +7 -0
  12. package/dist/lib/cms-api.js.map +1 -0
  13. package/dist/lib/data-utils.d.ts +218 -0
  14. package/dist/lib/data-utils.js +247 -0
  15. package/dist/lib/data-utils.js.map +1 -0
  16. package/dist/lib/image/lazy-load.d.ts +75 -0
  17. package/dist/lib/image/lazy-load.js +83 -0
  18. package/dist/lib/image/lazy-load.js.map +1 -0
  19. package/dist/lib/markdown-utils.d.ts +172 -0
  20. package/dist/lib/markdown-utils.js +137 -0
  21. package/dist/lib/markdown-utils.js.map +1 -0
  22. package/dist/lib/renderer.d.ts +36 -0
  23. package/dist/lib/renderer.js +343 -0
  24. package/dist/lib/renderer.js.map +1 -0
  25. package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
  26. package/dist/lib/result.js +37 -0
  27. package/dist/lib/result.js.map +1 -0
  28. package/dist/lib/schema.d.ts +15 -0
  29. package/dist/lib/schema.js +35 -0
  30. package/dist/lib/schema.js.map +1 -0
  31. package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
  32. package/dist/lib/trpc.js +7 -0
  33. package/dist/lib/trpc.js.map +1 -0
  34. package/dist/lib/types.d.ts +163 -0
  35. package/dist/lib/types.js +1 -0
  36. package/dist/lib/types.js.map +1 -0
  37. package/package.json +50 -11
  38. package/.turbo/turbo-check-types.log +0 -2
  39. package/lib/__tests__/enrich-block-images.test.ts +0 -394
  40. package/lib/block-renderer.tsx +0 -60
  41. package/lib/cms-api.ts +0 -86
  42. package/lib/data-utils.ts +0 -572
  43. package/lib/image/lazy-load.ts +0 -209
  44. package/lib/markdown-utils.ts +0 -368
  45. package/lib/renderer.tsx +0 -189
  46. package/lib/schema.ts +0 -74
  47. package/lib/types.ts +0 -201
  48. package/next.config.ts +0 -39
  49. package/postcss.config.mjs +0 -5
  50. package/tsconfig.json +0 -12
  51. package/tsconfig.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,26 +1,64 @@
1
1
  {
2
2
  "name": "cms-renderer",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
7
- "./components/*": "./components/*",
8
- "./components/blocks/*": "./components/blocks/*",
9
- "./lib/*": "./lib/*",
10
- "./server/*": "./server/*",
11
- "./styles/*": "./styles/*"
7
+ "./lib/block-renderer": {
8
+ "types": "./dist/lib/block-renderer.d.ts",
9
+ "import": "./dist/lib/block-renderer.js"
10
+ },
11
+ "./lib/cms-api": {
12
+ "types": "./dist/lib/cms-api.d.ts",
13
+ "import": "./dist/lib/cms-api.js"
14
+ },
15
+ "./lib/data-utils": {
16
+ "types": "./dist/lib/data-utils.d.ts",
17
+ "import": "./dist/lib/data-utils.js"
18
+ },
19
+ "./lib/markdown-utils": {
20
+ "types": "./dist/lib/markdown-utils.d.ts",
21
+ "import": "./dist/lib/markdown-utils.js"
22
+ },
23
+ "./lib/renderer": {
24
+ "types": "./dist/lib/renderer.d.ts",
25
+ "import": "./dist/lib/renderer.js"
26
+ },
27
+ "./lib/result": {
28
+ "types": "./dist/lib/result.d.ts",
29
+ "import": "./dist/lib/result.js"
30
+ },
31
+ "./lib/schema": {
32
+ "types": "./dist/lib/schema.d.ts",
33
+ "import": "./dist/lib/schema.js"
34
+ },
35
+ "./lib/trpc": {
36
+ "types": "./dist/lib/trpc.d.ts",
37
+ "import": "./dist/lib/trpc.js"
38
+ },
39
+ "./lib/types": {
40
+ "types": "./dist/lib/types.d.ts",
41
+ "import": "./dist/lib/types.js"
42
+ },
43
+ "./lib/image/lazy-load": {
44
+ "types": "./dist/lib/image/lazy-load.d.ts",
45
+ "import": "./dist/lib/image/lazy-load.js"
46
+ }
12
47
  },
48
+ "files": [
49
+ "dist"
50
+ ],
13
51
  "publishConfig": {
14
52
  "access": "public"
15
53
  },
16
54
  "scripts": {
55
+ "build": "tsup",
56
+ "prepublishOnly": "npm run build",
17
57
  "lint": "biome check .",
18
58
  "check-types": "tsc --noEmit",
19
- "clean": "rm -rf .next .turbo node_modules"
59
+ "clean": "rm -rf .next .turbo node_modules dist"
20
60
  },
21
61
  "dependencies": {
22
- "@auteur/trpc-utils": "0.0.0",
23
- "@repo/supabase-utils": "0.0.0",
24
62
  "@hookform/resolvers": "^5.1.0",
25
63
  "@lexical/code": "0.39.0",
26
64
  "@lexical/link": "0.39.0",
@@ -30,8 +68,6 @@
30
68
  "@lexical/rich-text": "0.39.0",
31
69
  "@lexical/selection": "0.39.0",
32
70
  "@lexical/utils": "0.39.0",
33
- "@repo/cms-schema": "0.0.0",
34
- "@repo/markdown-wasm": "0.0.0",
35
71
  "@supabase/ssr": "0.8.0",
36
72
  "@supabase/supabase-js": "2.90.1",
37
73
  "@tanstack/react-query": "5.90.16",
@@ -39,6 +75,7 @@
39
75
  "@trpc/client": "11.8.1",
40
76
  "@trpc/react-query": "11.8.1",
41
77
  "@trpc/server": "11.8.1",
78
+ "md4w": "^0.2.7",
42
79
  "next": "^16.1.1",
43
80
  "react": "^19.1.0",
44
81
  "react-dom": "^19.1.0",
@@ -47,7 +84,9 @@
47
84
  },
48
85
  "devDependencies": {
49
86
  "@happy-dom/global-registrator": "20.1.0",
87
+ "@repo/cms-schema": "0.0.0",
50
88
  "@repo/typescript-config": "0.0.0",
89
+ "tsup": "^8.3.5",
51
90
  "@tailwindcss/postcss": "4",
52
91
  "@testing-library/jest-dom": "6.9.1",
53
92
  "@types/node": "^25",
@@ -1,2 +0,0 @@
1
-
2
- $ tsc --noEmit
@@ -1,394 +0,0 @@
1
- import { beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
2
-
3
- // =============================================================================
4
- // Mocks - Must be set up BEFORE any imports that use these modules
5
- // =============================================================================
6
-
7
- // Mock 'server-only' to allow running in test environment
8
- // This must be called before the module that imports it is loaded
9
- mock.module('server-only', () => ({}));
10
-
11
- // Mock generateSignedImageData to return predictable signed data
12
- const mockGenerateSignedImageData = mock(
13
- (config: { assetId: string; width: number; height: number; url: string }) => ({
14
- signedSrcset: `signed-srcset-${config.assetId}`,
15
- signedFallbackSrcset: `signed-fallback-srcset-${config.assetId}`,
16
- signedDefaultSrc: `signed-default-src-${config.assetId}`,
17
- })
18
- );
19
-
20
- mock.module('../image-transforms.server', () => ({
21
- generateSignedImageData: mockGenerateSignedImageData,
22
- }));
23
-
24
- // =============================================================================
25
- // Dynamic Import - Must happen after mocks are registered
26
- // =============================================================================
27
-
28
- type EnrichFunction = <T extends Record<string, unknown>>(content: T | null) => T | null;
29
- let enrichBlockWithSignedUrls: EnrichFunction;
30
-
31
- beforeAll(async () => {
32
- const mod = await import('../enrich-block-images');
33
- enrichBlockWithSignedUrls = mod.enrichBlockWithSignedUrls;
34
- });
35
-
36
- // =============================================================================
37
- // Test Data
38
- // =============================================================================
39
-
40
- const createResolvedImageRef = (id: string = '12345678-1234-1234-1234-123456789abc') => ({
41
- alt: 'Test image',
42
- _asset: {
43
- id,
44
- url: '/api/dev/storage/test.jpg',
45
- width: 1920,
46
- height: 1080,
47
- lqip: 'data:image/jpeg;base64,/9j/...',
48
- },
49
- });
50
-
51
- // =============================================================================
52
- // Tests
53
- // =============================================================================
54
-
55
- describe('enrichBlockWithSignedUrls', () => {
56
- beforeEach(() => {
57
- mockGenerateSignedImageData.mockClear();
58
- });
59
-
60
- describe('Null/Undefined Handling', () => {
61
- it('returns null when content is null', () => {
62
- const result = enrichBlockWithSignedUrls(null);
63
- expect(result).toBeNull();
64
- });
65
-
66
- it('returns content unchanged when it has no image references', () => {
67
- const content = { title: 'Hello', count: 42 };
68
- const result = enrichBlockWithSignedUrls(content);
69
- expect(result).toEqual(content);
70
- });
71
- });
72
-
73
- describe('Image Reference Detection', () => {
74
- it('detects resolved image reference with alt and _asset', () => {
75
- const imageRef = createResolvedImageRef();
76
- const content = { backgroundImage: imageRef };
77
-
78
- const result = enrichBlockWithSignedUrls(content);
79
-
80
- expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
81
- expect(result?.backgroundImage._signed).toBeDefined();
82
- });
83
-
84
- it('ignores objects missing _asset.id', () => {
85
- const content = {
86
- notAnImage: {
87
- alt: 'Test',
88
- _asset: {
89
- url: '/test.jpg',
90
- width: 1920,
91
- height: 1080,
92
- },
93
- },
94
- };
95
-
96
- const result = enrichBlockWithSignedUrls(content);
97
-
98
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
99
- expect((result?.notAnImage as Record<string, unknown>)._signed).toBeUndefined();
100
- });
101
-
102
- it('ignores objects missing alt', () => {
103
- const content = {
104
- notAnImage: {
105
- _asset: {
106
- id: '12345678-1234-1234-1234-123456789abc',
107
- url: '/test.jpg',
108
- width: 1920,
109
- height: 1080,
110
- },
111
- },
112
- };
113
-
114
- const result = enrichBlockWithSignedUrls(content);
115
-
116
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
117
- expect((result?.notAnImage as Record<string, unknown>)._signed).toBeUndefined();
118
- });
119
-
120
- it('ignores objects where _asset is null', () => {
121
- const content = {
122
- notAnImage: {
123
- alt: 'Test',
124
- _asset: null,
125
- },
126
- };
127
-
128
- enrichBlockWithSignedUrls(content);
129
-
130
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
131
- });
132
-
133
- it('ignores objects where _asset is missing id', () => {
134
- const content = {
135
- notAnImage: {
136
- alt: 'Test',
137
- _asset: {
138
- url: '/test.jpg',
139
- width: 1920,
140
- height: 1080,
141
- },
142
- },
143
- };
144
-
145
- enrichBlockWithSignedUrls(content);
146
-
147
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
148
- });
149
-
150
- it('ignores objects where _asset is missing url', () => {
151
- const content = {
152
- notAnImage: {
153
- alt: 'Test',
154
- _asset: {
155
- id: '12345678-1234-1234-1234-123456789abc',
156
- width: 1920,
157
- height: 1080,
158
- },
159
- },
160
- };
161
-
162
- enrichBlockWithSignedUrls(content);
163
-
164
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
165
- });
166
-
167
- it('ignores objects where _asset is missing width', () => {
168
- const content = {
169
- notAnImage: {
170
- alt: 'Test',
171
- _asset: {
172
- id: '12345678-1234-1234-1234-123456789abc',
173
- url: '/test.jpg',
174
- height: 1080,
175
- },
176
- },
177
- };
178
-
179
- enrichBlockWithSignedUrls(content);
180
-
181
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
182
- });
183
-
184
- it('ignores objects where _asset is missing height', () => {
185
- const content = {
186
- notAnImage: {
187
- alt: 'Test',
188
- _asset: {
189
- id: '12345678-1234-1234-1234-123456789abc',
190
- url: '/test.jpg',
191
- width: 1920,
192
- },
193
- },
194
- };
195
-
196
- enrichBlockWithSignedUrls(content);
197
-
198
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
199
- });
200
-
201
- it('requires _asset.id to be a 36-character UUID', () => {
202
- const content = {
203
- notAnImage: {
204
- alt: 'Test',
205
- _asset: {
206
- id: 'short-id', // Not 36 characters
207
- url: '/test.jpg',
208
- width: 1920,
209
- height: 1080,
210
- },
211
- },
212
- };
213
-
214
- enrichBlockWithSignedUrls(content);
215
-
216
- expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
217
- });
218
- });
219
-
220
- describe('Recursive Traversal', () => {
221
- it('traverses nested objects', () => {
222
- const imageRef = createResolvedImageRef();
223
- const content = {
224
- section: {
225
- hero: {
226
- backgroundImage: imageRef,
227
- },
228
- },
229
- };
230
-
231
- const result = enrichBlockWithSignedUrls(content);
232
-
233
- expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
234
- expect(result?.section.hero.backgroundImage._signed).toBeDefined();
235
- });
236
-
237
- it('traverses arrays', () => {
238
- const imageRef1 = createResolvedImageRef('11111111-1111-1111-1111-111111111111');
239
- const imageRef2 = createResolvedImageRef('22222222-2222-2222-2222-222222222222');
240
-
241
- const content = {
242
- images: [imageRef1, imageRef2],
243
- };
244
-
245
- const result = enrichBlockWithSignedUrls(content);
246
-
247
- expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(2);
248
- expect(result?.images[0]._signed).toBeDefined();
249
- expect(result?.images[1]._signed).toBeDefined();
250
- });
251
-
252
- it('traverses deeply nested arrays within objects', () => {
253
- const imageRef = createResolvedImageRef();
254
- const content = {
255
- sections: [
256
- {
257
- cards: [
258
- {
259
- thumbnail: imageRef,
260
- },
261
- ],
262
- },
263
- ],
264
- };
265
-
266
- const result = enrichBlockWithSignedUrls(content);
267
-
268
- expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
269
- expect(result?.sections[0].cards[0].thumbnail._signed).toBeDefined();
270
- });
271
-
272
- it('handles multiple image references at different levels', () => {
273
- const heroImage = createResolvedImageRef('11111111-1111-1111-1111-111111111111');
274
- const thumbnailImage = createResolvedImageRef('22222222-2222-2222-2222-222222222222');
275
-
276
- const content = {
277
- hero: heroImage,
278
- sidebar: {
279
- thumbnail: thumbnailImage,
280
- },
281
- };
282
-
283
- const result = enrichBlockWithSignedUrls(content);
284
-
285
- expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(2);
286
- expect(result?.hero._signed).toBeDefined();
287
- expect(result?.sidebar.thumbnail._signed).toBeDefined();
288
- });
289
- });
290
-
291
- describe('Signed URL Data', () => {
292
- it('adds _signed with signedSrcset, signedFallbackSrcset, and signedDefaultSrc', () => {
293
- const imageRef = createResolvedImageRef();
294
- const content = { image: imageRef };
295
-
296
- const result = enrichBlockWithSignedUrls(content);
297
-
298
- expect(result?.image._signed).toEqual({
299
- signedSrcset: 'signed-srcset-asset-123',
300
- signedFallbackSrcset: 'signed-fallback-srcset-asset-123',
301
- signedDefaultSrc: 'signed-default-src-asset-123',
302
- });
303
- });
304
-
305
- it('passes correct config to generateSignedImageData', () => {
306
- const imageRef = createResolvedImageRef();
307
- const content = { image: imageRef };
308
-
309
- enrichBlockWithSignedUrls(content);
310
-
311
- expect(mockGenerateSignedImageData).toHaveBeenCalledWith(
312
- {
313
- assetId: 'asset-123',
314
- width: 1920,
315
- height: 1080,
316
- url: '/api/dev/storage/test.jpg',
317
- },
318
- expect.objectContaining({})
319
- );
320
- });
321
-
322
- it('passes focalPoint option when transformation includes hx/hy', () => {
323
- const imageRef = {
324
- ...createResolvedImageRef(),
325
- _asset: {
326
- ...createResolvedImageRef()._asset,
327
- transformation: 'hx=0.3&hy=0.7',
328
- },
329
- };
330
- const content = { image: imageRef };
331
-
332
- enrichBlockWithSignedUrls(content);
333
-
334
- expect(mockGenerateSignedImageData).toHaveBeenCalledWith(expect.objectContaining({}), {
335
- focalPoint: { x: 0.3, y: 0.7 },
336
- });
337
- });
338
- });
339
-
340
- describe('Immutability', () => {
341
- it('does not mutate the original object', () => {
342
- const imageRef = createResolvedImageRef();
343
- const content = { image: imageRef };
344
- const originalContent = JSON.stringify(content);
345
-
346
- enrichBlockWithSignedUrls(content);
347
-
348
- expect(JSON.stringify(content)).toBe(originalContent);
349
- expect((content.image as Record<string, unknown>)._signed).toBeUndefined();
350
- });
351
-
352
- it('returns a new object with signed data', () => {
353
- const imageRef = createResolvedImageRef();
354
- const content = { image: imageRef };
355
-
356
- const result = enrichBlockWithSignedUrls(content);
357
-
358
- expect(result).not.toBe(content);
359
- expect(result?.image).not.toBe(content.image);
360
- });
361
- });
362
-
363
- describe('Primitive Values', () => {
364
- it('preserves primitive values', () => {
365
- const content = {
366
- title: 'Hello World',
367
- count: 42,
368
- enabled: true,
369
- ratio: 3.14,
370
- };
371
-
372
- const result = enrichBlockWithSignedUrls(content);
373
-
374
- expect(result).toEqual(content);
375
- });
376
-
377
- it('preserves nested primitive values alongside images', () => {
378
- const imageRef = createResolvedImageRef();
379
- const content = {
380
- title: 'Hero Section',
381
- order: 1,
382
- visible: true,
383
- image: imageRef,
384
- };
385
-
386
- const result = enrichBlockWithSignedUrls(content);
387
-
388
- expect(result?.title).toBe('Hero Section');
389
- expect(result?.order).toBe(1);
390
- expect(result?.visible).toBe(true);
391
- expect(result?.image._signed).toBeDefined();
392
- });
393
- });
394
- });
@@ -1,60 +0,0 @@
1
- /**
2
- * Block Renderer Component
3
- *
4
- * Dispatches block data to the appropriate component using the ComponentMap pattern.
5
- * This is the main entry point for rendering blocks from the CMS.
6
- */
7
-
8
- import type { BlockComponentRegistry, BlockData } from './types';
9
-
10
- // -----------------------------------------------------------------------------
11
- // Props
12
- // -----------------------------------------------------------------------------
13
-
14
- interface BlockRendererProps {
15
- /**
16
- * The block data to render.
17
- * Must have a `type` field that maps to a registered component.
18
- */
19
- block: BlockData;
20
- registry: Partial<BlockComponentRegistry>;
21
- }
22
-
23
- // -----------------------------------------------------------------------------
24
- // Component
25
- // -----------------------------------------------------------------------------
26
-
27
- /**
28
- * Renders a single block by dispatching to the appropriate component.
29
- *
30
- * Uses the ComponentMap pattern: the block's `type` field determines which
31
- * component renders the block's `content`.
32
- *
33
- * @example
34
- * ```tsx
35
- * // Render a single block
36
- * <BlockRenderer block={{ type: 'header', content: { headline: 'Hello' } }} />
37
- *
38
- * // Render an array of blocks
39
- * {page.blocks.map((block, index) => (
40
- * <BlockRenderer key={index} block={block} />
41
- * ))}
42
- * ```
43
- */
44
- export function BlockRenderer({ block, registry }: BlockRendererProps) {
45
- const Component = registry[block.type];
46
-
47
- if (!Component) {
48
- // Log warning in development, render nothing in production
49
- if (process.env.NODE_ENV === 'development') {
50
- console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);
51
- }
52
- return null;
53
- }
54
-
55
- // TypeScript cannot narrow the content type through the component lookup,
56
- // so we use a type assertion here. Runtime safety is guaranteed by the
57
- // discriminated union and the blockComponents registry.
58
- // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union
59
- return <Component content={block.content as any} />;
60
- }
package/lib/cms-api.ts DELETED
@@ -1,86 +0,0 @@
1
- /**
2
- * CMS API Client
3
- *
4
- * Creates an HTTP-based tRPC client for calling the CMS API.
5
- * Used in Server Components to fetch routes and blocks from the CMS.
6
- */
7
-
8
- import type { AppRouter } from '@repo/cms-schema/trpc';
9
- import { type CreateTRPCClient, createTRPCClient, httpBatchLink } from '@trpc/client';
10
- import { cache } from 'react';
11
- import superjson from 'superjson';
12
-
13
- /** Type alias for the CMS API client */
14
- type CmsClient = CreateTRPCClient<AppRouter>;
15
-
16
- /**
17
- * Get the CMS API URL from environment variables.
18
- * Falls back to localhost:3000 for development.
19
- */
20
- function getCmsApiUrl(): string {
21
- const cmsUrl = process.env.NEXT_PUBLIC_CMS_URL || 'http://localhost:3000';
22
- return `${cmsUrl}/api/trpc`;
23
- }
24
-
25
- /** Options for creating a CMS client */
26
- export interface CmsClientOptions {
27
- /** API key for authentication (passed as query parameter) */
28
- apiKey?: string;
29
- }
30
-
31
- /**
32
- * Create a custom fetch function that appends API key as query parameter.
33
- */
34
- function createFetchWithApiKey(apiKey?: string) {
35
- return async (url: URL | RequestInfo, options?: RequestInit): Promise<Response> => {
36
- let finalUrl = url;
37
-
38
- // Append api_key to URL if provided
39
- if (apiKey) {
40
- const urlObj = new URL(url.toString());
41
- urlObj.searchParams.set('api_key', apiKey);
42
- finalUrl = urlObj.toString();
43
- }
44
-
45
- const response = await fetch(finalUrl, options);
46
-
47
- return response;
48
- };
49
- }
50
-
51
- /**
52
- * Create a tRPC client for the CMS API.
53
- * Client is created lazily to ensure environment variables are available at runtime.
54
- */
55
- function createCmsClient(options?: CmsClientOptions): CmsClient {
56
- const url = getCmsApiUrl();
57
- console.log('[CMS API] Creating client with URL:', url);
58
-
59
- return createTRPCClient<AppRouter>({
60
- links: [
61
- httpBatchLink({
62
- url,
63
- transformer: superjson,
64
- fetch: createFetchWithApiKey(options?.apiKey),
65
- }),
66
- ],
67
- });
68
- }
69
-
70
- /**
71
- * Get a CMS client with optional API key.
72
- * Uses React's cache() to dedupe requests within a single render.
73
- */
74
- export function getCmsClient(options?: CmsClientOptions): CmsClient {
75
- // If no API key, use the cached default client
76
- if (!options?.apiKey) {
77
- return getDefaultCmsClient();
78
- }
79
- // With API key, create a new client (keys are request-specific)
80
- return createCmsClient(options);
81
- }
82
-
83
- /**
84
- * Cached default CMS client (no auth) for public requests.
85
- */
86
- const getDefaultCmsClient = cache(() => createCmsClient());