@se-studio/contentful-rest-api 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 +467 -0
- package/dist/index.d.ts +462 -0
- package/dist/index.js +1435 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# @se-studio/contentful-rest-api
|
|
2
|
+
|
|
3
|
+
Type-safe Contentful REST API client with caching, rate limiting, and extensible converters for Next.js applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Type-safe** - Full TypeScript support with generated types from your Contentful space
|
|
8
|
+
- 🔄 **Dual API Support** - Content Delivery API (CDA) and Content Preview API (CPA)
|
|
9
|
+
- ⚡ **Next.js Optimized** - Built-in support for Next.js App Router cache tags and revalidation
|
|
10
|
+
- 🔁 **Retry Logic** - Automatic retry with exponential backoff for failed requests
|
|
11
|
+
- 🚦 **Rate Limiting** - Respect Contentful API rate limits with built-in rate limiter
|
|
12
|
+
- 🧩 **Extensible Converters** - Functional composition pattern for customizing content transformations
|
|
13
|
+
- 🛡️ **Error Handling** - Custom error types for different failure scenarios
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @se-studio/contentful-rest-api @se-studio/core-data-types contentful
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
|
|
27
|
+
|
|
28
|
+
// Fetch a page by slug
|
|
29
|
+
const page = await contentfulPageRest(
|
|
30
|
+
{
|
|
31
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
32
|
+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
|
|
33
|
+
environment: 'master'
|
|
34
|
+
},
|
|
35
|
+
'home',
|
|
36
|
+
{
|
|
37
|
+
locale: 'en-US',
|
|
38
|
+
cache: {
|
|
39
|
+
tags: ['home-page'],
|
|
40
|
+
revalidate: 3600 // 1 hour
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Preview Mode
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
|
|
50
|
+
|
|
51
|
+
// Use preview API for draft content
|
|
52
|
+
const page = await contentfulPageRest(
|
|
53
|
+
{
|
|
54
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
55
|
+
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
|
56
|
+
},
|
|
57
|
+
'home',
|
|
58
|
+
{
|
|
59
|
+
preview: true, // Uses Preview API
|
|
60
|
+
cache: { cache: 'no-store' } // Don't cache preview content
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### Client Functions
|
|
68
|
+
|
|
69
|
+
#### `createContentfulClient(config)`
|
|
70
|
+
|
|
71
|
+
Creates a Contentful Content Delivery API (CDA) client.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { createContentfulClient } from '@se-studio/contentful-rest-api';
|
|
75
|
+
|
|
76
|
+
const client = createContentfulClient({
|
|
77
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
78
|
+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
|
|
79
|
+
environment: 'master'
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `createContentfulPreviewClient(config)`
|
|
84
|
+
|
|
85
|
+
Creates a Contentful Content Preview API (CPA) client.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createContentfulPreviewClient } from '@se-studio/contentful-rest-api';
|
|
89
|
+
|
|
90
|
+
const previewClient = createContentfulPreviewClient({
|
|
91
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
92
|
+
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Content Fetching Functions
|
|
97
|
+
|
|
98
|
+
#### `contentfulPageRest(config, slug, options?, converter?)`
|
|
99
|
+
|
|
100
|
+
Fetches a page by slug.
|
|
101
|
+
|
|
102
|
+
**Parameters:**
|
|
103
|
+
- `config`: ContentfulConfig - Contentful client configuration
|
|
104
|
+
- `slug`: string - Page slug to fetch
|
|
105
|
+
- `options?`: FetchOptions - Optional fetch options (locale, preview, cache, retry)
|
|
106
|
+
- `converter?`: Converter - Optional custom converter function
|
|
107
|
+
|
|
108
|
+
**Returns:** `Promise<IPage | null>`
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const page = await contentfulPageRest(
|
|
112
|
+
config,
|
|
113
|
+
'about-us',
|
|
114
|
+
{
|
|
115
|
+
locale: 'en-US',
|
|
116
|
+
include: 10,
|
|
117
|
+
cache: { tags: ['about-page'], revalidate: 3600 }
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `contentfulPageByIdRest(config, id, options?, converter?)`
|
|
123
|
+
|
|
124
|
+
Fetches a page by entry ID.
|
|
125
|
+
|
|
126
|
+
**Parameters:**
|
|
127
|
+
- `config`: ContentfulConfig
|
|
128
|
+
- `id`: string - Entry ID
|
|
129
|
+
- `options?`: FetchOptions
|
|
130
|
+
- `converter?`: Converter
|
|
131
|
+
|
|
132
|
+
**Returns:** `Promise<IPage>`
|
|
133
|
+
|
|
134
|
+
**Throws:** `EntryNotFoundError` if entry doesn't exist
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const page = await contentfulPageByIdRest(
|
|
138
|
+
config,
|
|
139
|
+
'5nZHNlP9rZhWvKx4w2Z8zB'
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `contentfulAllPagesRest(config, options?, converter?)`
|
|
144
|
+
|
|
145
|
+
Fetches all pages from Contentful.
|
|
146
|
+
|
|
147
|
+
**Parameters:**
|
|
148
|
+
- `config`: ContentfulConfig
|
|
149
|
+
- `options?`: FetchOptions
|
|
150
|
+
- `converter?`: Converter
|
|
151
|
+
|
|
152
|
+
**Returns:** `Promise<IPage[]>`
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const pages = await contentfulAllPagesRest(
|
|
156
|
+
config,
|
|
157
|
+
{
|
|
158
|
+
locale: 'en-US',
|
|
159
|
+
cache: { tags: ['all-pages'], revalidate: 3600 }
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `contentfulEntryRest<TEntry, TResult>(config, id, converter, options?)`
|
|
165
|
+
|
|
166
|
+
Generic function to fetch any entry type with a custom converter.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const article = await contentfulEntryRest(
|
|
170
|
+
config,
|
|
171
|
+
'articleId123',
|
|
172
|
+
myArticleConverter,
|
|
173
|
+
{ locale: 'en-US' }
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Converter Pattern
|
|
178
|
+
|
|
179
|
+
The package uses a functional composition pattern for converting Contentful entries to your domain types.
|
|
180
|
+
|
|
181
|
+
### Base Converter
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { basePageConverter } from '@se-studio/contentful-rest-api';
|
|
185
|
+
|
|
186
|
+
// Use the base converter
|
|
187
|
+
const page = basePageConverter(contentfulEntry);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Enhancers
|
|
191
|
+
|
|
192
|
+
Enhancers are functions that wrap converters to add additional functionality:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import {
|
|
196
|
+
basePageConverter,
|
|
197
|
+
withSEO,
|
|
198
|
+
withSections,
|
|
199
|
+
withTags,
|
|
200
|
+
compose
|
|
201
|
+
} from '@se-studio/contentful-rest-api';
|
|
202
|
+
|
|
203
|
+
// Compose multiple enhancers
|
|
204
|
+
const myConverter = compose(
|
|
205
|
+
withSEO,
|
|
206
|
+
withSections,
|
|
207
|
+
withTags
|
|
208
|
+
)(basePageConverter);
|
|
209
|
+
|
|
210
|
+
// Use with API functions
|
|
211
|
+
const page = await contentfulPageRest(config, 'home', {}, myConverter);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Custom Converters
|
|
215
|
+
|
|
216
|
+
Create your own converter enhancers:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import type { Converter, PageEntrySkeleton } from '@se-studio/contentful-rest-api';
|
|
220
|
+
import type { IPage } from '@se-studio/core-data-types';
|
|
221
|
+
|
|
222
|
+
function withCustomField(
|
|
223
|
+
converter: Converter<PageEntrySkeleton, IPage>
|
|
224
|
+
): Converter<PageEntrySkeleton, IPage> {
|
|
225
|
+
return (entry) => {
|
|
226
|
+
const page = converter(entry);
|
|
227
|
+
|
|
228
|
+
// Add custom logic
|
|
229
|
+
return {
|
|
230
|
+
...page,
|
|
231
|
+
customField: entry.fields.customField || 'default'
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Use it
|
|
237
|
+
const converter = withCustomField(basePageConverter);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Caching
|
|
241
|
+
|
|
242
|
+
### Cache Tags
|
|
243
|
+
|
|
244
|
+
The package provides utilities for generating cache tags for Next.js:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { generateCacheTags, getPageCacheTags } from '@se-studio/contentful-rest-api';
|
|
248
|
+
|
|
249
|
+
// Generate tags for a content type
|
|
250
|
+
const tags = generateCacheTags('page', 'entry-id');
|
|
251
|
+
// Returns: ['contentful', 'contentful:page', 'contentful:page:entry-id']
|
|
252
|
+
|
|
253
|
+
// Get page-specific cache tags
|
|
254
|
+
const pageTags = getPageCacheTags('home', 'en-US');
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Cache Revalidation
|
|
258
|
+
|
|
259
|
+
Revalidate cache tags in Server Actions or Route Handlers:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
'use server'
|
|
263
|
+
|
|
264
|
+
import { revalidateTags } from '@se-studio/contentful-rest-api';
|
|
265
|
+
|
|
266
|
+
export async function revalidateHomePage() {
|
|
267
|
+
await revalidateTags(['contentful:page:home']);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Error Handling
|
|
272
|
+
|
|
273
|
+
The package provides custom error types for different scenarios:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import {
|
|
277
|
+
ContentfulError,
|
|
278
|
+
RateLimitError,
|
|
279
|
+
EntryNotFoundError,
|
|
280
|
+
AuthenticationError,
|
|
281
|
+
ValidationError,
|
|
282
|
+
isRetryableError
|
|
283
|
+
} from '@se-studio/contentful-rest-api';
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const page = await contentfulPageByIdRest(config, 'invalid-id');
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error instanceof EntryNotFoundError) {
|
|
289
|
+
console.log('Entry not found:', error.entryId);
|
|
290
|
+
} else if (error instanceof RateLimitError) {
|
|
291
|
+
console.log('Rate limited, retry after:', error.retryAfter);
|
|
292
|
+
} else if (isRetryableError(error)) {
|
|
293
|
+
// Handle retryable errors
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Retry Configuration
|
|
299
|
+
|
|
300
|
+
Configure retry behavior for resilient API calls:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
const page = await contentfulPageRest(
|
|
304
|
+
config,
|
|
305
|
+
'home',
|
|
306
|
+
{
|
|
307
|
+
retry: {
|
|
308
|
+
maxRetries: 3,
|
|
309
|
+
initialDelay: 1000,
|
|
310
|
+
maxDelay: 30000,
|
|
311
|
+
backoffMultiplier: 2
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Rate Limiting
|
|
318
|
+
|
|
319
|
+
Use the built-in rate limiter to control request rates:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { RateLimiter } from '@se-studio/contentful-rest-api';
|
|
323
|
+
|
|
324
|
+
// Create a rate limiter (5 requests per second)
|
|
325
|
+
const limiter = new RateLimiter(5, 1);
|
|
326
|
+
|
|
327
|
+
// Consume a token before making a request
|
|
328
|
+
await limiter.consume();
|
|
329
|
+
const page = await contentfulPageRest(config, 'home');
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Type Generation
|
|
333
|
+
|
|
334
|
+
Generate TypeScript types from your Contentful space:
|
|
335
|
+
|
|
336
|
+
### 1. Configure Environment Variables
|
|
337
|
+
|
|
338
|
+
Create a `.env.local` file:
|
|
339
|
+
|
|
340
|
+
```env
|
|
341
|
+
CONTENTFUL_SPACE_ID=your-space-id
|
|
342
|
+
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
|
|
343
|
+
CONTENTFUL_ENVIRONMENT_NAME=master
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 2. Generate Types
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
pnpm --filter @se-studio/contentful-rest-api generate:types
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
This creates `src/generated/contentful-types.ts` with types matching your Contentful content model.
|
|
353
|
+
|
|
354
|
+
### 3. Use Generated Types
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import type { TypePageSkeleton } from './generated/contentful-types';
|
|
358
|
+
import { contentfulEntryRest } from '@se-studio/contentful-rest-api';
|
|
359
|
+
|
|
360
|
+
const page = await client.getEntry<TypePageSkeleton>('entry-id');
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Configuration Options
|
|
364
|
+
|
|
365
|
+
### ContentfulConfig
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
interface ContentfulConfig {
|
|
369
|
+
spaceId: string;
|
|
370
|
+
accessToken: string;
|
|
371
|
+
environment?: string; // defaults to 'master'
|
|
372
|
+
host?: string; // for proxies or custom endpoints
|
|
373
|
+
options?: Partial<CreateClientParams>;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### FetchOptions
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
interface FetchOptions {
|
|
381
|
+
locale?: string;
|
|
382
|
+
preview?: boolean;
|
|
383
|
+
include?: number; // include depth for linked entries (default: 10)
|
|
384
|
+
cache?: CacheConfig;
|
|
385
|
+
retry?: RetryConfig;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### CacheConfig
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
interface CacheConfig {
|
|
393
|
+
tags?: string[]; // Next.js cache tags
|
|
394
|
+
revalidate?: number | false; // ISR revalidation time in seconds
|
|
395
|
+
cache?: 'force-cache' | 'no-store';
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### RetryConfig
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
interface RetryConfig {
|
|
403
|
+
maxRetries?: number; // default: 3
|
|
404
|
+
initialDelay?: number; // default: 1000ms
|
|
405
|
+
maxDelay?: number; // default: 30000ms
|
|
406
|
+
backoffMultiplier?: number; // default: 2
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Next.js App Router Integration
|
|
411
|
+
|
|
412
|
+
Example usage in a Next.js Server Component:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// app/[slug]/page.tsx
|
|
416
|
+
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
|
|
417
|
+
|
|
418
|
+
interface PageProps {
|
|
419
|
+
params: { slug: string };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default async function Page({ params }: PageProps) {
|
|
423
|
+
const page = await contentfulPageRest(
|
|
424
|
+
{
|
|
425
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
426
|
+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
|
|
427
|
+
},
|
|
428
|
+
params.slug,
|
|
429
|
+
{
|
|
430
|
+
cache: {
|
|
431
|
+
tags: [`page:${params.slug}`],
|
|
432
|
+
revalidate: 3600
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
if (!page) {
|
|
438
|
+
notFound();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<div>
|
|
443
|
+
<h1>{page.title}</h1>
|
|
444
|
+
<p>{page.description}</p>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function generateStaticParams() {
|
|
450
|
+
const pages = await contentfulAllPagesRest({
|
|
451
|
+
spaceId: process.env.CONTENTFUL_SPACE_ID!,
|
|
452
|
+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return pages.map((page) => ({
|
|
456
|
+
slug: page.slug,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## License
|
|
462
|
+
|
|
463
|
+
MIT
|
|
464
|
+
|
|
465
|
+
## Repository
|
|
466
|
+
|
|
467
|
+
https://github.com/Something-Else-Studio/se-core-product
|