@utilsy/cms-nextjs 0.3.0 → 0.5.0

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
@@ -164,7 +164,68 @@ export function ContactForm() {
164
164
 
165
165
  Optional honeypot: include `_honeypot` in `data`; submissions with a non-empty value are rejected.
166
166
 
167
- ### 6. Client content list (hooks)
167
+ ### 6. Newsletter (subscribe, unsubscribe, preferences)
168
+
169
+ Use your own signup / preference-center UI—no hosted form embed.
170
+
171
+ ```ts
172
+ // app/actions/newsletter.ts
173
+ "use server";
174
+
175
+ import { cms } from "@/lib/cms";
176
+
177
+ export async function subscribeNewsletter(email: string) {
178
+ return cms.newsletter.subscribe({ email, source: "footer" });
179
+ }
180
+
181
+ export async function unsubscribeNewsletter(email: string) {
182
+ return cms.newsletter.unsubscribe({ email });
183
+ }
184
+
185
+ export async function updateNewsletterPrefs(
186
+ token: string,
187
+ customFields: Record<string, unknown>,
188
+ ) {
189
+ return cms.newsletter.updatePreferences({ token, customFields });
190
+ }
191
+ ```
192
+
193
+ ```tsx
194
+ "use client";
195
+
196
+ import { CmsProvider, useNewsletterSubscribe } from "@utilsy/cms-nextjs/react";
197
+ import { cms } from "@/lib/cms";
198
+
199
+ export function NewsletterSignup() {
200
+ return (
201
+ <CmsProvider client={cms}>
202
+ <NewsletterSignupInner />
203
+ </CmsProvider>
204
+ );
205
+ }
206
+
207
+ function NewsletterSignupInner() {
208
+ const { subscribe, submitting, error } = useNewsletterSubscribe();
209
+
210
+ return (
211
+ <form
212
+ onSubmit={async (e) => {
213
+ e.preventDefault();
214
+ const fd = new FormData(e.currentTarget);
215
+ await subscribe({ email: String(fd.get("email") ?? "") });
216
+ }}
217
+ >
218
+ <input name="email" type="email" required />
219
+ <button type="submit" disabled={submitting}>Subscribe</button>
220
+ {error && <p>{error.message}</p>}
221
+ </form>
222
+ );
223
+ }
224
+ ```
225
+
226
+ Confirmation links from email hit `GET /public/newsletter/confirm/:token` (no `siteId` required on the server; the SDK still sends `siteId` when configured).
227
+
228
+ ### 7. Client content list (hooks)
168
229
 
169
230
  ```tsx
170
231
  "use client";
@@ -223,6 +284,41 @@ For local dev or gateway-only deployments, always set `siteId` in the client con
223
284
  - `filters` are exact matches on `data.<field>` (JSON query param). Example: `{ category: "news" }`.
224
285
  - Use `contentTypeApiId` (kebab-case slug from CMS), not the Mongo content type id.
225
286
 
287
+ ### COMPONENT and DYNAMIC_ZONE fields
288
+
289
+ Published entry `data` may include nested structures from the CMS builder:
290
+
291
+ | Field type | Shape in `entry.data` |
292
+ |------------|------------------------|
293
+ | **COMPONENT** (single) | `{ title: "…", … }` |
294
+ | **COMPONENT** (repeatable) | `[{ … }, { … }]` |
295
+ | **DYNAMIC_ZONE** | `[{ __component: "<apiId>", …fields }, …]` |
296
+
297
+ Helpers (no extra API calls):
298
+
299
+ ```ts
300
+ import {
301
+ getComponentItems,
302
+ getDynamicZoneBlocks,
303
+ filterDynamicZoneByComponent,
304
+ parseContentTypeFields,
305
+ } from "@utilsy/cms-nextjs";
306
+
307
+ const entry = await cms.content.getMappedEntry("landing", "home");
308
+ if (!entry) return;
309
+
310
+ const slides = getComponentItems(entry, "slides");
311
+ const ctas = filterDynamicZoneByComponent(
312
+ getDynamicZoneBlocks(entry, "blocks"),
313
+ "cta",
314
+ );
315
+
316
+ // Optional: inspect schema from populated contentType on the entry DTO
317
+ const fields = parseContentTypeFields(entry.contentType?.fields);
318
+ ```
319
+
320
+ Types: `FieldType`, `ContentTypeField`, `DynamicZoneBlock`, `ContentTypeCategory`.
321
+
226
322
  ## API reference
227
323
 
228
324
  ### Client (`createCmsClient`)
@@ -251,10 +347,25 @@ For local dev or gateway-only deployments, always set `siteId` in the client con
251
347
  | `content.getEntry(contentTypeApiId, idOrSlug, init?)` | Single raw DTO |
252
348
  | `content.listMappedEntries(...)` | List + `mapCmsEntryToContentEntry` |
253
349
  | `content.getMappedEntry(...)` | Single mapped `ContentEntry` |
350
+ | `content.getSingleMappedEntry(...)` | First mapped entry for SINGLE types (`limit=1`) |
351
+
352
+ Query params for `listEntries` / `listMappedEntries`: `page`, `limit`, `search`, `sort`, `order`, `filters` (object). For **SINGLE** content types the public API caps `limit` at 1 and may include `category: "SINGLE"` and `maxEntries: 1` in the list response.
353
+
354
+ #### Single content types
355
+
356
+ Use a CMS content type with category **SINGLE** for one document per site (homepage settings, global SEO, etc.). Admin creates the type and edits the auto-created draft on the **Content** tab.
357
+
358
+ ```tsx
359
+ // Prefer for singleton types — no entry id/slug required
360
+ const settings = await client.content.getSingleMappedEntry("site-settings");
254
361
 
255
- Query params for `listEntries` / `listMappedEntries`: `page`, `limit`, `search`, `sort`, `order`, `filters` (object).
362
+ // Or with React
363
+ const { entry, loading } = useSingleContentEntry("site-settings");
364
+ ```
365
+
366
+ `listEntries` / `listMappedEntries` still work; pass `limit: 1` or use `getSingleMappedEntry`.
256
367
 
257
- Helpers: `serializeContentFilters`, `mapCmsEntryToContentEntry`.
368
+ Helpers: `serializeContentFilters`, `mapCmsEntryToContentEntry`, `getComponentItems`, `getDynamicZoneBlocks`, `filterDynamicZoneByComponent`, `parseContentTypeFields`.
258
369
 
259
370
  #### Leads
260
371
 
@@ -266,6 +377,20 @@ Helpers: `serializeContentFilters`, `mapCmsEntryToContentEntry`.
266
377
  - Content type must be `LEAD_CAPTURE` and `apiAccess: PUBLIC`
267
378
  - Field keys in `data` must match the content type schema; CRM mapping uses `leadCaptureConfig` in admin
268
379
 
380
+ #### Newsletter
381
+
382
+ | Method | Description |
383
+ |--------|-------------|
384
+ | `newsletter.subscribe(input, init?)` | Subscribe by email; may send confirmation email |
385
+ | `newsletter.unsubscribe(input, init?)` | Unsubscribe by `token` (from email) or `email` |
386
+ | `newsletter.confirm(token, init?)` | Confirm subscription from email link |
387
+ | `newsletter.updatePreferences(input, init?)` | Update `customFields`, `tags`, and/or `listId` |
388
+
389
+ - Base path: `/public/newsletter` (plus optional `pathPrefix`)
390
+ - `subscribe`: `email` required; optional `source`, `listId`, `customFields`
391
+ - `unsubscribe` / `updatePreferences`: provide `token` or `email`
392
+ - `updatePreferences`: at least one of `customFields`, `tags`, or `listId`; optional `tagsAction` (`set` \| `add` \| `remove`)
393
+
269
394
  ### React (`@utilsy/cms-nextjs/react`)
270
395
 
271
396
  | Export | Description |
@@ -278,7 +403,12 @@ Helpers: `serializeContentFilters`, `mapCmsEntryToContentEntry`.
278
403
  | `useBlogComments` | List + submit comments |
279
404
  | `useContentEntries` | List mapped entries by content type |
280
405
  | `useContentEntry` | Single mapped entry by id or slug |
406
+ | `useSingleContentEntry` | Mapped entry for SINGLE types (no id/slug) |
281
407
  | `useSubmitLead` | Submit lead from client (prefer Server Action for forms) |
408
+ | `useNewsletterSubscribe` | Subscribe from client (prefer Server Action) |
409
+ | `useNewsletterUnsubscribe` | Unsubscribe by token or email |
410
+ | `useNewsletterUpdatePreferences` | Update subscriber preferences |
411
+ | `useNewsletterConfirm` | Confirm subscription token (e.g. preference page) |
282
412
 
283
413
  ## CORS
284
414
 
@@ -287,7 +417,7 @@ Public CMS reads from the browser require either:
287
417
  - Same-origin proxy (e.g. Next.js Route Handler forwarding to gateway), or
288
418
  - Server Components / Route Handlers calling the SDK server-side.
289
419
 
290
- Mutations (comments, likes, lead submit) should run server-side (Server Action / Route Handler) or via a same-origin proxy with CORS configured on the gateway.
420
+ Mutations (comments, likes, lead submit, newsletter subscribe/unsubscribe/preferences) should run server-side (Server Action / Route Handler) or via a same-origin proxy with CORS configured on the gateway.
291
421
 
292
422
  ## License
293
423
 
@@ -181,6 +181,8 @@ type CmsContentTypeDto = {
181
181
  id?: string;
182
182
  name?: string;
183
183
  apiId?: string;
184
+ /** COLLECTION (many entries), SINGLE (one entry), or COMPONENT (embedded schema only). */
185
+ category?: string;
184
186
  fields?: unknown[];
185
187
  status?: string;
186
188
  };
@@ -199,11 +201,15 @@ type CmsContentEntriesPage = {
199
201
  message?: string;
200
202
  docs: CmsContentEntryDto[];
201
203
  totalDocs: number;
204
+ /** Present when the content type category is SINGLE (at most one entry per site). */
205
+ category?: string;
206
+ maxEntries?: number;
202
207
  };
203
208
  type ContentTypeMeta = {
204
209
  id: string;
205
210
  name: string;
206
211
  apiId: string;
212
+ category?: string;
207
213
  fields?: unknown[];
208
214
  status?: string;
209
215
  };
@@ -219,6 +225,8 @@ type ContentEntry = {
219
225
  type MappedContentEntriesPage = {
220
226
  entries: ContentEntry[];
221
227
  totalDocs: number;
228
+ category?: string;
229
+ maxEntries?: number;
222
230
  };
223
231
 
224
232
  type ContentRequestContext = RequestContext & {
@@ -229,6 +237,7 @@ declare function createContentApi(ctx: ContentRequestContext): {
229
237
  getEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<CmsContentEntryDto | null>;
230
238
  listMappedEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<MappedContentEntriesPage | null>;
231
239
  getMappedEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
240
+ getSingleMappedEntry(contentTypeApiId: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
232
241
  };
233
242
  type ContentApi = ReturnType<typeof createContentApi>;
234
243
 
@@ -250,14 +259,65 @@ declare function createLeadsApi(ctx: LeadsRequestContext): {
250
259
  };
251
260
  type LeadsApi = ReturnType<typeof createLeadsApi>;
252
261
 
262
+ type NewsletterSubscriberSummary = {
263
+ id: string;
264
+ email?: string;
265
+ status?: string;
266
+ tags?: string[];
267
+ customFields?: Record<string, unknown>;
268
+ };
269
+ type NewsletterActionResult = {
270
+ ok: boolean;
271
+ message: string;
272
+ subscriberId?: string;
273
+ email?: string;
274
+ subscriber?: NewsletterSubscriberSummary;
275
+ };
276
+ type SubscribeNewsletterInput = {
277
+ email: string;
278
+ source?: string;
279
+ listId?: string;
280
+ customFields?: Record<string, unknown>;
281
+ };
282
+ type UnsubscribeNewsletterInput = {
283
+ /** Token from confirmation or unsubscribe email link */
284
+ token?: string;
285
+ email?: string;
286
+ };
287
+ type UpdateNewsletterPreferencesInput = {
288
+ token?: string;
289
+ email?: string;
290
+ /** Merged into existing customFields on the server */
291
+ customFields?: Record<string, unknown>;
292
+ tags?: string[];
293
+ tagsAction?: "set" | "add" | "remove";
294
+ listId?: string;
295
+ };
296
+ type ConfirmNewsletterResult = {
297
+ ok: boolean;
298
+ message: string;
299
+ email?: string;
300
+ };
301
+
302
+ type NewsletterRequestContext = RequestContext & {
303
+ newsletterBasePath: string;
304
+ };
305
+ declare function createNewsletterApi(ctx: NewsletterRequestContext): {
306
+ subscribe(input: SubscribeNewsletterInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
307
+ unsubscribe(input: UnsubscribeNewsletterInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
308
+ confirm(token: string, init?: FetchRequestInit): Promise<ConfirmNewsletterResult>;
309
+ updatePreferences(input: UpdateNewsletterPreferencesInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
310
+ };
311
+ type NewsletterApi = ReturnType<typeof createNewsletterApi>;
312
+
253
313
  type CmsClientConfig = {
254
314
  /** Base URL, e.g. https://cms-gateway.example.com */
255
315
  baseUrl: string;
256
316
  /** CMS site Mongo id — appended as ?siteId= on every public request */
257
317
  siteId?: string;
258
318
  /**
259
- * Path prefix before /public/blog and /public/api.
260
- * Default '' for gateway-cms direct (/public/blog/..., /public/api/...).
319
+ * Path prefix before /public/blog, /public/api, and /public/newsletter.
320
+ * Default '' for gateway-cms direct (/public/blog/..., etc.).
261
321
  * Use '/api/backend/cms' when routing through the main API gateway.
262
322
  */
263
323
  pathPrefix?: string;
@@ -268,7 +328,8 @@ type CmsClient = {
268
328
  blog: BlogApi;
269
329
  content: ContentApi;
270
330
  leads: LeadsApi;
331
+ newsletter: NewsletterApi;
271
332
  };
272
333
  declare function createCmsClient(config: CmsClientConfig): CmsClient;
273
334
 
274
- export { createCmsClient as A, type BlogApi as B, type CmsApiResponse as C, serializeContentFilters as D, type FetchRequestInit as F, type LeadSubmissionData as L, type MappedContentEntriesPage as M, type SubmitLeadOptions as S, type BlogAuthor as a, type BlogCategory as b, type BlogComment as c, type BlogCommentReply as d, type BlogEngagement as e, type BlogLikeResult as f, type BlogPost as g, type CmsBlogCategoryDto as h, type CmsBlogCommentDto as i, type CmsBlogPostDto as j, type CmsBlogPostsPage as k, type CmsClient as l, type CmsClientConfig as m, type CmsContentEntriesPage as n, type CmsContentEntryDto as o, type CmsContentTypeDto as p, type ContentApi as q, type ContentEntry as r, type ContentFilters as s, type ContentTypeMeta as t, type CreateBlogCommentInput as u, type CreateBlogCommentResult as v, type LeadsApi as w, type ListBlogPostsQuery as x, type ListContentEntriesQuery as y, type SubmitLeadResult as z };
335
+ export { type NewsletterApi as A, type BlogApi as B, type CmsApiResponse as C, type NewsletterSubscriberSummary as D, type SubmitLeadResult as E, type FetchRequestInit as F, type SubscribeNewsletterInput as G, type UpdateNewsletterPreferencesInput as H, createCmsClient as I, serializeContentFilters as J, type LeadSubmissionData as L, type MappedContentEntriesPage as M, type NewsletterActionResult as N, type SubmitLeadOptions as S, type UnsubscribeNewsletterInput as U, type BlogAuthor as a, type BlogCategory as b, type BlogComment as c, type BlogCommentReply as d, type BlogEngagement as e, type BlogLikeResult as f, type BlogPost as g, type CmsBlogCategoryDto as h, type CmsBlogCommentDto as i, type CmsBlogPostDto as j, type CmsBlogPostsPage as k, type CmsClient as l, type CmsClientConfig as m, type CmsContentEntriesPage as n, type CmsContentEntryDto as o, type CmsContentTypeDto as p, type ConfirmNewsletterResult as q, type ContentApi as r, type ContentEntry as s, type ContentFilters as t, type ContentTypeMeta as u, type CreateBlogCommentInput as v, type CreateBlogCommentResult as w, type LeadsApi as x, type ListBlogPostsQuery as y, type ListContentEntriesQuery as z };
@@ -181,6 +181,8 @@ type CmsContentTypeDto = {
181
181
  id?: string;
182
182
  name?: string;
183
183
  apiId?: string;
184
+ /** COLLECTION (many entries), SINGLE (one entry), or COMPONENT (embedded schema only). */
185
+ category?: string;
184
186
  fields?: unknown[];
185
187
  status?: string;
186
188
  };
@@ -199,11 +201,15 @@ type CmsContentEntriesPage = {
199
201
  message?: string;
200
202
  docs: CmsContentEntryDto[];
201
203
  totalDocs: number;
204
+ /** Present when the content type category is SINGLE (at most one entry per site). */
205
+ category?: string;
206
+ maxEntries?: number;
202
207
  };
203
208
  type ContentTypeMeta = {
204
209
  id: string;
205
210
  name: string;
206
211
  apiId: string;
212
+ category?: string;
207
213
  fields?: unknown[];
208
214
  status?: string;
209
215
  };
@@ -219,6 +225,8 @@ type ContentEntry = {
219
225
  type MappedContentEntriesPage = {
220
226
  entries: ContentEntry[];
221
227
  totalDocs: number;
228
+ category?: string;
229
+ maxEntries?: number;
222
230
  };
223
231
 
224
232
  type ContentRequestContext = RequestContext & {
@@ -229,6 +237,7 @@ declare function createContentApi(ctx: ContentRequestContext): {
229
237
  getEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<CmsContentEntryDto | null>;
230
238
  listMappedEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<MappedContentEntriesPage | null>;
231
239
  getMappedEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
240
+ getSingleMappedEntry(contentTypeApiId: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
232
241
  };
233
242
  type ContentApi = ReturnType<typeof createContentApi>;
234
243
 
@@ -250,14 +259,65 @@ declare function createLeadsApi(ctx: LeadsRequestContext): {
250
259
  };
251
260
  type LeadsApi = ReturnType<typeof createLeadsApi>;
252
261
 
262
+ type NewsletterSubscriberSummary = {
263
+ id: string;
264
+ email?: string;
265
+ status?: string;
266
+ tags?: string[];
267
+ customFields?: Record<string, unknown>;
268
+ };
269
+ type NewsletterActionResult = {
270
+ ok: boolean;
271
+ message: string;
272
+ subscriberId?: string;
273
+ email?: string;
274
+ subscriber?: NewsletterSubscriberSummary;
275
+ };
276
+ type SubscribeNewsletterInput = {
277
+ email: string;
278
+ source?: string;
279
+ listId?: string;
280
+ customFields?: Record<string, unknown>;
281
+ };
282
+ type UnsubscribeNewsletterInput = {
283
+ /** Token from confirmation or unsubscribe email link */
284
+ token?: string;
285
+ email?: string;
286
+ };
287
+ type UpdateNewsletterPreferencesInput = {
288
+ token?: string;
289
+ email?: string;
290
+ /** Merged into existing customFields on the server */
291
+ customFields?: Record<string, unknown>;
292
+ tags?: string[];
293
+ tagsAction?: "set" | "add" | "remove";
294
+ listId?: string;
295
+ };
296
+ type ConfirmNewsletterResult = {
297
+ ok: boolean;
298
+ message: string;
299
+ email?: string;
300
+ };
301
+
302
+ type NewsletterRequestContext = RequestContext & {
303
+ newsletterBasePath: string;
304
+ };
305
+ declare function createNewsletterApi(ctx: NewsletterRequestContext): {
306
+ subscribe(input: SubscribeNewsletterInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
307
+ unsubscribe(input: UnsubscribeNewsletterInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
308
+ confirm(token: string, init?: FetchRequestInit): Promise<ConfirmNewsletterResult>;
309
+ updatePreferences(input: UpdateNewsletterPreferencesInput, init?: FetchRequestInit): Promise<NewsletterActionResult>;
310
+ };
311
+ type NewsletterApi = ReturnType<typeof createNewsletterApi>;
312
+
253
313
  type CmsClientConfig = {
254
314
  /** Base URL, e.g. https://cms-gateway.example.com */
255
315
  baseUrl: string;
256
316
  /** CMS site Mongo id — appended as ?siteId= on every public request */
257
317
  siteId?: string;
258
318
  /**
259
- * Path prefix before /public/blog and /public/api.
260
- * Default '' for gateway-cms direct (/public/blog/..., /public/api/...).
319
+ * Path prefix before /public/blog, /public/api, and /public/newsletter.
320
+ * Default '' for gateway-cms direct (/public/blog/..., etc.).
261
321
  * Use '/api/backend/cms' when routing through the main API gateway.
262
322
  */
263
323
  pathPrefix?: string;
@@ -268,7 +328,8 @@ type CmsClient = {
268
328
  blog: BlogApi;
269
329
  content: ContentApi;
270
330
  leads: LeadsApi;
331
+ newsletter: NewsletterApi;
271
332
  };
272
333
  declare function createCmsClient(config: CmsClientConfig): CmsClient;
273
334
 
274
- export { createCmsClient as A, type BlogApi as B, type CmsApiResponse as C, serializeContentFilters as D, type FetchRequestInit as F, type LeadSubmissionData as L, type MappedContentEntriesPage as M, type SubmitLeadOptions as S, type BlogAuthor as a, type BlogCategory as b, type BlogComment as c, type BlogCommentReply as d, type BlogEngagement as e, type BlogLikeResult as f, type BlogPost as g, type CmsBlogCategoryDto as h, type CmsBlogCommentDto as i, type CmsBlogPostDto as j, type CmsBlogPostsPage as k, type CmsClient as l, type CmsClientConfig as m, type CmsContentEntriesPage as n, type CmsContentEntryDto as o, type CmsContentTypeDto as p, type ContentApi as q, type ContentEntry as r, type ContentFilters as s, type ContentTypeMeta as t, type CreateBlogCommentInput as u, type CreateBlogCommentResult as v, type LeadsApi as w, type ListBlogPostsQuery as x, type ListContentEntriesQuery as y, type SubmitLeadResult as z };
335
+ export { type NewsletterApi as A, type BlogApi as B, type CmsApiResponse as C, type NewsletterSubscriberSummary as D, type SubmitLeadResult as E, type FetchRequestInit as F, type SubscribeNewsletterInput as G, type UpdateNewsletterPreferencesInput as H, createCmsClient as I, serializeContentFilters as J, type LeadSubmissionData as L, type MappedContentEntriesPage as M, type NewsletterActionResult as N, type SubmitLeadOptions as S, type UnsubscribeNewsletterInput as U, type BlogAuthor as a, type BlogCategory as b, type BlogComment as c, type BlogCommentReply as d, type BlogEngagement as e, type BlogLikeResult as f, type BlogPost as g, type CmsBlogCategoryDto as h, type CmsBlogCommentDto as i, type CmsBlogPostDto as j, type CmsBlogPostsPage as k, type CmsClient as l, type CmsClientConfig as m, type CmsContentEntriesPage as n, type CmsContentEntryDto as o, type CmsContentTypeDto as p, type ConfirmNewsletterResult as q, type ContentApi as r, type ContentEntry as s, type ContentFilters as t, type ContentTypeMeta as u, type CreateBlogCommentInput as v, type CreateBlogCommentResult as w, type LeadsApi as x, type ListBlogPostsQuery as y, type ListContentEntriesQuery as z };
package/dist/index.cjs CHANGED
@@ -41,6 +41,19 @@ async function publicGetNullable(ctx, path, init) {
41
41
  return null;
42
42
  }
43
43
  }
44
+ async function publicPost(ctx, path, body, init) {
45
+ const res = await ctx.fetchFn(buildPublicUrl(ctx, path), {
46
+ ...init,
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ ...ctx.defaultHeaders,
51
+ ...init?.headers
52
+ },
53
+ body: JSON.stringify(body)
54
+ });
55
+ return unwrapResponse(res);
56
+ }
44
57
 
45
58
  // src/blog/mappers.ts
46
59
  function mediaUrl(value) {
@@ -226,6 +239,7 @@ function mapContentType(dto) {
226
239
  id,
227
240
  name: dto.name ?? "",
228
241
  apiId: dto.apiId ?? "",
242
+ category: dto.category,
229
243
  fields: dto.fields,
230
244
  status: dto.status
231
245
  };
@@ -276,13 +290,23 @@ function createContentApi(ctx) {
276
290
  if (!page?.docs) return null;
277
291
  return {
278
292
  entries: page.docs.map(mapCmsEntryToContentEntry),
279
- totalDocs: page.totalDocs
293
+ totalDocs: page.totalDocs,
294
+ ...page.category ? { category: page.category } : {},
295
+ ...page.maxEntries != null ? { maxEntries: page.maxEntries } : {}
280
296
  };
281
297
  },
282
298
  async getMappedEntry(contentTypeApiId, idOrSlug, init) {
283
299
  const dto = await this.getEntry(contentTypeApiId, idOrSlug, init);
284
300
  if (!dto) return null;
285
301
  return mapCmsEntryToContentEntry(dto);
302
+ },
303
+ async getSingleMappedEntry(contentTypeApiId, init) {
304
+ const page = await this.listMappedEntries(
305
+ contentTypeApiId,
306
+ { limit: 1, page: 1 },
307
+ init
308
+ );
309
+ return page?.entries?.[0] ?? null;
286
310
  }
287
311
  };
288
312
  }
@@ -315,6 +339,29 @@ function createLeadsApi(ctx) {
315
339
  };
316
340
  }
317
341
 
342
+ // src/newsletter/endpoints.ts
343
+ function createNewsletterApi(ctx) {
344
+ const prefix = ctx.newsletterBasePath;
345
+ return {
346
+ subscribe(input, init) {
347
+ return publicPost(ctx, `${prefix}/subscribe`, input, init);
348
+ },
349
+ unsubscribe(input, init) {
350
+ const body = {};
351
+ if (input.token) body.token = input.token;
352
+ if (input.email) body.email = input.email;
353
+ return publicPost(ctx, `${prefix}/unsubscribe`, body, init);
354
+ },
355
+ confirm(token, init) {
356
+ const path = `${prefix}/confirm/${encodeURIComponent(token)}`;
357
+ return publicGet(ctx, path, init);
358
+ },
359
+ updatePreferences(input, init) {
360
+ return publicPost(ctx, `${prefix}/preferences`, input, init);
361
+ }
362
+ };
363
+ }
364
+
318
365
  // src/client.ts
319
366
  function normalizeBaseUrl(url) {
320
367
  return url.replace(/\/$/, "");
@@ -329,6 +376,7 @@ function createCmsClient(config) {
329
376
  const pathPrefix = normalizePathPrefix(config.pathPrefix ?? "");
330
377
  const blogBasePath = `${pathPrefix}/public/blog`.replace(/\/+/g, "/");
331
378
  const contentBasePath = `${pathPrefix}/public/api`.replace(/\/+/g, "/");
379
+ const newsletterBasePath = `${pathPrefix}/public/newsletter`.replace(/\/+/g, "/");
332
380
  const ctx = {
333
381
  baseUrl,
334
382
  siteId: config.siteId,
@@ -338,16 +386,112 @@ function createCmsClient(config) {
338
386
  return {
339
387
  blog: createBlogApi({ ...ctx, blogBasePath }),
340
388
  content: createContentApi({ ...ctx, contentBasePath }),
341
- leads: createLeadsApi({ ...ctx, contentBasePath })
389
+ leads: createLeadsApi({ ...ctx, contentBasePath }),
390
+ newsletter: createNewsletterApi({ ...ctx, newsletterBasePath })
342
391
  };
343
392
  }
344
393
 
394
+ // src/content/field-types.ts
395
+ var ContentTypeCategory = {
396
+ COLLECTION: "COLLECTION",
397
+ SINGLE: "SINGLE",
398
+ COMPONENT: "COMPONENT"
399
+ };
400
+ var FieldType = {
401
+ TEXT: "TEXT",
402
+ TEXTAREA: "TEXTAREA",
403
+ RICHTEXT: "RICHTEXT",
404
+ NUMBER: "NUMBER",
405
+ BOOLEAN: "BOOLEAN",
406
+ DATE: "DATE",
407
+ DATETIME: "DATETIME",
408
+ TIME: "TIME",
409
+ EMAIL: "EMAIL",
410
+ URL: "URL",
411
+ MEDIA: "MEDIA",
412
+ RELATION: "RELATION",
413
+ JSON: "JSON",
414
+ ENUM: "ENUM",
415
+ PHONE: "PHONE",
416
+ WEEKDAY: "WEEKDAY",
417
+ COMPONENT: "COMPONENT",
418
+ DYNAMIC_ZONE: "DYNAMIC_ZONE",
419
+ PAGE_EDITOR: "PAGE_EDITOR"
420
+ };
421
+
422
+ // src/content/structured-data.ts
423
+ function isPlainObject(value) {
424
+ return value != null && typeof value === "object" && !Array.isArray(value);
425
+ }
426
+ function parseContentTypeFields(raw) {
427
+ if (!Array.isArray(raw)) return [];
428
+ const out = [];
429
+ for (const item of raw) {
430
+ if (!isPlainObject(item)) continue;
431
+ const name = item.name;
432
+ const type = item.type;
433
+ if (typeof name !== "string" || !name.trim()) continue;
434
+ if (typeof type !== "string" || !type.trim()) continue;
435
+ out.push(item);
436
+ }
437
+ return out;
438
+ }
439
+ function findContentTypeField(fields, fieldName) {
440
+ return fields?.find((f) => f.name === fieldName);
441
+ }
442
+ function getEntryFieldValue(entry, fieldName) {
443
+ return entry.data[fieldName];
444
+ }
445
+ function getComponentItems(entry, fieldName) {
446
+ const value = entry.data[fieldName];
447
+ if (value == null) return [];
448
+ if (Array.isArray(value)) {
449
+ return value.filter(isPlainObject);
450
+ }
451
+ if (isPlainObject(value)) {
452
+ return [value];
453
+ }
454
+ return [];
455
+ }
456
+ function getComponentItem(entry, fieldName) {
457
+ const items = getComponentItems(entry, fieldName);
458
+ return items[0];
459
+ }
460
+ function getDynamicZoneBlocks(entry, fieldName) {
461
+ const value = entry.data[fieldName];
462
+ if (!Array.isArray(value)) return [];
463
+ return value.filter(
464
+ (item) => isPlainObject(item) && typeof item.__component === "string" && item.__component.length > 0
465
+ );
466
+ }
467
+ function filterDynamicZoneByComponent(blocks, componentApiId) {
468
+ return blocks.filter((b) => b.__component === componentApiId);
469
+ }
470
+ function isRepeatableComponentField(field) {
471
+ return field.type === FieldType.COMPONENT && field.component?.repeatable === true;
472
+ }
473
+ function isDynamicZoneField(field) {
474
+ return field.type === FieldType.DYNAMIC_ZONE;
475
+ }
476
+
477
+ exports.ContentTypeCategory = ContentTypeCategory;
478
+ exports.FieldType = FieldType;
345
479
  exports.createCmsClient = createCmsClient;
480
+ exports.filterDynamicZoneByComponent = filterDynamicZoneByComponent;
481
+ exports.findContentTypeField = findContentTypeField;
482
+ exports.getComponentItem = getComponentItem;
483
+ exports.getComponentItems = getComponentItems;
484
+ exports.getDynamicZoneBlocks = getDynamicZoneBlocks;
485
+ exports.getEntryFieldValue = getEntryFieldValue;
486
+ exports.isDynamicZoneField = isDynamicZoneField;
487
+ exports.isPlainObject = isPlainObject;
488
+ exports.isRepeatableComponentField = isRepeatableComponentField;
346
489
  exports.mapCmsCategoryToBlogCategory = mapCmsCategoryToBlogCategory;
347
490
  exports.mapCmsCommentToBlogComment = mapCmsCommentToBlogComment;
348
491
  exports.mapCmsEntryToContentEntry = mapCmsEntryToContentEntry;
349
492
  exports.mapCmsPostToBlogPost = mapCmsPostToBlogPost;
350
493
  exports.mediaUrl = mediaUrl;
494
+ exports.parseContentTypeFields = parseContentTypeFields;
351
495
  exports.serializeContentFilters = serializeContentFilters;
352
496
  //# sourceMappingURL=index.cjs.map
353
497
  //# sourceMappingURL=index.cjs.map