@rezamirzapour/pod-sdk 1.0.0 → 1.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 +217 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,131 +77,266 @@ export const podSdk = createPodSdk({
|
|
|
77
77
|
|
|
78
78
|
POD CMS stores dynamic fields inside a `metadata.content` array of `{ code, value, type }` objects. `@rezamirzapour/pod-sdk` automatically formats these into clean typed key-value pairs (`item.formatted`) and allows an optional custom `normalizer` function (`item.__normalized`).
|
|
79
79
|
|
|
80
|
-
###
|
|
80
|
+
### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
In enterprise Next.js applications, avoid calling SDK clients directly inside UI components. Instead, encapsulate CMS queries, generics, normalizers, and caching logic inside a dedicated Data Access Layer (`services/newsApi.ts`).
|
|
83
|
+
|
|
84
|
+
#### Step 1: Create the Data Access Layer (`services/newsApi.ts`)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// services/newsApi.ts
|
|
84
88
|
import { podSdk } from '@/lib/pod';
|
|
85
89
|
|
|
86
|
-
// 1. Define the
|
|
87
|
-
interface
|
|
90
|
+
// 1. Define the raw CMS Formatted Metadata schema (T)
|
|
91
|
+
export interface NewsArticleFormatted {
|
|
88
92
|
title: string;
|
|
89
93
|
coverImage: string; // Auto-transformed from hash to full Podspace CDN URL!
|
|
90
94
|
lead: string;
|
|
91
95
|
body: string;
|
|
92
|
-
|
|
96
|
+
author?: string;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
// 2.
|
|
96
|
-
interface
|
|
99
|
+
// 2. Define computed / normalized domain attributes (P)
|
|
100
|
+
export interface NewsArticleNormalized {
|
|
97
101
|
summary: string;
|
|
98
|
-
|
|
102
|
+
readTime: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Define the Clean Domain Model consumed by your React Components
|
|
106
|
+
export interface NewsItem {
|
|
107
|
+
id: number;
|
|
108
|
+
entityId: number;
|
|
109
|
+
title: string;
|
|
110
|
+
coverImage: string;
|
|
111
|
+
summary: string;
|
|
112
|
+
body: string;
|
|
113
|
+
readTime: number;
|
|
114
|
+
publishedAt?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface GetNewsListParams {
|
|
118
|
+
page?: number;
|
|
119
|
+
pageSize?: number;
|
|
120
|
+
tagTrees?: string[];
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
export
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
export class NewsApi {
|
|
124
|
+
private static readonly CONTENT_TYPE = 'company_news';
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetches published news articles with Next.js App Router ISR Caching
|
|
128
|
+
*/
|
|
129
|
+
public static async getNewsList({
|
|
130
|
+
page = 1,
|
|
131
|
+
pageSize = 10,
|
|
132
|
+
tagTrees,
|
|
133
|
+
}: GetNewsListParams = {}): Promise<{ items: NewsItem[]; total: number }> {
|
|
134
|
+
const offset = (page - 1) * pageSize;
|
|
135
|
+
|
|
136
|
+
// Call getContent with exact generics: <NewsArticleFormatted, NewsArticleNormalized>
|
|
137
|
+
const response = await podSdk.cms.getContent<NewsArticleFormatted, NewsArticleNormalized>(
|
|
138
|
+
{
|
|
139
|
+
contentTypeUniqueId: this.CONTENT_TYPE,
|
|
140
|
+
size: pageSize,
|
|
141
|
+
offset,
|
|
142
|
+
tagTrees,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
// Custom normalizer executed during data formatting
|
|
146
|
+
normalizer: (item) => ({
|
|
147
|
+
summary: item.formatted.lead || item.formatted.title,
|
|
148
|
+
readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
|
|
149
|
+
}),
|
|
150
|
+
revalidate: 1800, // Next.js ISR: cache for 30 minutes
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (response.hasError || !Array.isArray(response.result)) {
|
|
155
|
+
return { items: [], total: 0 };
|
|
117
156
|
}
|
|
118
|
-
|
|
157
|
+
|
|
158
|
+
const items: NewsItem[] = response.result.map((item) => ({
|
|
159
|
+
id: item.id,
|
|
160
|
+
entityId: item.entityId,
|
|
161
|
+
title: item.formatted.title,
|
|
162
|
+
coverImage: item.formatted.coverImage,
|
|
163
|
+
summary: item.__normalized.summary,
|
|
164
|
+
body: item.formatted.body,
|
|
165
|
+
readTime: item.__normalized.readTime,
|
|
166
|
+
publishedAt: item.timestamp,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
items,
|
|
171
|
+
total: response.count || items.length,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Fetches a single news article by its entity ID
|
|
177
|
+
*/
|
|
178
|
+
public static async getNewsById(entityId: number): Promise<NewsItem | null> {
|
|
179
|
+
const response = await podSdk.cms.getContentByEntityId<NewsArticleFormatted, NewsArticleNormalized>({
|
|
180
|
+
entityId,
|
|
181
|
+
contentTypeUniqueId: this.CONTENT_TYPE,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const item = response.result?.[0];
|
|
185
|
+
if (!item) return null;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
id: item.id,
|
|
189
|
+
entityId: item.entityId,
|
|
190
|
+
title: item.formatted.title,
|
|
191
|
+
coverImage: item.formatted.coverImage,
|
|
192
|
+
summary: item.formatted.lead || item.formatted.title,
|
|
193
|
+
body: item.formatted.body,
|
|
194
|
+
readTime: Math.max(1, Math.ceil((item.formatted.body?.length || 0) / 500)),
|
|
195
|
+
publishedAt: item.timestamp,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Publishes a new article to CMS
|
|
201
|
+
*/
|
|
202
|
+
public static async publishNews(data: {
|
|
203
|
+
title: string;
|
|
204
|
+
lead: string;
|
|
205
|
+
body: string;
|
|
206
|
+
imageHash: string;
|
|
207
|
+
}) {
|
|
208
|
+
return podSdk.cms.addContent({
|
|
209
|
+
contentTypeUniqueId: this.CONTENT_TYPE,
|
|
210
|
+
body: {
|
|
211
|
+
name: data.title,
|
|
212
|
+
fieldCode: ['title', 'lead', 'body', 'coverImage'],
|
|
213
|
+
fieldValue: [data.title, data.lead, data.body, data.imageHash],
|
|
214
|
+
canComment: true,
|
|
215
|
+
canLike: true,
|
|
216
|
+
enable: true,
|
|
217
|
+
isSearchable: true,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
#### Step 2: Consume `NewsApi` in Next.js Server Components (`app/news/page.tsx`)
|
|
227
|
+
|
|
228
|
+
Notice how clean the UI layer becomes! No low-level SDK parameters, headers, or raw hashes leaked into your views:
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// app/news/page.tsx (Server Component)
|
|
232
|
+
import { NewsApi } from '@/services/newsApi';
|
|
233
|
+
import Link from 'next/link';
|
|
234
|
+
|
|
235
|
+
interface NewsPageProps {
|
|
236
|
+
searchParams: { page?: string };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default async function NewsPage({ searchParams }: NewsPageProps) {
|
|
240
|
+
const page = Number(searchParams.page) || 1;
|
|
241
|
+
const { items, total } = await NewsApi.getNewsList({ page, pageSize: 12 });
|
|
119
242
|
|
|
120
243
|
return (
|
|
121
|
-
<
|
|
122
|
-
<h1 className="text-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{
|
|
244
|
+
<main className="container mx-auto py-8 px-4">
|
|
245
|
+
<h1 className="text-3xl font-bold mb-6">Company News</h1>
|
|
246
|
+
|
|
247
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
248
|
+
{items.map((news) => (
|
|
249
|
+
<article key={news.entityId} className="border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition">
|
|
250
|
+
{news.coverImage && (
|
|
128
251
|
<img
|
|
129
|
-
src={
|
|
130
|
-
alt={
|
|
131
|
-
className="w-full h-48 object-cover
|
|
252
|
+
src={news.coverImage}
|
|
253
|
+
alt={news.title}
|
|
254
|
+
className="w-full h-48 object-cover"
|
|
132
255
|
/>
|
|
133
256
|
)}
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
257
|
+
<div className="p-4">
|
|
258
|
+
<h2 className="text-xl font-semibold mb-2">
|
|
259
|
+
<Link href={`/news/${news.entityId}`}>{news.title}</Link>
|
|
260
|
+
</h2>
|
|
261
|
+
<p className="text-gray-600 text-sm line-clamp-2">{news.summary}</p>
|
|
262
|
+
<div className="mt-4 flex items-center justify-between text-xs text-gray-400">
|
|
263
|
+
<span>{news.readTime} min read</span>
|
|
264
|
+
<Link href={`/news/${news.entityId}`} className="text-blue-600 font-medium hover:underline">
|
|
265
|
+
Read more →
|
|
266
|
+
</Link>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
139
269
|
</article>
|
|
140
270
|
))}
|
|
141
271
|
</div>
|
|
142
|
-
</
|
|
272
|
+
</main>
|
|
143
273
|
);
|
|
144
274
|
}
|
|
145
275
|
```
|
|
146
276
|
|
|
147
|
-
|
|
277
|
+
---
|
|
148
278
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
279
|
+
#### Step 3: Detail Page with `NewsApi.getNewsById` (`app/news/[id]/page.tsx`)
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// app/news/[id]/page.tsx (Server Component)
|
|
283
|
+
import { NewsApi } from '@/services/newsApi';
|
|
152
284
|
import { notFound } from 'next/navigation';
|
|
153
285
|
|
|
154
|
-
interface
|
|
155
|
-
|
|
156
|
-
content: string;
|
|
157
|
-
author: string;
|
|
158
|
-
coverImage: string;
|
|
286
|
+
interface NewsDetailPageProps {
|
|
287
|
+
params: { id: string };
|
|
159
288
|
}
|
|
160
289
|
|
|
161
|
-
export default async function NewsDetailPage({ params }:
|
|
162
|
-
const
|
|
163
|
-
entityId: Number(params.id),
|
|
164
|
-
contentTypeUniqueId: 'company_news',
|
|
165
|
-
});
|
|
290
|
+
export default async function NewsDetailPage({ params }: NewsDetailPageProps) {
|
|
291
|
+
const news = await NewsApi.getNewsById(Number(params.id));
|
|
166
292
|
|
|
167
|
-
|
|
168
|
-
|
|
293
|
+
if (!news) {
|
|
294
|
+
notFound();
|
|
295
|
+
}
|
|
169
296
|
|
|
170
297
|
return (
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
298
|
+
<article className="max-w-3xl mx-auto py-10 px-4">
|
|
299
|
+
{news.coverImage && (
|
|
300
|
+
<img
|
|
301
|
+
src={news.coverImage}
|
|
302
|
+
alt={news.title}
|
|
303
|
+
className="w-full h-72 object-cover rounded-xl mb-6"
|
|
304
|
+
/>
|
|
305
|
+
)}
|
|
306
|
+
<h1 className="text-3xl font-bold mb-3">{news.title}</h1>
|
|
307
|
+
<p className="text-sm text-gray-500 mb-6">{news.readTime} min read</p>
|
|
308
|
+
<div className="prose lg:prose-lg" dangerouslySetInnerHTML={{ __html: news.body }} />
|
|
309
|
+
</article>
|
|
176
310
|
);
|
|
177
311
|
}
|
|
178
312
|
```
|
|
179
313
|
|
|
180
|
-
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
#### Step 4: Server Action for CMS Publishing (`app/actions/news.ts`)
|
|
181
317
|
|
|
182
318
|
```typescript
|
|
183
|
-
// app/actions/
|
|
319
|
+
// app/actions/news.ts
|
|
184
320
|
'use server';
|
|
185
321
|
|
|
186
|
-
import {
|
|
322
|
+
import { NewsApi } from '@/services/newsApi';
|
|
323
|
+
import { revalidatePath } from 'next/cache';
|
|
187
324
|
|
|
188
|
-
export async function
|
|
189
|
-
title
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
canComment: true,
|
|
200
|
-
canLike: true,
|
|
201
|
-
enable: true,
|
|
202
|
-
isSearchable: true,
|
|
203
|
-
},
|
|
325
|
+
export async function publishArticle(formData: FormData) {
|
|
326
|
+
const title = formData.get('title') as string;
|
|
327
|
+
const lead = formData.get('lead') as string;
|
|
328
|
+
const body = formData.get('body') as string;
|
|
329
|
+
const imageHash = formData.get('imageHash') as string;
|
|
330
|
+
|
|
331
|
+
const result = await NewsApi.publishNews({
|
|
332
|
+
title,
|
|
333
|
+
lead,
|
|
334
|
+
body,
|
|
335
|
+
imageHash,
|
|
204
336
|
});
|
|
337
|
+
|
|
338
|
+
revalidatePath('/news');
|
|
339
|
+
return { success: !result.hasError };
|
|
205
340
|
}
|
|
206
341
|
```
|
|
207
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rezamirzapour/pod-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Unified, type-safe enterprise SDK for POD Platform microservices (SSO, CustomPost, Podspace, Podform, Notification, Social, CMS, IUMS) powered by @rezamirzapour/http.",
|
|
5
5
|
"author": "Reza",
|
|
6
6
|
"license": "MIT",
|