@utilsy/cms-nextjs 0.1.0 → 0.3.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
@@ -1,6 +1,6 @@
1
1
  # @utilsy/cms-nextjs
2
2
 
3
- Headless Next.js SDK for [Utilsy gateway-cms](https://github.com/utilsy/utilsy-cms-nextjs-sdk) public blog APIs. Provides a typed HTTP client, domain mappers, server-friendly fetch helpers, and React hooks—bring your own UI.
3
+ Headless Next.js SDK for [Utilsy gateway-cms](https://github.com/utilsy/utilsy-cms-nextjs-sdk) public CMS APIs: blog (`/public/blog`) and dynamic content types (`/public/api`). Provides a typed HTTP client, domain mappers, server-friendly fetch helpers, and React data hooks—bring your own UI.
4
4
 
5
5
  ## Install
6
6
 
@@ -25,7 +25,7 @@ export const cms = createCmsClient({
25
25
  });
26
26
  ```
27
27
 
28
- **Main API gateway** (prefix before `/public/blog`):
28
+ **Main API gateway** (prefix before `/public/blog` and `/public/api`):
29
29
 
30
30
  ```ts
31
31
  export const cms = createCmsClient({
@@ -62,7 +62,35 @@ export default async function BlogPage() {
62
62
  }
63
63
  ```
64
64
 
65
- ### 3. Client engagement (likes & comments)
65
+ ### 3. Server Component (filtered content by type)
66
+
67
+ ```tsx
68
+ import { cms } from "@/lib/cms";
69
+
70
+ export default async function ServicesPage() {
71
+ const result = await cms.content.listMappedEntries(
72
+ "services",
73
+ { page: 1, limit: 12, filters: { featured: true } },
74
+ { next: { revalidate: 60, tags: ["cms:services"] } },
75
+ );
76
+
77
+ if (!result?.entries.length) {
78
+ return <p>No services yet.</p>;
79
+ }
80
+
81
+ return (
82
+ <ul>
83
+ {result.entries.map((entry) => (
84
+ <li key={entry.id}>
85
+ <a href={`/services/${entry.data.slug}`}>{String(entry.data.title ?? "")}</a>
86
+ </li>
87
+ ))}
88
+ </ul>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### 4. Client engagement (likes & comments)
66
94
 
67
95
  ```tsx
68
96
  "use client";
@@ -97,6 +125,80 @@ function Engagement({ postId }: { postId: string }) {
97
125
  }
98
126
  ```
99
127
 
128
+ ### 5. Lead capture (custom form via Server Action)
129
+
130
+ Use a **LEAD_CAPTURE** content type in CMS (with field mapping configured). Submit from your own form UI—no hosted-form embed.
131
+
132
+ ```ts
133
+ // app/actions/submit-lead.ts
134
+ "use server";
135
+
136
+ import { cms } from "@/lib/cms";
137
+
138
+ export async function submitContactLead(data: Record<string, unknown>) {
139
+ return cms.leads.submit("contact-enquiries", data);
140
+ }
141
+ ```
142
+
143
+ ```tsx
144
+ "use client";
145
+
146
+ import { submitContactLead } from "@/app/actions/submit-lead";
147
+
148
+ export function ContactForm() {
149
+ return (
150
+ <form
151
+ action={async (formData) => {
152
+ await submitContactLead({
153
+ firstName: String(formData.get("firstName") ?? ""),
154
+ email: String(formData.get("email") ?? ""),
155
+ message: String(formData.get("message") ?? ""),
156
+ });
157
+ }}
158
+ >
159
+ {/* your fields */}
160
+ </form>
161
+ );
162
+ }
163
+ ```
164
+
165
+ Optional honeypot: include `_honeypot` in `data`; submissions with a non-empty value are rejected.
166
+
167
+ ### 6. Client content list (hooks)
168
+
169
+ ```tsx
170
+ "use client";
171
+
172
+ import { CmsProvider, useContentEntries } from "@utilsy/cms-nextjs/react";
173
+ import { cms } from "@/lib/cms";
174
+
175
+ export function ServicesList() {
176
+ return (
177
+ <CmsProvider client={cms}>
178
+ <ServicesListInner />
179
+ </CmsProvider>
180
+ );
181
+ }
182
+
183
+ function ServicesListInner() {
184
+ const { entries, loading, error } = useContentEntries("services", {
185
+ page: 1,
186
+ limit: 20,
187
+ });
188
+
189
+ if (loading) return <p>Loading…</p>;
190
+ if (error) return <p>Error: {error.message}</p>;
191
+
192
+ return (
193
+ <ul>
194
+ {entries.map((e) => (
195
+ <li key={e.id}>{String(e.data.title ?? "")}</li>
196
+ ))}
197
+ </ul>
198
+ );
199
+ }
200
+ ```
201
+
100
202
  ## Environment variables
101
203
 
102
204
  | Variable | Description |
@@ -114,10 +216,19 @@ The CMS resolves the site from:
114
216
 
115
217
  For local dev or gateway-only deployments, always set `siteId` in the client config.
116
218
 
219
+ ## Content types and filters
220
+
221
+ - Endpoint: `GET /public/api/{contentTypeApiId}` (published entries only).
222
+ - Content types must have `apiAccess: PUBLIC`.
223
+ - `filters` are exact matches on `data.<field>` (JSON query param). Example: `{ category: "news" }`.
224
+ - Use `contentTypeApiId` (kebab-case slug from CMS), not the Mongo content type id.
225
+
117
226
  ## API reference
118
227
 
119
228
  ### Client (`createCmsClient`)
120
229
 
230
+ #### Blog
231
+
121
232
  | Method | Description |
122
233
  |--------|-------------|
123
234
  | `blog.listPosts(query?, init?)` | Paginated list (summary fields only) |
@@ -132,6 +243,29 @@ For local dev or gateway-only deployments, always set `siteId` in the client con
132
243
  | `blog.listMappedCategories(...)` | Mapped categories |
133
244
  | `blog.listMappedComments(...)` | Mapped comments |
134
245
 
246
+ #### Content
247
+
248
+ | Method | Description |
249
+ |--------|-------------|
250
+ | `content.listEntries(contentTypeApiId, query?, init?)` | Paginated raw DTOs |
251
+ | `content.getEntry(contentTypeApiId, idOrSlug, init?)` | Single raw DTO |
252
+ | `content.listMappedEntries(...)` | List + `mapCmsEntryToContentEntry` |
253
+ | `content.getMappedEntry(...)` | Single mapped `ContentEntry` |
254
+
255
+ Query params for `listEntries` / `listMappedEntries`: `page`, `limit`, `search`, `sort`, `order`, `filters` (object).
256
+
257
+ Helpers: `serializeContentFilters`, `mapCmsEntryToContentEntry`.
258
+
259
+ #### Leads
260
+
261
+ | Method | Description |
262
+ |--------|-------------|
263
+ | `leads.submit(contentTypeApiId, data, options?)` | Submit lead fields; creates content entry + CRM enquiry (throws on error) |
264
+
265
+ - Endpoint: `POST /public/api/{contentTypeApiId}/submissions`
266
+ - Content type must be `LEAD_CAPTURE` and `apiAccess: PUBLIC`
267
+ - Field keys in `data` must match the content type schema; CRM mapping uses `leadCaptureConfig` in admin
268
+
135
269
  ### React (`@utilsy/cms-nextjs/react`)
136
270
 
137
271
  | Export | Description |
@@ -142,15 +276,18 @@ For local dev or gateway-only deployments, always set `siteId` in the client con
142
276
  | `useBlogEngagement` | Load engagement stats |
143
277
  | `useBlogLike` | Like toggle + counts |
144
278
  | `useBlogComments` | List + submit comments |
279
+ | `useContentEntries` | List mapped entries by content type |
280
+ | `useContentEntry` | Single mapped entry by id or slug |
281
+ | `useSubmitLead` | Submit lead from client (prefer Server Action for forms) |
145
282
 
146
283
  ## CORS
147
284
 
148
- Public blog reads from the browser require either:
285
+ Public CMS reads from the browser require either:
149
286
 
150
287
  - Same-origin proxy (e.g. Next.js Route Handler forwarding to gateway), or
151
288
  - Server Components / Route Handlers calling the SDK server-side.
152
289
 
153
- Mutations (comments, likes) should run client-side with a proxy or configured CORS on the gateway.
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.
154
291
 
155
292
  ## License
156
293
 
@@ -11,6 +11,13 @@ type FetchRequestInit = RequestInit & {
11
11
  cache?: RequestCache;
12
12
  };
13
13
 
14
+ type RequestContext = {
15
+ baseUrl: string;
16
+ siteId?: string;
17
+ fetchFn: typeof fetch;
18
+ defaultHeaders: Record<string, string>;
19
+ };
20
+
14
21
  /** Wire DTO from CMS public blog API */
15
22
  type CmsBlogPostDto = {
16
23
  id: string;
@@ -131,12 +138,8 @@ type ListBlogPostsQuery = {
131
138
  tag?: string;
132
139
  };
133
140
 
134
- type BlogRequestContext = {
135
- baseUrl: string;
141
+ type BlogRequestContext = RequestContext & {
136
142
  blogBasePath: string;
137
- siteId?: string;
138
- fetchFn: typeof fetch;
139
- defaultHeaders: Record<string, string>;
140
143
  };
141
144
  declare function createBlogApi(ctx: BlogRequestContext): {
142
145
  listPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<CmsBlogPostsPage | null>;
@@ -162,14 +165,99 @@ declare function createBlogApi(ctx: BlogRequestContext): {
162
165
  };
163
166
  type BlogApi = ReturnType<typeof createBlogApi>;
164
167
 
168
+ type ContentFilters = Record<string, unknown>;
169
+ declare function serializeContentFilters(filters: ContentFilters): string;
170
+
171
+ type ListContentEntriesQuery = {
172
+ page?: number;
173
+ limit?: number;
174
+ search?: string;
175
+ sort?: string;
176
+ order?: "asc" | "desc";
177
+ filters?: ContentFilters;
178
+ };
179
+ type CmsContentTypeDto = {
180
+ _id?: string;
181
+ id?: string;
182
+ name?: string;
183
+ apiId?: string;
184
+ fields?: unknown[];
185
+ status?: string;
186
+ };
187
+ type CmsContentEntryDto = {
188
+ _id?: string;
189
+ id?: string;
190
+ contentTypeId?: string;
191
+ contentType?: CmsContentTypeDto;
192
+ siteId?: string;
193
+ status?: string;
194
+ data?: Record<string, unknown>;
195
+ createdAt?: string;
196
+ updatedAt?: string;
197
+ };
198
+ type CmsContentEntriesPage = {
199
+ message?: string;
200
+ docs: CmsContentEntryDto[];
201
+ totalDocs: number;
202
+ };
203
+ type ContentTypeMeta = {
204
+ id: string;
205
+ name: string;
206
+ apiId: string;
207
+ fields?: unknown[];
208
+ status?: string;
209
+ };
210
+ type ContentEntry = {
211
+ id: string;
212
+ contentTypeId: string;
213
+ contentType?: ContentTypeMeta;
214
+ status: string;
215
+ data: Record<string, unknown>;
216
+ createdAt?: string;
217
+ updatedAt?: string;
218
+ };
219
+ type MappedContentEntriesPage = {
220
+ entries: ContentEntry[];
221
+ totalDocs: number;
222
+ };
223
+
224
+ type ContentRequestContext = RequestContext & {
225
+ contentBasePath: string;
226
+ };
227
+ declare function createContentApi(ctx: ContentRequestContext): {
228
+ listEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<CmsContentEntriesPage | null>;
229
+ getEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<CmsContentEntryDto | null>;
230
+ listMappedEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<MappedContentEntriesPage | null>;
231
+ getMappedEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
232
+ };
233
+ type ContentApi = ReturnType<typeof createContentApi>;
234
+
235
+ type LeadSubmissionData = Record<string, unknown>;
236
+ type SubmitLeadOptions = {
237
+ /** Entry status (defaults to PUBLISHED on the server) */
238
+ status?: "DRAFT" | "PUBLISHED" | "ARCHIVED";
239
+ };
240
+ type SubmitLeadResult = {
241
+ entryId: string;
242
+ enquiryId?: string;
243
+ };
244
+
245
+ type LeadsRequestContext = RequestContext & {
246
+ contentBasePath: string;
247
+ };
248
+ declare function createLeadsApi(ctx: LeadsRequestContext): {
249
+ submit(contentTypeApiId: string, data: LeadSubmissionData, options?: SubmitLeadOptions, init?: FetchRequestInit): Promise<SubmitLeadResult>;
250
+ };
251
+ type LeadsApi = ReturnType<typeof createLeadsApi>;
252
+
165
253
  type CmsClientConfig = {
166
254
  /** Base URL, e.g. https://cms-gateway.example.com */
167
255
  baseUrl: string;
168
- /** CMS site Mongo id — appended as ?siteId= on every blog request */
256
+ /** CMS site Mongo id — appended as ?siteId= on every public request */
169
257
  siteId?: string;
170
258
  /**
171
- * Path prefix before /public/blog.
172
- * Default '' for gateway-cms direct (/public/blog/...).
259
+ * Path prefix before /public/blog and /public/api.
260
+ * Default '' for gateway-cms direct (/public/blog/..., /public/api/...).
173
261
  * Use '/api/backend/cms' when routing through the main API gateway.
174
262
  */
175
263
  pathPrefix?: string;
@@ -178,7 +266,9 @@ type CmsClientConfig = {
178
266
  };
179
267
  type CmsClient = {
180
268
  blog: BlogApi;
269
+ content: ContentApi;
270
+ leads: LeadsApi;
181
271
  };
182
272
  declare function createCmsClient(config: CmsClientConfig): CmsClient;
183
273
 
184
- export { type BlogApi as B, type CmsApiResponse as C, type FetchRequestInit as F, type ListBlogPostsQuery as L, 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 CreateBlogCommentInput as n, type CreateBlogCommentResult as o, createCmsClient as p };
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 };
@@ -11,6 +11,13 @@ type FetchRequestInit = RequestInit & {
11
11
  cache?: RequestCache;
12
12
  };
13
13
 
14
+ type RequestContext = {
15
+ baseUrl: string;
16
+ siteId?: string;
17
+ fetchFn: typeof fetch;
18
+ defaultHeaders: Record<string, string>;
19
+ };
20
+
14
21
  /** Wire DTO from CMS public blog API */
15
22
  type CmsBlogPostDto = {
16
23
  id: string;
@@ -131,12 +138,8 @@ type ListBlogPostsQuery = {
131
138
  tag?: string;
132
139
  };
133
140
 
134
- type BlogRequestContext = {
135
- baseUrl: string;
141
+ type BlogRequestContext = RequestContext & {
136
142
  blogBasePath: string;
137
- siteId?: string;
138
- fetchFn: typeof fetch;
139
- defaultHeaders: Record<string, string>;
140
143
  };
141
144
  declare function createBlogApi(ctx: BlogRequestContext): {
142
145
  listPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<CmsBlogPostsPage | null>;
@@ -162,14 +165,99 @@ declare function createBlogApi(ctx: BlogRequestContext): {
162
165
  };
163
166
  type BlogApi = ReturnType<typeof createBlogApi>;
164
167
 
168
+ type ContentFilters = Record<string, unknown>;
169
+ declare function serializeContentFilters(filters: ContentFilters): string;
170
+
171
+ type ListContentEntriesQuery = {
172
+ page?: number;
173
+ limit?: number;
174
+ search?: string;
175
+ sort?: string;
176
+ order?: "asc" | "desc";
177
+ filters?: ContentFilters;
178
+ };
179
+ type CmsContentTypeDto = {
180
+ _id?: string;
181
+ id?: string;
182
+ name?: string;
183
+ apiId?: string;
184
+ fields?: unknown[];
185
+ status?: string;
186
+ };
187
+ type CmsContentEntryDto = {
188
+ _id?: string;
189
+ id?: string;
190
+ contentTypeId?: string;
191
+ contentType?: CmsContentTypeDto;
192
+ siteId?: string;
193
+ status?: string;
194
+ data?: Record<string, unknown>;
195
+ createdAt?: string;
196
+ updatedAt?: string;
197
+ };
198
+ type CmsContentEntriesPage = {
199
+ message?: string;
200
+ docs: CmsContentEntryDto[];
201
+ totalDocs: number;
202
+ };
203
+ type ContentTypeMeta = {
204
+ id: string;
205
+ name: string;
206
+ apiId: string;
207
+ fields?: unknown[];
208
+ status?: string;
209
+ };
210
+ type ContentEntry = {
211
+ id: string;
212
+ contentTypeId: string;
213
+ contentType?: ContentTypeMeta;
214
+ status: string;
215
+ data: Record<string, unknown>;
216
+ createdAt?: string;
217
+ updatedAt?: string;
218
+ };
219
+ type MappedContentEntriesPage = {
220
+ entries: ContentEntry[];
221
+ totalDocs: number;
222
+ };
223
+
224
+ type ContentRequestContext = RequestContext & {
225
+ contentBasePath: string;
226
+ };
227
+ declare function createContentApi(ctx: ContentRequestContext): {
228
+ listEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<CmsContentEntriesPage | null>;
229
+ getEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<CmsContentEntryDto | null>;
230
+ listMappedEntries(contentTypeApiId: string, query?: ListContentEntriesQuery, init?: FetchRequestInit): Promise<MappedContentEntriesPage | null>;
231
+ getMappedEntry(contentTypeApiId: string, idOrSlug: string, init?: FetchRequestInit): Promise<ContentEntry | null>;
232
+ };
233
+ type ContentApi = ReturnType<typeof createContentApi>;
234
+
235
+ type LeadSubmissionData = Record<string, unknown>;
236
+ type SubmitLeadOptions = {
237
+ /** Entry status (defaults to PUBLISHED on the server) */
238
+ status?: "DRAFT" | "PUBLISHED" | "ARCHIVED";
239
+ };
240
+ type SubmitLeadResult = {
241
+ entryId: string;
242
+ enquiryId?: string;
243
+ };
244
+
245
+ type LeadsRequestContext = RequestContext & {
246
+ contentBasePath: string;
247
+ };
248
+ declare function createLeadsApi(ctx: LeadsRequestContext): {
249
+ submit(contentTypeApiId: string, data: LeadSubmissionData, options?: SubmitLeadOptions, init?: FetchRequestInit): Promise<SubmitLeadResult>;
250
+ };
251
+ type LeadsApi = ReturnType<typeof createLeadsApi>;
252
+
165
253
  type CmsClientConfig = {
166
254
  /** Base URL, e.g. https://cms-gateway.example.com */
167
255
  baseUrl: string;
168
- /** CMS site Mongo id — appended as ?siteId= on every blog request */
256
+ /** CMS site Mongo id — appended as ?siteId= on every public request */
169
257
  siteId?: string;
170
258
  /**
171
- * Path prefix before /public/blog.
172
- * Default '' for gateway-cms direct (/public/blog/...).
259
+ * Path prefix before /public/blog and /public/api.
260
+ * Default '' for gateway-cms direct (/public/blog/..., /public/api/...).
173
261
  * Use '/api/backend/cms' when routing through the main API gateway.
174
262
  */
175
263
  pathPrefix?: string;
@@ -178,7 +266,9 @@ type CmsClientConfig = {
178
266
  };
179
267
  type CmsClient = {
180
268
  blog: BlogApi;
269
+ content: ContentApi;
270
+ leads: LeadsApi;
181
271
  };
182
272
  declare function createCmsClient(config: CmsClientConfig): CmsClient;
183
273
 
184
- export { type BlogApi as B, type CmsApiResponse as C, type FetchRequestInit as F, type ListBlogPostsQuery as L, 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 CreateBlogCommentInput as n, type CreateBlogCommentResult as o, createCmsClient as p };
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 };