@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.
Files changed (2) hide show
  1. package/README.md +217 -82
  2. 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
- ### 1. Defining Types and Fetching CMS Articles with `getContent<T, P>`
80
+ ### 🏗️ Clean Architecture Pattern: Dedicated Data Access Layer (`NewsApi`)
81
81
 
82
- ```tsx
83
- // app/news/page.tsx (Server Component)
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 formatted structure of your CMS content type
87
- interface NewsArticle {
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
- publishDate: string;
96
+ author?: string;
93
97
  }
94
98
 
95
- // 2. Optional: Define normalized helper fields computed on the fly
96
- interface NewsNormalized {
99
+ // 2. Define computed / normalized domain attributes (P)
100
+ export interface NewsArticleNormalized {
97
101
  summary: string;
98
- readTimeEstimate: number;
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 default async function NewsPage() {
102
- // Call getContent with exact generic parameters: <T, P>
103
- const response = await podSdk.cms.getContent<NewsArticle, NewsNormalized>(
104
- {
105
- contentTypeUniqueId: 'company_news',
106
- size: 10,
107
- offset: 0,
108
- },
109
- {
110
- // Optional: compute custom normalized fields
111
- normalizer: (item) => ({
112
- summary: item.formatted.lead || item.formatted.title,
113
- readTimeEstimate: Math.ceil((item.formatted.body?.length || 0) / 500),
114
- }),
115
- // Next.js ISR options
116
- revalidate: 1800,
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
- <div className="container mx-auto p-6">
122
- <h1 className="text-2xl font-bold mb-4">Latest News</h1>
123
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
124
- {response.result.map((item) => (
125
- <article key={item.id} className="border rounded-lg p-4 shadow">
126
- {/* item.formatted is 100% typed as NewsArticle */}
127
- {item.formatted.coverImage && (
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={item.formatted.coverImage}
130
- alt={item.formatted.title}
131
- className="w-full h-48 object-cover rounded"
252
+ src={news.coverImage}
253
+ alt={news.title}
254
+ className="w-full h-48 object-cover"
132
255
  />
133
256
  )}
134
- <h2 className="text-xl font-semibold mt-2">{item.formatted.title}</h2>
135
- <p className="text-gray-600 mt-1">{item.__normalized.summary}</p>
136
- <span className="text-xs text-gray-400">
137
- Estimated read: {item.__normalized.readTimeEstimate} min
138
- </span>
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
- </div>
272
+ </main>
143
273
  );
144
274
  }
145
275
  ```
146
276
 
147
- ### 2. Fetching Single Content by Entity ID: `getContentByEntityId<T>`
277
+ ---
148
278
 
149
- ```typescript
150
- // app/news/[id]/page.tsx
151
- import { podSdk } from '@/lib/pod';
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 ArticleDetail {
155
- title: string;
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 }: { params: { id: string } }) {
162
- const res = await podSdk.cms.getContentByEntityId<ArticleDetail>({
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
- const article = res.result?.[0];
168
- if (!article) notFound();
293
+ if (!news) {
294
+ notFound();
295
+ }
169
296
 
170
297
  return (
171
- <main>
172
- <h1>{article.formatted.title}</h1>
173
- <p>By {article.formatted.author}</p>
174
- <div dangerouslySetInnerHTML={{ __html: article.formatted.content }} />
175
- </main>
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
- ### 3. Adding and Editing CMS Content: `addContent<T>` & `editContent<T>`
314
+ ---
315
+
316
+ #### Step 4: Server Action for CMS Publishing (`app/actions/news.ts`)
181
317
 
182
318
  ```typescript
183
- // app/actions/cms.ts
319
+ // app/actions/news.ts
184
320
  'use server';
185
321
 
186
- import { podSdk } from '@/lib/pod';
322
+ import { NewsApi } from '@/services/newsApi';
323
+ import { revalidatePath } from 'next/cache';
187
324
 
188
- export async function createNewsArticle(data: {
189
- title: string;
190
- body: string;
191
- imageHash: string;
192
- }) {
193
- return podSdk.cms.addContent({
194
- contentTypeUniqueId: 'company_news',
195
- body: {
196
- name: data.title,
197
- fieldCode: ['title', 'body', 'coverImage'],
198
- fieldValue: [data.title, data.body, data.imageHash],
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.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",