@content-streamline/nextjs 0.0.8 → 0.0.10

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 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
- - 💾 **Smart Caching** - Automatic file-based caching with refresh control
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 = 3600; // Revalidate every hour (ISR)
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: 3600, // Revalidate every hour
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: 3600,
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 automatic caching.
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
- maxCacheAge: 60, // Cache age in minutes (default: 60)
208
- forceRefresh: false, // Force API fetch (default: 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
- maxCacheAge: 60,
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
- maxCacheAge: 60,
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 automatically caches posts in `.next/content-streamline-cache/`.
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
- - **Default cache age**: 60 minutes
449
- - **Cache location**: `.next/content-streamline-cache/`
450
- - **Automatic refresh**: When cache is older than `maxCacheAge`
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,
@@ -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
- constructor(baseUrl: string, accessToken: string);
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
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,IAAI,EACJ,kBAAkB,EAClB,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B,cAAM,oBAAoB;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;YAKlC,KAAK;IAsDnB;;OAEG;IACG,SAAS,CAAC,IAAI,GAAE,MAAU,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAS5E;;OAEG;IACG,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA4BrD;;OAEG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,aAAa,GAAE,UAAU,GAAG,MAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAIxG;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,oBAAoB,CAE1F"}
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"}
@@ -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
- return this.fetch(`/public/v1/posts?${params.toString()}`);
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
- return this.fetch(`/public/v1/posts/${id}?content_format=${format}`);
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
  }
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;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,GACd,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"}
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';
@@ -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
- * Note: Only successful API responses are cached. Failed API calls
11
- * (network errors, auth failures, etc.) will NOT be cached, allowing
12
- * subsequent calls to retry the API.
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
- export declare function getAllPosts(options?: {
15
- maxCacheAge?: number;
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
- }): Promise<Post[]>;
26
+ /** Additional cache tags for on-demand revalidation */
27
+ tags?: string[];
28
+ }
19
29
  /**
20
- * Get a single post by ID
21
- * Use this in Server Components or API Routes
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
- * Note: Only successful API responses are cached. Failed API calls
24
- * will NOT be cached, allowing subsequent calls to retry the API.
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 getPost(id: number, options?: {
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
- maxCacheAge?: number;
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
- }): Promise<PostDetailResponse | null>;
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
- * Use this in Server Components or API Routes
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;;;GAGG;AAWH,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AA+BlE;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAyGlB;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GACA,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAoDpC;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2BpC;AAGD,YAAY,EACV,IAAI,EACJ,kBAAkB,EAClB,eAAe,EACf,SAAS,EACT,aAAa,EACb,UAAU,GACX,MAAM,mBAAmB,CAAC"}
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"}
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * Server-side functions for Next.js
3
- * Use these in Server Components, API Routes, or getServerSideProps
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
- import { getAllCachedPosts, getCachedPost, cachePost, updatePostsIndex, shouldRefreshCache, } from '../cache/file-cache.js';
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,170 +28,103 @@ function getAPIConfig() {
17
28
  return { baseUrl, accessToken };
18
29
  }
19
30
  /**
20
- * Get API configuration, throwing if not configured
21
- * Use this when API is required
22
- */
23
- function requireAPIConfig() {
24
- const config = getAPIConfig();
25
- if (!config) {
26
- throw new Error('Missing API configuration. Set CONTENT_STREAMLINE_API_BASE_URL and CONTENT_STREAMLINE_API_ACCESS_TOKEN environment variables.');
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
- * Note: Only successful API responses are cached. Failed API calls
35
- * (network errors, auth failures, etc.) will NOT be cached, allowing
36
- * subsequent calls to retry the API.
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 { maxCacheAge = 15, forceRefresh = false, platform } = options || {};
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
- if (cached.length > 0) {
48
- // Filter cached posts by platform if specified
49
- if (platform) {
50
- const filtered = cached.filter(post => post.platform.toLowerCase() === platform.toLowerCase());
51
- if (filtered.length > 0) {
52
- return filtered;
53
- }
54
- }
55
- else {
56
- return cached;
57
- }
58
- }
59
- }
60
- }
61
- // If API is not configured, return empty array but DON'T cache it
62
52
  if (!apiConfig) {
63
53
  console.warn('Content Streamline: API not configured. Set CONTENT_STREAMLINE_API_BASE_URL and CONTENT_STREAMLINE_API_ACCESS_TOKEN environment variables.');
64
54
  return [];
65
55
  }
66
- // Fetch from API
67
- let posts;
56
+ const cacheOptions = {
57
+ revalidate: typeof revalidate === 'number' ? revalidate : undefined,
58
+ forceRefresh,
59
+ tags: ['content-streamline', 'posts-list', ...tags],
60
+ };
68
61
  try {
69
- const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
70
- console.log(`Content Streamline: Fetching posts from API (forceRefresh: ${forceRefresh}, platform: ${platform || 'all'})`);
71
- posts = await api.getAllPosts(platform);
72
- console.log(`Content Streamline: Successfully fetched ${posts.length} posts from API`);
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;
73
69
  }
74
70
  catch (error) {
75
- // API call failed - DON'T cache the error, just return empty
76
- // This allows subsequent calls to retry the API
77
71
  const errorMessage = error instanceof Error ? error.message : String(error);
78
- console.error('Content Streamline: Failed to fetch posts from API:', errorMessage);
79
- if (error instanceof Error && error.stack) {
80
- console.error('Content Streamline: Error stack:', error.stack);
81
- }
82
- // Try to return stale cache if available (better than nothing)
83
- const staleCached = await getAllCachedPosts();
84
- if (staleCached.length > 0) {
85
- console.warn(`Content Streamline: Returning ${staleCached.length} stale cached posts due to API error`);
86
- if (platform) {
87
- const filtered = staleCached.filter(post => post.platform.toLowerCase() === platform.toLowerCase());
88
- return filtered;
89
- }
90
- return staleCached;
91
- }
92
- console.warn('Content Streamline: No cached data available, returning empty array');
93
- return [];
94
- }
95
- // Validate that we got posts
96
- if (!Array.isArray(posts)) {
97
- console.error('Content Streamline: API returned invalid response (not an array):', typeof posts);
72
+ console.error('Content Streamline: Failed to fetch posts:', errorMessage);
98
73
  return [];
99
74
  }
100
- // API call succeeded - cache the results with success metadata
101
- // Note: We cache individual post details, but failures here don't prevent returning the posts
102
- let cachedCount = 0;
103
- for (const post of posts) {
104
- try {
105
- const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
106
- const fullPost = await api.getPost(post.id, 'markdown');
107
- await cachePost(fullPost);
108
- cachedCount++;
109
- }
110
- catch (error) {
111
- // Log but don't fail - we still want to return the posts
112
- console.warn(`Content Streamline: Failed to cache full details for post ${post.id}:`, error instanceof Error ? error.message : String(error));
113
- }
114
- }
115
- if (cachedCount > 0) {
116
- console.log(`Content Streamline: Cached ${cachedCount} of ${posts.length} posts`);
117
- }
118
- // Update index with success metadata
119
- try {
120
- await updatePostsIndex(posts, true);
121
- }
122
- catch (error) {
123
- console.warn('Content Streamline: Failed to update posts index cache (non-fatal):', error instanceof Error ? error.message : String(error));
124
- }
125
- console.log(`Content Streamline: Returning ${posts.length} posts`);
126
- return posts;
127
75
  }
128
76
  /**
129
- * Get a single post by ID
130
- * Use this in Server Components or API Routes
77
+ * Get a single post by ID with Next.js caching
131
78
  *
132
- * Note: Only successful API responses are cached. Failed API calls
133
- * will NOT be cached, allowing subsequent calls to retry the API.
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');
134
90
  */
135
91
  export async function getPost(id, options) {
136
- const { language = 'en', contentFormat = 'markdown', maxCacheAge = 15, forceRefresh = false, } = options || {};
137
- // Check if API is configured
92
+ const { contentFormat = 'markdown', revalidate = DEFAULT_REVALIDATE_SECONDS, forceRefresh = false, tags = [], } = options || {};
138
93
  const apiConfig = getAPIConfig();
139
- // Try cache first (cache validation includes metadata checks)
140
- if (!forceRefresh) {
141
- const needsRefresh = await shouldRefreshCache(maxCacheAge);
142
- if (!needsRefresh) {
143
- const cached = await getCachedPost(id, language);
144
- if (cached) {
145
- return cached;
146
- }
147
- }
148
- }
149
- // If API is not configured, return null but DON'T cache it
150
94
  if (!apiConfig) {
151
95
  console.warn('Content Streamline: API not configured. Set CONTENT_STREAMLINE_API_BASE_URL and CONTENT_STREAMLINE_API_ACCESS_TOKEN environment variables.');
152
96
  return null;
153
97
  }
154
- // Fetch from API
98
+ const cacheOptions = {
99
+ revalidate: typeof revalidate === 'number' ? revalidate : undefined,
100
+ forceRefresh,
101
+ tags: ['content-streamline', `post-${id}`, ...tags],
102
+ };
155
103
  try {
156
- const api = createAPIClient(apiConfig.baseUrl, apiConfig.accessToken);
157
- const post = await api.getPost(id, contentFormat);
158
- // API call succeeded - cache it
159
- await cachePost(post);
160
- 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);
161
106
  }
162
107
  catch (error) {
163
- // API call failed - DON'T cache the error
164
- // Try to return stale cache if available
165
- console.error(`Content Streamline: Failed to fetch post ${id}:`, error);
166
- const staleCached = await getCachedPost(id, language);
167
- if (staleCached) {
168
- console.warn(`Content Streamline: Returning stale cached data for post ${id} due to API error`);
169
- return staleCached;
170
- }
108
+ const errorMessage = error instanceof Error ? error.message : String(error);
109
+ console.error(`Content Streamline: Failed to fetch post ${id}:`, errorMessage);
171
110
  return null;
172
111
  }
173
112
  }
174
113
  /**
175
- * Get post by slug
176
- * Use this in Server Components or API Routes
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);
177
121
  */
178
122
  export async function getPostBySlug(slug, options) {
179
- // First, get all posts to find the one with matching slug
180
123
  const posts = await getAllPosts({
181
- maxCacheAge: options?.maxCacheAge,
124
+ revalidate: options?.revalidate,
182
125
  forceRefresh: options?.forceRefresh,
183
126
  platform: options?.platform,
127
+ tags: options?.tags,
184
128
  });
185
129
  const language = options?.language || 'en';
186
130
  // Find post with matching slug
@@ -190,10 +134,34 @@ export async function getPostBySlug(slug, options) {
190
134
  return getPost(post.id, {
191
135
  language,
192
136
  contentFormat: options?.contentFormat,
193
- maxCacheAge: options?.maxCacheAge,
137
+ revalidate: options?.revalidate,
194
138
  forceRefresh: options?.forceRefresh,
139
+ tags: options?.tags,
195
140
  });
196
141
  }
197
142
  }
198
143
  return null;
199
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@content-streamline/nextjs",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Next.js library for Content Streamline API with reusable components",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,8 +33,8 @@
33
33
  "author": "",
34
34
  "license": "MIT",
35
35
  "peerDependencies": {
36
- "react": "^18.0.0 || ^19.0.0",
37
- "next": "^14.0.0 || ^15.0.0"
36
+ "next": "^14.0.0 || ^15.0.0",
37
+ "react": "^18.0.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^20.14.0",