@content-streamline/nextjs 0.0.9 → 0.0.11
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 +97 -14
- package/dist/api/client.d.ts +15 -5
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +26 -10
- package/dist/components/PostsList.d.ts.map +1 -1
- package/dist/components/PostsList.js +7 -7
- package/dist/components/styles.css +17 -9
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/server/index.d.ts +97 -26
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +99 -133
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Next.js library for Content Streamline API with reusable, responsive components.
|
|
|
6
6
|
|
|
7
7
|
- 🚀 **Easy Setup** - Configure via environment variables
|
|
8
8
|
- 📦 **Reusable Components** - Pre-built React components for posts
|
|
9
|
-
- 💾 **
|
|
9
|
+
- 💾 **Next.js Native Caching** - Uses `fetch()` with `revalidate` for ISR support
|
|
10
|
+
- 🔄 **On-Demand Revalidation** - Cache tags for targeted refresh via `revalidateTag()`
|
|
10
11
|
- 🎨 **Customizable** - Responsive components with configurable props
|
|
11
12
|
- 📱 **Mobile-Friendly** - Built-in responsive design
|
|
12
13
|
- 🔧 **TypeScript** - Full TypeScript support
|
|
@@ -55,7 +56,7 @@ import '@content-streamline/nextjs/components/styles.css';
|
|
|
55
56
|
import { getAllPosts } from '@content-streamline/nextjs/server';
|
|
56
57
|
import { LatestPosts, PostsList } from '@content-streamline/nextjs/components';
|
|
57
58
|
|
|
58
|
-
export const revalidate =
|
|
59
|
+
export const revalidate = 300; // Revalidate every 5 minutes (ISR)
|
|
59
60
|
|
|
60
61
|
export default async function BlogPage() {
|
|
61
62
|
const posts = await getAllPosts({ platform: 'blog' });
|
|
@@ -133,7 +134,7 @@ export const getStaticProps: GetStaticProps = async () => {
|
|
|
133
134
|
|
|
134
135
|
return {
|
|
135
136
|
props: { posts },
|
|
136
|
-
revalidate:
|
|
137
|
+
revalidate: 300, // Revalidate every 5 minutes
|
|
137
138
|
};
|
|
138
139
|
};
|
|
139
140
|
|
|
@@ -182,7 +183,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
|
|
|
182
183
|
|
|
183
184
|
return {
|
|
184
185
|
props: { post },
|
|
185
|
-
revalidate:
|
|
186
|
+
revalidate: 300,
|
|
186
187
|
};
|
|
187
188
|
};
|
|
188
189
|
|
|
@@ -197,15 +198,16 @@ export default function PostPage({ post }: { post: PostDetailResponse }) {
|
|
|
197
198
|
|
|
198
199
|
#### `getAllPosts(options?)`
|
|
199
200
|
|
|
200
|
-
Get all posts with
|
|
201
|
+
Get all posts with Next.js caching (ISR-compatible).
|
|
201
202
|
|
|
202
203
|
```tsx
|
|
203
204
|
import { getAllPosts } from '@content-streamline/nextjs/server';
|
|
204
205
|
|
|
205
206
|
const posts = await getAllPosts({
|
|
206
207
|
platform: 'blog', // Filter by platform (default: undefined = all)
|
|
207
|
-
|
|
208
|
-
forceRefresh: false, //
|
|
208
|
+
revalidate: 300, // Cache TTL in seconds (default: 300 = 5 min)
|
|
209
|
+
forceRefresh: false, // Bypass cache (default: false)
|
|
210
|
+
tags: ['custom-tag'], // Additional cache tags for revalidateTag()
|
|
209
211
|
});
|
|
210
212
|
```
|
|
211
213
|
|
|
@@ -221,8 +223,9 @@ import { getPost } from '@content-streamline/nextjs/server';
|
|
|
221
223
|
const post = await getPost(123, {
|
|
222
224
|
language: 'en',
|
|
223
225
|
contentFormat: 'markdown', // or 'html'
|
|
224
|
-
|
|
226
|
+
revalidate: 300, // Cache TTL in seconds (default: 300)
|
|
225
227
|
forceRefresh: false,
|
|
228
|
+
tags: ['custom-tag'],
|
|
226
229
|
});
|
|
227
230
|
```
|
|
228
231
|
|
|
@@ -237,11 +240,24 @@ const post = await getPostBySlug('my-post-slug', {
|
|
|
237
240
|
language: 'en',
|
|
238
241
|
contentFormat: 'markdown',
|
|
239
242
|
platform: 'blog',
|
|
240
|
-
|
|
243
|
+
revalidate: 300,
|
|
241
244
|
forceRefresh: false,
|
|
242
245
|
});
|
|
243
246
|
```
|
|
244
247
|
|
|
248
|
+
#### `getCacheTags()`
|
|
249
|
+
|
|
250
|
+
Get cache tag names for use with `revalidateTag()`.
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
import { getCacheTags } from '@content-streamline/nextjs/server';
|
|
254
|
+
|
|
255
|
+
const tags = getCacheTags();
|
|
256
|
+
// tags.all → 'content-streamline' (all content)
|
|
257
|
+
// tags.postsList → 'posts-list' (post listings)
|
|
258
|
+
// tags.post(123) → 'post-123' (specific post)
|
|
259
|
+
```
|
|
260
|
+
|
|
245
261
|
### Components
|
|
246
262
|
|
|
247
263
|
#### `<LatestPosts />`
|
|
@@ -443,17 +459,81 @@ import { PostDetail } from '@content-streamline/nextjs/components';
|
|
|
443
459
|
|
|
444
460
|
## Caching
|
|
445
461
|
|
|
446
|
-
The library
|
|
462
|
+
The library uses **Next.js native fetch caching** for optimal performance with ISR (Incremental Static Regeneration).
|
|
463
|
+
|
|
464
|
+
### How It Works
|
|
465
|
+
|
|
466
|
+
- **Default TTL**: 5 minutes (300 seconds)
|
|
467
|
+
- **Automatic revalidation**: Next.js refreshes cache in background after TTL expires
|
|
468
|
+
- **Cache tags**: Each request is tagged for targeted invalidation
|
|
469
|
+
- **Works with static generation**: Pages can be statically generated at build time
|
|
470
|
+
|
|
471
|
+
### Cache Tags
|
|
447
472
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
473
|
+
Every request is tagged for granular cache control:
|
|
474
|
+
|
|
475
|
+
| Tag | Description |
|
|
476
|
+
|-----|-------------|
|
|
477
|
+
| `content-streamline` | All content from this library |
|
|
478
|
+
| `posts-list` | Post listing requests |
|
|
479
|
+
| `post-{id}` | Individual post by ID (e.g., `post-123`) |
|
|
480
|
+
|
|
481
|
+
### Automatic Revalidation (Time-Based)
|
|
482
|
+
|
|
483
|
+
Cache automatically refreshes after the TTL expires:
|
|
451
484
|
|
|
452
|
-
To force refresh:
|
|
453
485
|
```tsx
|
|
486
|
+
// 5 minute cache (default)
|
|
487
|
+
const posts = await getAllPosts();
|
|
488
|
+
|
|
489
|
+
// 1 minute cache
|
|
490
|
+
const posts = await getAllPosts({ revalidate: 60 });
|
|
491
|
+
|
|
492
|
+
// 1 hour cache
|
|
493
|
+
const posts = await getAllPosts({ revalidate: 3600 });
|
|
494
|
+
|
|
495
|
+
// No caching (always fresh)
|
|
454
496
|
const posts = await getAllPosts({ forceRefresh: true });
|
|
455
497
|
```
|
|
456
498
|
|
|
499
|
+
### On-Demand Revalidation
|
|
500
|
+
|
|
501
|
+
Invalidate cache instantly when content changes (e.g., from a webhook):
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
// app/api/revalidate/route.ts
|
|
505
|
+
import { revalidateTag } from 'next/cache';
|
|
506
|
+
import { getCacheTags } from '@content-streamline/nextjs';
|
|
507
|
+
|
|
508
|
+
export async function POST(request: Request) {
|
|
509
|
+
const { postId } = await request.json();
|
|
510
|
+
|
|
511
|
+
// Revalidate specific post
|
|
512
|
+
if (postId) {
|
|
513
|
+
revalidateTag(getCacheTags().post(postId));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Or revalidate all posts
|
|
517
|
+
revalidateTag(getCacheTags().postsList);
|
|
518
|
+
|
|
519
|
+
return Response.json({ revalidated: true });
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Static Generation with ISR
|
|
524
|
+
|
|
525
|
+
For fully static pages with background revalidation:
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
// app/blog/page.tsx
|
|
529
|
+
export const revalidate = 300; // Revalidate page every 5 minutes
|
|
530
|
+
|
|
531
|
+
export default async function BlogPage() {
|
|
532
|
+
const posts = await getAllPosts({ platform: 'blog' });
|
|
533
|
+
return <PostsList posts={posts} />;
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
457
537
|
## TypeScript
|
|
458
538
|
|
|
459
539
|
Full TypeScript support is included:
|
|
@@ -462,6 +542,9 @@ Full TypeScript support is included:
|
|
|
462
542
|
import type {
|
|
463
543
|
Post,
|
|
464
544
|
PostDetailResponse,
|
|
545
|
+
GetAllPostsOptions,
|
|
546
|
+
GetPostOptions,
|
|
547
|
+
CacheOptions,
|
|
465
548
|
PostsListProps,
|
|
466
549
|
LatestPostsProps,
|
|
467
550
|
PostDetailProps,
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content Streamline Public API Client
|
|
3
|
+
* Uses Next.js fetch caching for optimal performance
|
|
3
4
|
*/
|
|
4
5
|
import type { Post, PostDetailResponse, PostsResponse } from '../types/index.js';
|
|
6
|
+
export interface CacheOptions {
|
|
7
|
+
/** Revalidation time in seconds (default: 300 = 5 minutes) */
|
|
8
|
+
revalidate?: number;
|
|
9
|
+
/** Cache tags for on-demand revalidation */
|
|
10
|
+
tags?: string[];
|
|
11
|
+
/** Force fresh data, bypassing cache */
|
|
12
|
+
forceRefresh?: boolean;
|
|
13
|
+
}
|
|
5
14
|
declare class ContentStreamlineAPI {
|
|
6
15
|
private baseUrl;
|
|
7
16
|
private accessToken;
|
|
8
|
-
|
|
17
|
+
private defaultRevalidate;
|
|
18
|
+
constructor(baseUrl: string, accessToken: string, defaultRevalidate?: number);
|
|
9
19
|
private fetch;
|
|
10
20
|
/**
|
|
11
21
|
* List all posts with pagination
|
|
12
22
|
*/
|
|
13
|
-
listPosts(page?: number, platform?: string): Promise<PostsResponse>;
|
|
23
|
+
listPosts(page?: number, platform?: string, cacheOptions?: CacheOptions): Promise<PostsResponse>;
|
|
14
24
|
/**
|
|
15
25
|
* Get all posts by fetching all pages
|
|
16
26
|
*/
|
|
17
|
-
getAllPosts(platform?: string): Promise<Post[]>;
|
|
27
|
+
getAllPosts(platform?: string, cacheOptions?: CacheOptions): Promise<Post[]>;
|
|
18
28
|
/**
|
|
19
29
|
* Get a single post by ID
|
|
20
30
|
*/
|
|
21
|
-
getPost(id: number, contentFormat?: 'markdown' | 'html'): Promise<PostDetailResponse>;
|
|
31
|
+
getPost(id: number, contentFormat?: 'markdown' | 'html', cacheOptions?: CacheOptions): Promise<PostDetailResponse>;
|
|
22
32
|
}
|
|
23
|
-
export declare function createAPIClient(baseUrl: string, accessToken: string): ContentStreamlineAPI;
|
|
33
|
+
export declare function createAPIClient(baseUrl: string, accessToken: string, defaultRevalidate?: number): ContentStreamlineAPI;
|
|
24
34
|
export {};
|
|
25
35
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,IAAI,EACJ,kBAAkB,EAClB,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,cAAM,oBAAoB;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAY;YAMnE,KAAK;IAwEnB;;OAEG;IACG,SAAS,CACb,IAAI,GAAE,MAAU,EAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,YAAY,GAAE,YAAiB,GAC9B,OAAO,CAAC,aAAa,CAAC;IAezB;;OAEG;IACG,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,YAAY,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA4BtF;;OAEG;IACG,OAAO,CACX,EAAE,EAAE,MAAM,EACV,aAAa,GAAE,UAAU,GAAG,MAAmB,EAC/C,YAAY,GAAE,YAAiB,GAC9B,OAAO,CAAC,kBAAkB,CAAC;CAS/B;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,iBAAiB,GAAE,MAAY,GAC9B,oBAAoB,CAEtB"}
|
package/dist/api/client.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content Streamline Public API Client
|
|
3
|
+
* Uses Next.js fetch caching for optimal performance
|
|
3
4
|
*/
|
|
4
5
|
class ContentStreamlineAPI {
|
|
5
|
-
constructor(baseUrl, accessToken) {
|
|
6
|
+
constructor(baseUrl, accessToken, defaultRevalidate = 300) {
|
|
6
7
|
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
7
8
|
this.accessToken = accessToken;
|
|
9
|
+
this.defaultRevalidate = defaultRevalidate;
|
|
8
10
|
}
|
|
9
|
-
async fetch(endpoint, options = {}) {
|
|
11
|
+
async fetch(endpoint, options = {}, cacheOptions = {}) {
|
|
10
12
|
const url = `${this.baseUrl}${endpoint}`;
|
|
11
13
|
const headers = {
|
|
12
14
|
'Authorization': `Bearer ${this.accessToken}`,
|
|
13
15
|
'Content-Type': 'application/json',
|
|
14
16
|
...options.headers,
|
|
15
17
|
};
|
|
18
|
+
const { revalidate = this.defaultRevalidate, tags, forceRefresh } = cacheOptions;
|
|
19
|
+
// Build Next.js cache options
|
|
20
|
+
const nextOptions = {};
|
|
21
|
+
if (!forceRefresh) {
|
|
22
|
+
nextOptions.revalidate = revalidate;
|
|
23
|
+
}
|
|
24
|
+
if (tags && tags.length > 0) {
|
|
25
|
+
nextOptions.tags = tags;
|
|
26
|
+
}
|
|
16
27
|
let response;
|
|
17
28
|
try {
|
|
18
29
|
response = await fetch(url, {
|
|
19
30
|
...options,
|
|
20
31
|
headers,
|
|
32
|
+
// Next.js cache configuration
|
|
33
|
+
cache: forceRefresh ? 'no-store' : undefined,
|
|
34
|
+
next: Object.keys(nextOptions).length > 0 ? nextOptions : undefined,
|
|
21
35
|
});
|
|
22
36
|
}
|
|
23
37
|
catch (error) {
|
|
@@ -61,23 +75,24 @@ class ContentStreamlineAPI {
|
|
|
61
75
|
/**
|
|
62
76
|
* List all posts with pagination
|
|
63
77
|
*/
|
|
64
|
-
async listPosts(page = 1, platform) {
|
|
78
|
+
async listPosts(page = 1, platform, cacheOptions = {}) {
|
|
65
79
|
const params = new URLSearchParams();
|
|
66
80
|
params.set('page', page.toString());
|
|
67
81
|
if (platform) {
|
|
68
82
|
params.set('platform', platform);
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
const tags = cacheOptions.tags || ['content-streamline', 'posts-list'];
|
|
85
|
+
return this.fetch(`/public/v1/posts?${params.toString()}`, {}, { ...cacheOptions, tags });
|
|
71
86
|
}
|
|
72
87
|
/**
|
|
73
88
|
* Get all posts by fetching all pages
|
|
74
89
|
*/
|
|
75
|
-
async getAllPosts(platform) {
|
|
90
|
+
async getAllPosts(platform, cacheOptions = {}) {
|
|
76
91
|
const allPosts = [];
|
|
77
92
|
let currentPage = 1;
|
|
78
93
|
let hasMore = true;
|
|
79
94
|
while (hasMore) {
|
|
80
|
-
const response = await this.listPosts(currentPage, platform);
|
|
95
|
+
const response = await this.listPosts(currentPage, platform, cacheOptions);
|
|
81
96
|
// Validate response structure
|
|
82
97
|
if (!response || !Array.isArray(response.data)) {
|
|
83
98
|
throw new Error(`Invalid API response: expected PostsResponse with data array, got ${typeof response}`);
|
|
@@ -96,11 +111,12 @@ class ContentStreamlineAPI {
|
|
|
96
111
|
/**
|
|
97
112
|
* Get a single post by ID
|
|
98
113
|
*/
|
|
99
|
-
async getPost(id, contentFormat = 'markdown') {
|
|
114
|
+
async getPost(id, contentFormat = 'markdown', cacheOptions = {}) {
|
|
100
115
|
const format = contentFormat === 'html' ? 'html' : 'markdown';
|
|
101
|
-
|
|
116
|
+
const tags = cacheOptions.tags || ['content-streamline', `post-${id}`];
|
|
117
|
+
return this.fetch(`/public/v1/posts/${id}?content_format=${format}`, {}, { ...cacheOptions, tags });
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
|
-
export function createAPIClient(baseUrl, accessToken) {
|
|
105
|
-
return new ContentStreamlineAPI(baseUrl, accessToken);
|
|
120
|
+
export function createAPIClient(baseUrl, accessToken, defaultRevalidate = 300) {
|
|
121
|
+
return new ContentStreamlineAPI(baseUrl, accessToken, defaultRevalidate);
|
|
106
122
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostsList.d.ts","sourceRoot":"","sources":["../../src/components/PostsList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IACzF;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;CAC5F;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,OAAkB,EAClB,QAAiB,EACjB,SAAa,EACb,QAAe,EACf,SAAc,EACd,aAAkB,EAClB,QAAe,EACf,WAAkB,EAClB,UAAiB,EACjB,WAAW,EACX,aAAa,GACd,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"PostsList.d.ts","sourceRoot":"","sources":["../../src/components/PostsList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IACzF;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;CAC5F;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,OAAkB,EAClB,QAAiB,EACjB,SAAa,EACb,QAAe,EACf,SAAc,EACd,aAAkB,EAClB,QAAe,EACf,WAAkB,EAClB,UAAiB,EACjB,WAAW,EACX,aAAa,GACd,EAAE,cAAc,2CA8EhB"}
|
|
@@ -17,12 +17,12 @@ export function PostsList({ posts, baseUrl = '/posts', platform = 'blog', skipFi
|
|
|
17
17
|
return null;
|
|
18
18
|
const featuredImage = post.images.find((img) => img.ordering === 0) || post.images[0];
|
|
19
19
|
const date = new Date(post.created_at);
|
|
20
|
-
return (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
return (_jsx("article", { className: `content-streamline-list-item ${itemClassName}`, children: _jsxs("a", { href: `${baseUrl}/${post.id}/${translation.slug}`, className: "content-streamline-list-link", children: [showImages && featuredImage && (_jsx("div", { className: "content-streamline-list-image", children: _jsx("img", { src: featuredImage.thumbnail_url, alt: featuredImage.alt || translation.title, loading: "lazy" }) })), _jsxs("div", { className: "content-streamline-list-content", children: [showDate && (_jsx("time", { className: "content-streamline-list-date", dateTime: post.created_at, children: date.toLocaleDateString('en-US', {
|
|
21
|
+
year: 'numeric',
|
|
22
|
+
month: 'short',
|
|
23
|
+
day: 'numeric',
|
|
24
|
+
}) })), _jsx("h3", { className: "content-streamline-list-title", children: renderTitle ? renderTitle(post, translation) : translation.title }), showExcerpt && translation.meta_description && (_jsx("p", { className: "content-streamline-list-excerpt", children: renderExcerpt
|
|
25
|
+
? renderExcerpt(post, translation)
|
|
26
|
+
: translation.meta_description }))] })] }) }, post.id));
|
|
27
27
|
}) }));
|
|
28
28
|
}
|
|
@@ -191,11 +191,13 @@
|
|
|
191
191
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
.content-streamline-list-
|
|
195
|
-
display:
|
|
194
|
+
.content-streamline-list-link {
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-columns: inherit;
|
|
197
|
+
gap: inherit;
|
|
196
198
|
text-decoration: none;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
color: inherit;
|
|
200
|
+
cursor: pointer;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
.content-streamline-list-image {
|
|
@@ -236,15 +238,11 @@
|
|
|
236
238
|
font-weight: 600;
|
|
237
239
|
margin: 0 0 0.5rem 0;
|
|
238
240
|
line-height: 1.4;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
.content-streamline-list-title a {
|
|
242
241
|
color: #111827;
|
|
243
|
-
text-decoration: none;
|
|
244
242
|
transition: color 0.2s ease;
|
|
245
243
|
}
|
|
246
244
|
|
|
247
|
-
.content-streamline-list-
|
|
245
|
+
.content-streamline-list-link:hover .content-streamline-list-title {
|
|
248
246
|
color: #3b82f6;
|
|
249
247
|
}
|
|
250
248
|
|
|
@@ -511,6 +509,11 @@
|
|
|
511
509
|
padding: 0.75rem;
|
|
512
510
|
}
|
|
513
511
|
|
|
512
|
+
.content-streamline-list-link {
|
|
513
|
+
grid-template-columns: inherit;
|
|
514
|
+
gap: inherit;
|
|
515
|
+
}
|
|
516
|
+
|
|
514
517
|
.content-streamline-list-title {
|
|
515
518
|
font-size: 0.9375rem;
|
|
516
519
|
}
|
|
@@ -545,6 +548,11 @@
|
|
|
545
548
|
gap: 0.75rem;
|
|
546
549
|
}
|
|
547
550
|
|
|
551
|
+
.content-streamline-list-link {
|
|
552
|
+
grid-template-columns: 1fr;
|
|
553
|
+
gap: 0.75rem;
|
|
554
|
+
}
|
|
555
|
+
|
|
548
556
|
.content-streamline-list-image {
|
|
549
557
|
aspect-ratio: 16 / 9;
|
|
550
558
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content Streamline Next.js Library
|
|
3
3
|
* Main entry point
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Next.js native caching with automatic revalidation
|
|
7
|
+
* - Cache tags for on-demand revalidation via revalidateTag()
|
|
8
|
+
* - Static page generation with ISR support
|
|
9
|
+
* - Default 5-minute cache TTL (configurable)
|
|
4
10
|
*/
|
|
5
11
|
export type { Post, PostDetailResponse, PostTranslation, PostImage, PostsResponse, Pagination, CacheMetadata, } from './types/index.js';
|
|
6
|
-
export { getAllPosts, getPost, getPostBySlug, } from './server/index.js';
|
|
12
|
+
export { getAllPosts, getPost, getPostBySlug, getCacheTags, } from './server/index.js';
|
|
13
|
+
export type { GetAllPostsOptions, GetPostOptions, CacheOptions, } from './server/index.js';
|
|
7
14
|
export { clearCache, isAPIConfigured, } from './cache/file-cache.js';
|
|
8
15
|
export { PostsList, PostDetail, LatestPosts, } from './components/index.js';
|
|
9
16
|
export type { PostsListProps, PostDetailProps, LatestPostsProps, } from './components/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,eAAe,EACf,SAAS,EACT,aAAa,EACb,UAAU,EACV,aAAa,GACd,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,WAAW,EACX,OAAO,EACP,aAAa,EACb,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,YAAY,GACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,UAAU,EACV,eAAe,GAChB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,SAAS,EACT,UAAU,EACV,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content Streamline Next.js Library
|
|
3
3
|
* Main entry point
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Next.js native caching with automatic revalidation
|
|
7
|
+
* - Cache tags for on-demand revalidation via revalidateTag()
|
|
8
|
+
* - Static page generation with ISR support
|
|
9
|
+
* - Default 5-minute cache TTL (configurable)
|
|
4
10
|
*/
|
|
5
|
-
// Export server functions
|
|
6
|
-
export { getAllPosts, getPost, getPostBySlug, } from './server/index.js';
|
|
7
|
-
// Export cache utilities
|
|
11
|
+
// Export server functions with Next.js caching
|
|
12
|
+
export { getAllPosts, getPost, getPostBySlug, getCacheTags, } from './server/index.js';
|
|
13
|
+
// Export legacy cache utilities (optional, for advanced use)
|
|
8
14
|
export { clearCache, isAPIConfigured, } from './cache/file-cache.js';
|
|
9
15
|
// Export components
|
|
10
16
|
export { PostsList, PostDetail, LatestPosts, } from './components/index.js';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,44 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side functions for Next.js
|
|
3
|
-
* Use these in Server Components, API Routes, or getServerSideProps
|
|
4
|
-
*/
|
|
5
|
-
import type { Post, PostDetailResponse } from '../types/index.js';
|
|
6
|
-
/**
|
|
7
|
-
* Get all posts (with automatic cache refresh)
|
|
8
|
-
* Use this in Server Components or API Routes
|
|
9
3
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* Uses Next.js native caching via fetch() with revalidate options.
|
|
5
|
+
* This enables:
|
|
6
|
+
* - Static page generation with ISR (Incremental Static Regeneration)
|
|
7
|
+
* - Automatic cache revalidation (default: 5 minutes)
|
|
8
|
+
* - Cache tags for on-demand revalidation via revalidateTag()
|
|
9
|
+
*
|
|
10
|
+
* Cache tags used:
|
|
11
|
+
* - 'content-streamline': All content from this library
|
|
12
|
+
* - 'posts-list': Post listings
|
|
13
|
+
* - 'post-{id}': Individual post by ID
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
import type { Post, PostDetailResponse } from '../types/index.js';
|
|
16
|
+
export interface GetAllPostsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Revalidation time in seconds (default: 300 = 5 minutes)
|
|
19
|
+
* Set to 0 for no caching, or false for infinite cache
|
|
20
|
+
*/
|
|
21
|
+
revalidate?: number | false;
|
|
22
|
+
/** Force fresh data, bypassing cache */
|
|
16
23
|
forceRefresh?: boolean;
|
|
24
|
+
/** Filter by platform: 'x', 'linkedin', 'blog', 'facebook', 'instagram', 'other' */
|
|
17
25
|
platform?: string;
|
|
18
|
-
|
|
26
|
+
/** Additional cache tags for on-demand revalidation */
|
|
27
|
+
tags?: string[];
|
|
28
|
+
}
|
|
19
29
|
/**
|
|
20
|
-
* Get
|
|
21
|
-
*
|
|
30
|
+
* Get all posts with Next.js caching
|
|
31
|
+
*
|
|
32
|
+
* Uses Next.js fetch caching with automatic revalidation.
|
|
33
|
+
* Perfect for static page generation with ISR.
|
|
22
34
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
35
|
+
* @example
|
|
36
|
+
* // In a Server Component or page.tsx
|
|
37
|
+
* const posts = await getAllPosts({ revalidate: 300 }); // 5 min cache
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Force fresh data
|
|
41
|
+
* const posts = await getAllPosts({ forceRefresh: true });
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Revalidate on-demand in an API route
|
|
45
|
+
* import { revalidateTag } from 'next/cache';
|
|
46
|
+
* revalidateTag('posts-list');
|
|
25
47
|
*/
|
|
26
|
-
export declare function
|
|
48
|
+
export declare function getAllPosts(options?: GetAllPostsOptions): Promise<Post[]>;
|
|
49
|
+
export interface GetPostOptions {
|
|
50
|
+
/** Language for translation lookup (default: 'en') */
|
|
27
51
|
language?: string;
|
|
52
|
+
/** Content format: 'markdown' or 'html' (default: 'markdown') */
|
|
28
53
|
contentFormat?: 'markdown' | 'html';
|
|
29
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Revalidation time in seconds (default: 300 = 5 minutes)
|
|
56
|
+
* Set to 0 for no caching, or false for infinite cache
|
|
57
|
+
*/
|
|
58
|
+
revalidate?: number | false;
|
|
59
|
+
/** Force fresh data, bypassing cache */
|
|
30
60
|
forceRefresh?: boolean;
|
|
31
|
-
|
|
61
|
+
/** Additional cache tags for on-demand revalidation */
|
|
62
|
+
tags?: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get a single post by ID with Next.js caching
|
|
66
|
+
*
|
|
67
|
+
* Uses Next.js fetch caching with automatic revalidation.
|
|
68
|
+
* Each post has its own cache tag for targeted revalidation.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // In a Server Component
|
|
72
|
+
* const post = await getPost(123, { contentFormat: 'html' });
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // Revalidate specific post on-demand
|
|
76
|
+
* import { revalidateTag } from 'next/cache';
|
|
77
|
+
* revalidateTag('post-123');
|
|
78
|
+
*/
|
|
79
|
+
export declare function getPost(id: number, options?: GetPostOptions): Promise<PostDetailResponse | null>;
|
|
32
80
|
/**
|
|
33
|
-
* Get post by slug
|
|
34
|
-
*
|
|
81
|
+
* Get post by slug with Next.js caching
|
|
82
|
+
*
|
|
83
|
+
* Finds a post by its URL slug across all posts.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // In a dynamic route [slug]/page.tsx
|
|
87
|
+
* const post = await getPostBySlug(params.slug);
|
|
35
88
|
*/
|
|
36
|
-
export declare function getPostBySlug(slug: string, options?: {
|
|
37
|
-
language?: string;
|
|
38
|
-
contentFormat?: 'markdown' | 'html';
|
|
39
|
-
maxCacheAge?: number;
|
|
40
|
-
forceRefresh?: boolean;
|
|
89
|
+
export declare function getPostBySlug(slug: string, options?: GetPostOptions & {
|
|
41
90
|
platform?: string;
|
|
42
91
|
}): Promise<PostDetailResponse | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Get cache tags for use with revalidateTag()
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Revalidate all content
|
|
97
|
+
* revalidateTag(getCacheTags().all);
|
|
98
|
+
*
|
|
99
|
+
* // Revalidate posts list
|
|
100
|
+
* revalidateTag(getCacheTags().postsList);
|
|
101
|
+
*
|
|
102
|
+
* // Revalidate specific post
|
|
103
|
+
* revalidateTag(getCacheTags().post(123));
|
|
104
|
+
*/
|
|
105
|
+
export declare function getCacheTags(): {
|
|
106
|
+
/** Tag for all content-streamline data */
|
|
107
|
+
all: string;
|
|
108
|
+
/** Tag for posts list */
|
|
109
|
+
postsList: string;
|
|
110
|
+
/** Get tag for a specific post by ID */
|
|
111
|
+
post: (id: number) => string;
|
|
112
|
+
};
|
|
43
113
|
export type { Post, PostDetailResponse, PostTranslation, PostImage, PostsResponse, Pagination, } from '../types/index.js';
|
|
114
|
+
export type { CacheOptions } from '../api/client.js';
|
|
44
115
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAoBlE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5B,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CA2C/E;AAED,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,aAAa,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5B,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAC3B,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAoCpC;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA4BpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY;IAExB,0CAA0C;;IAE1C,yBAAyB;;IAEzB,wCAAwC;eAC7B,MAAM;EAEpB;AAGD,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,eAAe,EACf,SAAS,EACT,aAAa,EACb,UAAU,GACX,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side functions for Next.js
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Uses Next.js native caching via fetch() with revalidate options.
|
|
5
|
+
* This enables:
|
|
6
|
+
* - Static page generation with ISR (Incremental Static Regeneration)
|
|
7
|
+
* - Automatic cache revalidation (default: 5 minutes)
|
|
8
|
+
* - Cache tags for on-demand revalidation via revalidateTag()
|
|
9
|
+
*
|
|
10
|
+
* Cache tags used:
|
|
11
|
+
* - 'content-streamline': All content from this library
|
|
12
|
+
* - 'posts-list': Post listings
|
|
13
|
+
* - 'post-{id}': Individual post by ID
|
|
4
14
|
*/
|
|
5
15
|
import { createAPIClient } from '../api/client.js';
|
|
6
|
-
|
|
16
|
+
/** Default revalidation time: 5 minutes */
|
|
17
|
+
const DEFAULT_REVALIDATE_SECONDS = 300;
|
|
7
18
|
/**
|
|
8
19
|
* Get API configuration from environment variables
|
|
9
20
|
* Returns null if not configured (instead of throwing)
|
|
@@ -17,172 +28,103 @@ function getAPIConfig() {
|
|
|
17
28
|
return { baseUrl, accessToken };
|
|
18
29
|
}
|
|
19
30
|
/**
|
|
20
|
-
* Get
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return config;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get all posts (with automatic cache refresh)
|
|
32
|
-
* Use this in Server Components or API Routes
|
|
31
|
+
* Get all posts with Next.js caching
|
|
32
|
+
*
|
|
33
|
+
* Uses Next.js fetch caching with automatic revalidation.
|
|
34
|
+
* Perfect for static page generation with ISR.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // In a Server Component or page.tsx
|
|
38
|
+
* const posts = await getAllPosts({ revalidate: 300 }); // 5 min cache
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Force fresh data
|
|
42
|
+
* const posts = await getAllPosts({ forceRefresh: true });
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Revalidate on-demand in an API route
|
|
46
|
+
* import { revalidateTag } from 'next/cache';
|
|
47
|
+
* revalidateTag('posts-list');
|
|
37
48
|
*/
|
|
38
49
|
export async function getAllPosts(options) {
|
|
39
|
-
const {
|
|
40
|
-
// Check if API is configured
|
|
50
|
+
const { revalidate = DEFAULT_REVALIDATE_SECONDS, forceRefresh = false, platform, tags = [] } = options || {};
|
|
41
51
|
const apiConfig = getAPIConfig();
|
|
42
|
-
// Check cache freshness (this also validates cache metadata)
|
|
43
|
-
if (!forceRefresh) {
|
|
44
|
-
const needsRefresh = await shouldRefreshCache(maxCacheAge);
|
|
45
|
-
if (!needsRefresh) {
|
|
46
|
-
const cached = await getAllCachedPosts();
|
|
47
|
-
console.log('Content Streamline: cached', cached);
|
|
48
|
-
if (cached.length > 0) {
|
|
49
|
-
// Filter cached posts by platform if specified
|
|
50
|
-
if (platform) {
|
|
51
|
-
const filtered = cached.filter(post => post.platform.toLowerCase() === platform.toLowerCase());
|
|
52
|
-
if (filtered.length > 0) {
|
|
53
|
-
return filtered;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
return cached;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// If API is not configured, return empty array but DON'T cache it
|
|
63
52
|
if (!apiConfig) {
|
|
64
53
|
console.warn('Content Streamline: API not configured. Set CONTENT_STREAMLINE_API_BASE_URL and CONTENT_STREAMLINE_API_ACCESS_TOKEN environment variables.');
|
|
65
54
|
return [];
|
|
66
55
|
}
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
const cacheOptions = {
|
|
57
|
+
revalidate: typeof revalidate === 'number' ? revalidate : undefined,
|
|
58
|
+
forceRefresh,
|
|
59
|
+
tags: ['content-streamline', 'posts-list', ...tags],
|
|
60
|
+
};
|
|
69
61
|
try {
|
|
70
|
-
const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken, typeof revalidate === 'number' ? revalidate : DEFAULT_REVALIDATE_SECONDS);
|
|
63
|
+
const posts = await api.getAllPosts(platform, cacheOptions);
|
|
64
|
+
if (!Array.isArray(posts)) {
|
|
65
|
+
console.error('Content Streamline: API returned invalid response (not an array)');
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return posts;
|
|
75
69
|
}
|
|
76
70
|
catch (error) {
|
|
77
|
-
// API call failed - DON'T cache the error, just return empty
|
|
78
|
-
// This allows subsequent calls to retry the API
|
|
79
71
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
80
|
-
console.error('Content Streamline: Failed to fetch posts
|
|
81
|
-
if (error instanceof Error && error.stack) {
|
|
82
|
-
console.error('Content Streamline: Error stack:', error.stack);
|
|
83
|
-
}
|
|
84
|
-
// Try to return stale cache if available (better than nothing)
|
|
85
|
-
const staleCached = await getAllCachedPosts();
|
|
86
|
-
if (staleCached.length > 0) {
|
|
87
|
-
console.warn(`Content Streamline: Returning ${staleCached.length} stale cached posts due to API error`);
|
|
88
|
-
if (platform) {
|
|
89
|
-
const filtered = staleCached.filter(post => post.platform.toLowerCase() === platform.toLowerCase());
|
|
90
|
-
return filtered;
|
|
91
|
-
}
|
|
92
|
-
return staleCached;
|
|
93
|
-
}
|
|
94
|
-
console.warn('Content Streamline: No cached data available, returning empty array');
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
// Validate that we got posts
|
|
98
|
-
if (!Array.isArray(posts)) {
|
|
99
|
-
console.error('Content Streamline: API returned invalid response (not an array):', typeof posts);
|
|
72
|
+
console.error('Content Streamline: Failed to fetch posts:', errorMessage);
|
|
100
73
|
return [];
|
|
101
74
|
}
|
|
102
|
-
// API call succeeded - cache the results with success metadata
|
|
103
|
-
// Note: We cache individual post details, but failures here don't prevent returning the posts
|
|
104
|
-
let cachedCount = 0;
|
|
105
|
-
for (const post of posts) {
|
|
106
|
-
try {
|
|
107
|
-
const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
|
|
108
|
-
const fullPost = await api.getPost(post.id, 'markdown');
|
|
109
|
-
await cachePost(fullPost);
|
|
110
|
-
cachedCount++;
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
// Log but don't fail - we still want to return the posts
|
|
114
|
-
console.warn(`Content Streamline: Failed to cache full details for post ${post.id}:`, error instanceof Error ? error.message : String(error));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (cachedCount > 0) {
|
|
118
|
-
console.log(`Content Streamline: Cached ${cachedCount} of ${posts.length} posts`);
|
|
119
|
-
}
|
|
120
|
-
// Update index with success metadata
|
|
121
|
-
try {
|
|
122
|
-
await updatePostsIndex(posts, true);
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
console.warn('Content Streamline: Failed to update posts index cache (non-fatal):', error instanceof Error ? error.message : String(error));
|
|
126
|
-
}
|
|
127
|
-
console.log(`Content Streamline: Returning ${posts.length} posts`);
|
|
128
|
-
return posts;
|
|
129
75
|
}
|
|
130
76
|
/**
|
|
131
|
-
* Get a single post by ID
|
|
132
|
-
* Use this in Server Components or API Routes
|
|
77
|
+
* Get a single post by ID with Next.js caching
|
|
133
78
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
79
|
+
* Uses Next.js fetch caching with automatic revalidation.
|
|
80
|
+
* Each post has its own cache tag for targeted revalidation.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // In a Server Component
|
|
84
|
+
* const post = await getPost(123, { contentFormat: 'html' });
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // Revalidate specific post on-demand
|
|
88
|
+
* import { revalidateTag } from 'next/cache';
|
|
89
|
+
* revalidateTag('post-123');
|
|
136
90
|
*/
|
|
137
91
|
export async function getPost(id, options) {
|
|
138
|
-
const {
|
|
139
|
-
// Check if API is configured
|
|
92
|
+
const { contentFormat = 'markdown', revalidate = DEFAULT_REVALIDATE_SECONDS, forceRefresh = false, tags = [], } = options || {};
|
|
140
93
|
const apiConfig = getAPIConfig();
|
|
141
|
-
// Try cache first (cache validation includes metadata checks)
|
|
142
|
-
if (!forceRefresh) {
|
|
143
|
-
const needsRefresh = await shouldRefreshCache(maxCacheAge);
|
|
144
|
-
if (!needsRefresh) {
|
|
145
|
-
const cached = await getCachedPost(id, language);
|
|
146
|
-
if (cached) {
|
|
147
|
-
return cached;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// If API is not configured, return null but DON'T cache it
|
|
152
94
|
if (!apiConfig) {
|
|
153
95
|
console.warn('Content Streamline: API not configured. Set CONTENT_STREAMLINE_API_BASE_URL and CONTENT_STREAMLINE_API_ACCESS_TOKEN environment variables.');
|
|
154
96
|
return null;
|
|
155
97
|
}
|
|
156
|
-
|
|
98
|
+
const cacheOptions = {
|
|
99
|
+
revalidate: typeof revalidate === 'number' ? revalidate : undefined,
|
|
100
|
+
forceRefresh,
|
|
101
|
+
tags: ['content-streamline', `post-${id}`, ...tags],
|
|
102
|
+
};
|
|
157
103
|
try {
|
|
158
|
-
const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
|
|
159
|
-
|
|
160
|
-
// API call succeeded - cache it
|
|
161
|
-
await cachePost(post);
|
|
162
|
-
return post;
|
|
104
|
+
const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken, typeof revalidate === 'number' ? revalidate : DEFAULT_REVALIDATE_SECONDS);
|
|
105
|
+
return await api.getPost(id, contentFormat, cacheOptions);
|
|
163
106
|
}
|
|
164
107
|
catch (error) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.error(`Content Streamline: Failed to fetch post ${id}:`, error);
|
|
168
|
-
const staleCached = await getCachedPost(id, language);
|
|
169
|
-
if (staleCached) {
|
|
170
|
-
console.warn(`Content Streamline: Returning stale cached data for post ${id} due to API error`);
|
|
171
|
-
return staleCached;
|
|
172
|
-
}
|
|
108
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.error(`Content Streamline: Failed to fetch post ${id}:`, errorMessage);
|
|
173
110
|
return null;
|
|
174
111
|
}
|
|
175
112
|
}
|
|
176
113
|
/**
|
|
177
|
-
* Get post by slug
|
|
178
|
-
*
|
|
114
|
+
* Get post by slug with Next.js caching
|
|
115
|
+
*
|
|
116
|
+
* Finds a post by its URL slug across all posts.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // In a dynamic route [slug]/page.tsx
|
|
120
|
+
* const post = await getPostBySlug(params.slug);
|
|
179
121
|
*/
|
|
180
122
|
export async function getPostBySlug(slug, options) {
|
|
181
|
-
// First, get all posts to find the one with matching slug
|
|
182
123
|
const posts = await getAllPosts({
|
|
183
|
-
|
|
124
|
+
revalidate: options?.revalidate,
|
|
184
125
|
forceRefresh: options?.forceRefresh,
|
|
185
126
|
platform: options?.platform,
|
|
127
|
+
tags: options?.tags,
|
|
186
128
|
});
|
|
187
129
|
const language = options?.language || 'en';
|
|
188
130
|
// Find post with matching slug
|
|
@@ -192,10 +134,34 @@ export async function getPostBySlug(slug, options) {
|
|
|
192
134
|
return getPost(post.id, {
|
|
193
135
|
language,
|
|
194
136
|
contentFormat: options?.contentFormat,
|
|
195
|
-
|
|
137
|
+
revalidate: options?.revalidate,
|
|
196
138
|
forceRefresh: options?.forceRefresh,
|
|
139
|
+
tags: options?.tags,
|
|
197
140
|
});
|
|
198
141
|
}
|
|
199
142
|
}
|
|
200
143
|
return null;
|
|
201
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Get cache tags for use with revalidateTag()
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // Revalidate all content
|
|
150
|
+
* revalidateTag(getCacheTags().all);
|
|
151
|
+
*
|
|
152
|
+
* // Revalidate posts list
|
|
153
|
+
* revalidateTag(getCacheTags().postsList);
|
|
154
|
+
*
|
|
155
|
+
* // Revalidate specific post
|
|
156
|
+
* revalidateTag(getCacheTags().post(123));
|
|
157
|
+
*/
|
|
158
|
+
export function getCacheTags() {
|
|
159
|
+
return {
|
|
160
|
+
/** Tag for all content-streamline data */
|
|
161
|
+
all: 'content-streamline',
|
|
162
|
+
/** Tag for posts list */
|
|
163
|
+
postsList: 'posts-list',
|
|
164
|
+
/** Get tag for a specific post by ID */
|
|
165
|
+
post: (id) => `post-${id}`,
|
|
166
|
+
};
|
|
167
|
+
}
|