@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 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
  }
@@ -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,2CAgFhB"}
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 (_jsxs("article", { className: `content-streamline-list-item ${itemClassName}`, children: [showImages && featuredImage && (_jsx("a", { href: `${baseUrl}/${post.id}/${translation.slug}`, className: "content-streamline-list-image-link", children: _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: _jsx("a", { href: `${baseUrl}/${post.id}/${translation.slug}`, 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));
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-image-link {
195
- display: block;
194
+ .content-streamline-list-link {
195
+ display: grid;
196
+ grid-template-columns: inherit;
197
+ gap: inherit;
196
198
  text-decoration: none;
197
- border-radius: 8px;
198
- overflow: hidden;
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-title a:hover {
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';
@@ -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,CA2GlB;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,172 +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
- 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
- // Fetch from API
68
- let posts;
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
- console.log(`Content Streamline: Fetching posts from API (forceRefresh: ${forceRefresh}, platform: ${platform || 'all'})`);
72
- posts = await api.getAllPosts(platform);
73
- console.log('Content Streamline: posts', posts);
74
- 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;
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 from API:', errorMessage);
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
- * Note: Only successful API responses are cached. Failed API calls
135
- * 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');
136
90
  */
137
91
  export async function getPost(id, options) {
138
- const { language = 'en', contentFormat = 'markdown', maxCacheAge = 15, forceRefresh = false, } = options || {};
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
- // Fetch from API
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
- const post = await api.getPost(id, contentFormat);
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
- // API call failed - DON'T cache the error
166
- // Try to return stale cache if available
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
- * 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);
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
- maxCacheAge: options?.maxCacheAge,
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
- maxCacheAge: options?.maxCacheAge,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@content-streamline/nextjs",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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",