@content-streamline/nextjs 0.0.1

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 ADDED
@@ -0,0 +1,400 @@
1
+ # @content-streamline/nextjs
2
+
3
+ Next.js library for Content Streamline API with reusable, responsive components.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Easy Setup** - Configure via environment variables
8
+ - 📦 **Reusable Components** - Pre-built React components for posts list and detail
9
+ - 💾 **Smart Caching** - Automatic file-based caching with refresh control
10
+ - 🎨 **Customizable** - Responsive components with configurable props
11
+ - 📱 **Mobile-Friendly** - Built-in responsive design
12
+ - 🔧 **TypeScript** - Full TypeScript support
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @content-streamline/nextjs
18
+ # or
19
+ bun add @content-streamline/nextjs
20
+ ```
21
+
22
+ **Note**: React and Next.js are peer dependencies. Make sure you have them installed in your Next.js project.
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Configure Environment Variables
27
+
28
+ Create a `.env.local` file in your Next.js project:
29
+
30
+ ```env
31
+ CONTENT_STREAMLINE_API_BASE_URL=https://your-api-domain.com
32
+ CONTENT_STREAMLINE_API_ACCESS_TOKEN=your_access_token_here
33
+ ```
34
+
35
+ Or use shorter names:
36
+ ```env
37
+ API_BASE_URL=https://your-api-domain.com
38
+ API_ACCESS_TOKEN=your_access_token_here
39
+ ```
40
+
41
+ ### 2. Import Styles
42
+
43
+ In your `app/layout.tsx` or `pages/_app.tsx`:
44
+
45
+ ```tsx
46
+ import '@content-streamline/nextjs/components/styles.css';
47
+ ```
48
+
49
+ ### 3. Use in Server Components (App Router)
50
+
51
+ **Blog List Page** (`app/blog/page.tsx`):
52
+
53
+ ```tsx
54
+ import { getAllPosts } from '@content-streamline/nextjs/server';
55
+ import { PostsList } from '@content-streamline/nextjs/components';
56
+
57
+ export const revalidate = 3600; // Revalidate every hour (ISR)
58
+
59
+ export default async function BlogPage() {
60
+ const posts = await getAllPosts();
61
+
62
+ return (
63
+ <div>
64
+ <h1>Blog</h1>
65
+ <PostsList posts={posts} baseUrl="/blog" />
66
+ </div>
67
+ );
68
+ }
69
+ ```
70
+
71
+ **Post Detail Page** (`app/blog/[id]/[slug]/page.tsx`):
72
+
73
+ ```tsx
74
+ import { getPostBySlug } from '@content-streamline/nextjs/server';
75
+ import { PostDetail } from '@content-streamline/nextjs/components';
76
+ import { notFound } from 'next/navigation';
77
+
78
+ export async function generateStaticParams() {
79
+ const { getAllPosts } = await import('@content-streamline/nextjs/server');
80
+ const posts = await getAllPosts();
81
+
82
+ return posts.flatMap(post =>
83
+ post.post_translations
84
+ .filter(t => t.slug)
85
+ .map(translation => ({
86
+ id: post.id.toString(),
87
+ slug: translation.slug,
88
+ }))
89
+ );
90
+ }
91
+
92
+ export default async function PostPage({
93
+ params,
94
+ }: {
95
+ params: { id: string; slug: string };
96
+ }) {
97
+ const post = await getPostBySlug(params.slug);
98
+
99
+ if (!post) {
100
+ notFound();
101
+ }
102
+
103
+ return (
104
+ <PostDetail post={post} backUrl="/blog" />
105
+ );
106
+ }
107
+ ```
108
+
109
+ ### 4. Use in Pages Router
110
+
111
+ **Blog List Page** (`pages/blog/index.tsx`):
112
+
113
+ ```tsx
114
+ import { getAllPosts } from '@content-streamline/nextjs/server';
115
+ import { PostsList } from '@content-streamline/nextjs/components';
116
+ import type { GetStaticProps } from 'next';
117
+
118
+ export const getStaticProps: GetStaticProps = async () => {
119
+ const posts = await getAllPosts();
120
+
121
+ return {
122
+ props: { posts },
123
+ revalidate: 3600, // Revalidate every hour
124
+ };
125
+ };
126
+
127
+ export default function BlogPage({ posts }: { posts: Post[] }) {
128
+ return (
129
+ <div>
130
+ <h1>Blog</h1>
131
+ <PostsList posts={posts} baseUrl="/blog" />
132
+ </div>
133
+ );
134
+ }
135
+ ```
136
+
137
+ **Post Detail Page** (`pages/blog/[id]/[slug].tsx`):
138
+
139
+ ```tsx
140
+ import { getPostBySlug, getAllPosts } from '@content-streamline/nextjs/server';
141
+ import { PostDetail } from '@content-streamline/nextjs/components';
142
+ import type { GetStaticProps, GetStaticPaths } from 'next';
143
+
144
+ export const getStaticPaths: GetStaticPaths = async () => {
145
+ const posts = await getAllPosts();
146
+
147
+ const paths = posts.flatMap(post =>
148
+ post.post_translations
149
+ .filter(t => t.slug)
150
+ .map(translation => ({
151
+ params: {
152
+ id: post.id.toString(),
153
+ slug: translation.slug,
154
+ },
155
+ }))
156
+ );
157
+
158
+ return { paths, fallback: 'blocking' };
159
+ };
160
+
161
+ export const getStaticProps: GetStaticProps = async ({ params }) => {
162
+ const post = await getPostBySlug(params!.slug as string);
163
+
164
+ if (!post) {
165
+ return { notFound: true };
166
+ }
167
+
168
+ return {
169
+ props: { post },
170
+ revalidate: 3600,
171
+ };
172
+ };
173
+
174
+ export default function PostPage({ post }: { post: PostDetailResponse }) {
175
+ return <PostDetail post={post} backUrl="/blog" />;
176
+ }
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### Server Functions
182
+
183
+ #### `getAllPosts(options?)`
184
+
185
+ Get all posts with automatic caching.
186
+
187
+ ```tsx
188
+ import { getAllPosts } from '@content-streamline/nextjs/server';
189
+
190
+ const posts = await getAllPosts({
191
+ maxCacheAge: 60, // Cache age in minutes (default: 60)
192
+ forceRefresh: false, // Force API fetch (default: false)
193
+ });
194
+ ```
195
+
196
+ #### `getPost(id, options?)`
197
+
198
+ Get a single post by ID.
199
+
200
+ ```tsx
201
+ import { getPost } from '@content-streamline/nextjs/server';
202
+
203
+ const post = await getPost(123, {
204
+ language: 'en',
205
+ contentFormat: 'markdown', // or 'html'
206
+ maxCacheAge: 60,
207
+ forceRefresh: false,
208
+ });
209
+ ```
210
+
211
+ #### `getPostBySlug(slug, options?)`
212
+
213
+ Get a post by slug.
214
+
215
+ ```tsx
216
+ import { getPostBySlug } from '@content-streamline/nextjs/server';
217
+
218
+ const post = await getPostBySlug('my-post-slug', {
219
+ language: 'en',
220
+ contentFormat: 'markdown',
221
+ maxCacheAge: 60,
222
+ forceRefresh: false,
223
+ });
224
+ ```
225
+
226
+ ### Components
227
+
228
+ #### `<PostsList />`
229
+
230
+ Display a list of posts.
231
+
232
+ ```tsx
233
+ import { PostsList } from '@content-streamline/nextjs/components';
234
+
235
+ <PostsList
236
+ posts={posts}
237
+ baseUrl="/blog" // Base URL for post links
238
+ language="en" // Language for translations
239
+ className="custom-class" // Custom container class
240
+ cardClassName="custom" // Custom card class
241
+ showDate={true} // Show post date
242
+ showExcerpt={true} // Show post excerpt
243
+ showImages={true} // Show post images
244
+ renderTitle={(post, translation) => <CustomTitle />} // Custom title renderer
245
+ renderExcerpt={(post, translation) => <CustomExcerpt />} // Custom excerpt renderer
246
+ />
247
+ ```
248
+
249
+ #### `<PostDetail />`
250
+
251
+ Display a single post.
252
+
253
+ ```tsx
254
+ import { PostDetail } from '@content-streamline/nextjs/components';
255
+
256
+ <PostDetail
257
+ post={post}
258
+ backUrl="/blog" // URL for back link
259
+ language="en" // Language for translation
260
+ className="custom-class" // Custom container class
261
+ showBackLink={true} // Show back link
262
+ showDate={true} // Show post date
263
+ showType={true} // Show post type badge
264
+ showImage={true} // Show featured image
265
+ showGallery={true} // Show gallery images
266
+ renderContent={(content) => <MarkdownRenderer content={content} />} // Custom content renderer
267
+ renderTitle={(post, translation) => <CustomTitle />} // Custom title renderer
268
+ />
269
+ ```
270
+
271
+ ## Customization
272
+
273
+ ### Styling
274
+
275
+ Components use CSS classes with the `content-streamline-` prefix. You can override styles:
276
+
277
+ ```css
278
+ /* Override post card styles */
279
+ .content-streamline-post-card {
280
+ border-radius: 16px;
281
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
282
+ }
283
+
284
+ /* Override post title */
285
+ .content-streamline-post-title a {
286
+ color: #your-color;
287
+ }
288
+ ```
289
+
290
+ ### Custom Rendering
291
+
292
+ Use render props for complete control:
293
+
294
+ ```tsx
295
+ <PostsList
296
+ posts={posts}
297
+ renderTitle={(post, translation) => (
298
+ <h2 className="my-custom-title">
299
+ {translation.title}
300
+ </h2>
301
+ )}
302
+ renderExcerpt={(post, translation) => (
303
+ <p className="my-custom-excerpt">
304
+ {translation.meta_description}
305
+ </p>
306
+ )}
307
+ />
308
+ ```
309
+
310
+ ### Markdown Rendering
311
+
312
+ For proper markdown rendering, use a library like `react-markdown`:
313
+
314
+ ```tsx
315
+ import ReactMarkdown from 'react-markdown';
316
+ import { PostDetail } from '@content-streamline/nextjs/components';
317
+
318
+ <PostDetail
319
+ post={post}
320
+ renderContent={(content) => (
321
+ <ReactMarkdown>{content}</ReactMarkdown>
322
+ )}
323
+ />
324
+ ```
325
+
326
+ ## Caching
327
+
328
+ The library automatically caches posts in `.next/content-streamline-cache/`.
329
+
330
+ - **Default cache age**: 60 minutes
331
+ - **Cache location**: `.next/content-streamline-cache/`
332
+ - **Automatic refresh**: When cache is older than `maxCacheAge`
333
+
334
+ To force refresh:
335
+ ```tsx
336
+ const posts = await getAllPosts({ forceRefresh: true });
337
+ ```
338
+
339
+ ## TypeScript
340
+
341
+ Full TypeScript support is included:
342
+
343
+ ```tsx
344
+ import type { Post, PostDetailResponse } from '@content-streamline/nextjs';
345
+
346
+ function MyComponent({ post }: { post: PostDetailResponse }) {
347
+ // ...
348
+ }
349
+ ```
350
+
351
+ ## Examples
352
+
353
+ ### Basic Usage
354
+
355
+ ```tsx
356
+ // app/blog/page.tsx
357
+ import { getAllPosts } from '@content-streamline/nextjs/server';
358
+ import { PostsList } from '@content-streamline/nextjs/components';
359
+
360
+ export default async function Blog() {
361
+ const posts = await getAllPosts();
362
+ return <PostsList posts={posts} baseUrl="/blog" />;
363
+ }
364
+ ```
365
+
366
+ ### With Custom Styling
367
+
368
+ ```tsx
369
+ <PostsList
370
+ posts={posts}
371
+ baseUrl="/blog"
372
+ className="my-blog-list"
373
+ cardClassName="my-post-card"
374
+ showImages={false}
375
+ />
376
+ ```
377
+
378
+ ### With Markdown Rendering
379
+
380
+ ```tsx
381
+ import ReactMarkdown from 'react-markdown';
382
+ import { getPostBySlug } from '@content-streamline/nextjs/server';
383
+ import { PostDetail } from '@content-streamline/nextjs/components';
384
+
385
+ export default async function PostPage({ params }) {
386
+ const post = await getPostBySlug(params.slug);
387
+
388
+ return (
389
+ <PostDetail
390
+ post={post}
391
+ renderContent={(content) => <ReactMarkdown>{content}</ReactMarkdown>}
392
+ />
393
+ );
394
+ }
395
+ ```
396
+
397
+ ## License
398
+
399
+ MIT
400
+
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Content Streamline Public API Client
3
+ */
4
+ import type { Post, PostDetailResponse, PostsResponse } from '../types';
5
+ declare class ContentStreamlineAPI {
6
+ private baseUrl;
7
+ private accessToken;
8
+ constructor(baseUrl: string, accessToken: string);
9
+ private fetch;
10
+ /**
11
+ * List all posts with pagination
12
+ */
13
+ listPosts(page?: number): Promise<PostsResponse>;
14
+ /**
15
+ * Get all posts by fetching all pages
16
+ */
17
+ getAllPosts(): Promise<Post[]>;
18
+ /**
19
+ * Get a single post by ID
20
+ */
21
+ getPost(id: number, contentFormat?: 'markdown' | 'html'): Promise<PostDetailResponse>;
22
+ }
23
+ export declare function createAPIClient(baseUrl: string, accessToken: string): ContentStreamlineAPI;
24
+ export {};
25
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +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,UAAU,CAAC;AAElB,cAAM,oBAAoB;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;YAKlC,KAAK;IAiCnB;;OAEG;IACG,SAAS,CAAC,IAAI,GAAE,MAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAIzD;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAgBpC;;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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Content Streamline Public API Client
3
+ */
4
+ class ContentStreamlineAPI {
5
+ constructor(baseUrl, accessToken) {
6
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
7
+ this.accessToken = accessToken;
8
+ }
9
+ async fetch(endpoint, options = {}) {
10
+ const url = `${this.baseUrl}${endpoint}`;
11
+ const headers = {
12
+ 'Authorization': `Bearer ${this.accessToken}`,
13
+ 'Content-Type': 'application/json',
14
+ ...options.headers,
15
+ };
16
+ const response = await fetch(url, {
17
+ ...options,
18
+ headers,
19
+ });
20
+ if (!response.ok) {
21
+ if (response.status === 401) {
22
+ const errorText = await response.text();
23
+ if (errorText.includes('Missing access token')) {
24
+ throw new Error('Missing access token');
25
+ }
26
+ throw new Error('Invalid access token');
27
+ }
28
+ if (response.status === 403) {
29
+ throw new Error('Access token has been revoked');
30
+ }
31
+ if (response.status === 404) {
32
+ throw new Error('Post not found');
33
+ }
34
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
35
+ }
36
+ return response.json();
37
+ }
38
+ /**
39
+ * List all posts with pagination
40
+ */
41
+ async listPosts(page = 1) {
42
+ return this.fetch(`/public/v1/posts?page=${page}`);
43
+ }
44
+ /**
45
+ * Get all posts by fetching all pages
46
+ */
47
+ async getAllPosts() {
48
+ const allPosts = [];
49
+ let currentPage = 1;
50
+ let hasMore = true;
51
+ while (hasMore) {
52
+ const response = await this.listPosts(currentPage);
53
+ allPosts.push(...response.data);
54
+ hasMore = response.pagination.next_page !== null;
55
+ currentPage = response.pagination.next_page || currentPage + 1;
56
+ }
57
+ return allPosts;
58
+ }
59
+ /**
60
+ * Get a single post by ID
61
+ */
62
+ async getPost(id, contentFormat = 'markdown') {
63
+ const format = contentFormat === 'html' ? 'html' : 'markdown';
64
+ return this.fetch(`/public/v1/posts/${id}?content_format=${format}`);
65
+ }
66
+ }
67
+ export function createAPIClient(baseUrl, accessToken) {
68
+ return new ContentStreamlineAPI(baseUrl, accessToken);
69
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * File-based cache for posts
3
+ * Works in Next.js server environment
4
+ */
5
+ import type { Post, PostDetailResponse, CachedPostIndex } from '../types';
6
+ /**
7
+ * Initialize cache directory
8
+ */
9
+ export declare function initCache(): Promise<void>;
10
+ /**
11
+ * Save a post to cache
12
+ */
13
+ export declare function cachePost(post: Post | PostDetailResponse): Promise<void>;
14
+ /**
15
+ * Get a cached post by ID and language
16
+ */
17
+ export declare function getCachedPost(id: number, language?: string): Promise<PostDetailResponse | null>;
18
+ /**
19
+ * Get all cached posts
20
+ */
21
+ export declare function getAllCachedPosts(): Promise<Post[]>;
22
+ /**
23
+ * Update the posts index
24
+ */
25
+ export declare function updatePostsIndex(posts: Post[]): Promise<void>;
26
+ /**
27
+ * Get the posts index
28
+ */
29
+ export declare function getPostsIndex(): Promise<CachedPostIndex | null>;
30
+ /**
31
+ * Check if cache needs refresh (older than specified minutes)
32
+ */
33
+ export declare function shouldRefreshCache(maxAgeMinutes?: number): Promise<boolean>;
34
+ /**
35
+ * Clear all cached posts
36
+ */
37
+ export declare function clearCache(): Promise<void>;
38
+ //# sourceMappingURL=file-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-cache.d.ts","sourceRoot":"","sources":["../../src/cache/file-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAM1E;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAO/C;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAS9E;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAa,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAS3G;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CA0BzD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBnE;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAOrE;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,aAAa,GAAE,MAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CASrF;AAED;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAYhD"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * File-based cache for posts
3
+ * Works in Next.js server environment
4
+ */
5
+ import { writeFile, readFile, mkdir, readdir, unlink } from 'fs/promises';
6
+ import { join } from 'path';
7
+ const CACHE_DIR = join(process.cwd(), '.next', 'content-streamline-cache');
8
+ const POSTS_INDEX_FILE = join(CACHE_DIR, 'posts-index.json');
9
+ const POSTS_DIR = join(CACHE_DIR, 'posts');
10
+ /**
11
+ * Initialize cache directory
12
+ */
13
+ export async function initCache() {
14
+ try {
15
+ await mkdir(CACHE_DIR, { recursive: true });
16
+ await mkdir(POSTS_DIR, { recursive: true });
17
+ }
18
+ catch (error) {
19
+ // Directory might already exist, that's fine
20
+ }
21
+ }
22
+ /**
23
+ * Save a post to cache
24
+ */
25
+ export async function cachePost(post) {
26
+ await initCache();
27
+ for (const translation of post.post_translations) {
28
+ const filename = `${post.id}-${translation.language}.json`;
29
+ const filepath = join(POSTS_DIR, filename);
30
+ await writeFile(filepath, JSON.stringify(post, null, 2), 'utf-8');
31
+ }
32
+ }
33
+ /**
34
+ * Get a cached post by ID and language
35
+ */
36
+ export async function getCachedPost(id, language = 'en') {
37
+ try {
38
+ const filename = `${id}-${language}.json`;
39
+ const filepath = join(POSTS_DIR, filename);
40
+ const content = await readFile(filepath, 'utf-8');
41
+ return JSON.parse(content);
42
+ }
43
+ catch (error) {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Get all cached posts
49
+ */
50
+ export async function getAllCachedPosts() {
51
+ try {
52
+ const files = await readdir(POSTS_DIR);
53
+ const posts = [];
54
+ const seenIds = new Set();
55
+ for (const file of files) {
56
+ if (!file.endsWith('.json'))
57
+ continue;
58
+ const filepath = join(POSTS_DIR, file);
59
+ const content = await readFile(filepath, 'utf-8');
60
+ const post = JSON.parse(content);
61
+ // Only add each post once (in case of multiple translations)
62
+ if (!seenIds.has(post.id)) {
63
+ posts.push(post);
64
+ seenIds.add(post.id);
65
+ }
66
+ }
67
+ return posts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
68
+ }
69
+ catch (error) {
70
+ return [];
71
+ }
72
+ }
73
+ /**
74
+ * Update the posts index
75
+ */
76
+ export async function updatePostsIndex(posts) {
77
+ await initCache();
78
+ const index = {
79
+ posts: posts.flatMap(post => post.post_translations.map(translation => ({
80
+ id: post.id,
81
+ slug: translation.slug,
82
+ language: translation.language,
83
+ updated_at: post.created_at,
84
+ }))),
85
+ last_sync: new Date().toISOString(),
86
+ };
87
+ await writeFile(POSTS_INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8');
88
+ }
89
+ /**
90
+ * Get the posts index
91
+ */
92
+ export async function getPostsIndex() {
93
+ try {
94
+ const content = await readFile(POSTS_INDEX_FILE, 'utf-8');
95
+ return JSON.parse(content);
96
+ }
97
+ catch (error) {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Check if cache needs refresh (older than specified minutes)
103
+ */
104
+ export async function shouldRefreshCache(maxAgeMinutes = 60) {
105
+ const index = await getPostsIndex();
106
+ if (!index)
107
+ return true;
108
+ const lastSync = new Date(index.last_sync);
109
+ const now = new Date();
110
+ const ageMinutes = (now.getTime() - lastSync.getTime()) / (1000 * 60);
111
+ return ageMinutes > maxAgeMinutes;
112
+ }
113
+ /**
114
+ * Clear all cached posts
115
+ */
116
+ export async function clearCache() {
117
+ try {
118
+ const files = await readdir(POSTS_DIR);
119
+ for (const file of files) {
120
+ if (file.endsWith('.json')) {
121
+ await unlink(join(POSTS_DIR, file));
122
+ }
123
+ }
124
+ await unlink(POSTS_INDEX_FILE);
125
+ }
126
+ catch (error) {
127
+ // Ignore errors
128
+ }
129
+ }