@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 +400 -0
- package/dist/api/client.d.ts +25 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +69 -0
- package/dist/cache/file-cache.d.ts +38 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/file-cache.js +129 -0
- package/dist/components/PostDetail.d.ts +47 -0
- package/dist/components/PostDetail.d.ts.map +1 -0
- package/dist/components/PostDetail.js +23 -0
- package/dist/components/PostsList.d.ts +44 -0
- package/dist/components/PostsList.d.ts.map +1 -0
- package/dist/components/PostsList.js +22 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/components/styles.css +331 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +106 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/package.json +52 -0
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
|
+
}
|