@utilsy/cms-nextjs 0.1.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 ADDED
@@ -0,0 +1,157 @@
1
+ # @utilsy/cms-nextjs
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.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @utilsy/cms-nextjs
9
+ ```
10
+
11
+ Peer dependencies: `react` ^18 or ^19. `next` is optional (only needed if you pass `next.revalidate` to fetch).
12
+
13
+ ## Quick start
14
+
15
+ ### 1. Create a client
16
+
17
+ **Direct gateway-cms** (default path prefix):
18
+
19
+ ```ts
20
+ import { createCmsClient } from "@utilsy/cms-nextjs";
21
+
22
+ export const cms = createCmsClient({
23
+ baseUrl: process.env.NEXT_PUBLIC_CMS_BASE_URL!,
24
+ siteId: process.env.NEXT_PUBLIC_CMS_SITE_ID,
25
+ });
26
+ ```
27
+
28
+ **Main API gateway** (prefix before `/public/blog`):
29
+
30
+ ```ts
31
+ export const cms = createCmsClient({
32
+ baseUrl: process.env.NEXT_PUBLIC_CMS_BASE_URL!,
33
+ siteId: process.env.NEXT_PUBLIC_CMS_SITE_ID,
34
+ pathPrefix: "/api/backend/cms",
35
+ });
36
+ ```
37
+
38
+ ### 2. Server Component (list posts)
39
+
40
+ ```tsx
41
+ import { cms } from "@/lib/cms";
42
+
43
+ export default async function BlogPage() {
44
+ const result = await cms.blog.listMappedPosts(
45
+ { page: 1, limit: 20 },
46
+ { next: { revalidate: 60 } },
47
+ );
48
+
49
+ if (!result?.posts.length) {
50
+ return <p>No posts yet.</p>;
51
+ }
52
+
53
+ return (
54
+ <ul>
55
+ {result.posts.map((post) => (
56
+ <li key={post.postId}>
57
+ <a href={`/blog/${post.slug}`}>{post.title}</a>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### 3. Client engagement (likes & comments)
66
+
67
+ ```tsx
68
+ "use client";
69
+
70
+ import { CmsProvider, useBlogLike, useBlogComments } from "@utilsy/cms-nextjs/react";
71
+ import { cms } from "@/lib/cms";
72
+
73
+ export function PostEngagement({ postId }: { postId: string }) {
74
+ return (
75
+ <CmsProvider client={cms}>
76
+ <Engagement postId={postId} />
77
+ </CmsProvider>
78
+ );
79
+ }
80
+
81
+ function Engagement({ postId }: { postId: string }) {
82
+ const { liked, likeCount, toggle, toggling } = useBlogLike(postId);
83
+ const { comments, submitComment, submitting } = useBlogComments(postId);
84
+
85
+ return (
86
+ <div>
87
+ <button type="button" onClick={toggle} disabled={toggling}>
88
+ {liked ? "Unlike" : "Like"} ({likeCount})
89
+ </button>
90
+ <ul>
91
+ {comments.map((c) => (
92
+ <li key={c.id}>{c.name}: {c.message}</li>
93
+ ))}
94
+ </ul>
95
+ </div>
96
+ );
97
+ }
98
+ ```
99
+
100
+ ## Environment variables
101
+
102
+ | Variable | Description |
103
+ |----------|-------------|
104
+ | `NEXT_PUBLIC_CMS_BASE_URL` | Gateway or API base URL (no trailing slash) |
105
+ | `NEXT_PUBLIC_CMS_SITE_ID` | CMS site Mongo id — **recommended** when Host does not match a CMS domain |
106
+ | `NEXT_PUBLIC_CMS_PATH_PREFIX` | Optional, e.g. `/api/backend/cms` |
107
+
108
+ ## Site resolution
109
+
110
+ The CMS resolves the site from:
111
+
112
+ 1. `?siteId=` query param (SDK appends this when `siteId` is configured), or
113
+ 2. `Host` / `x-origin` header matching a registered domain.
114
+
115
+ For local dev or gateway-only deployments, always set `siteId` in the client config.
116
+
117
+ ## API reference
118
+
119
+ ### Client (`createCmsClient`)
120
+
121
+ | Method | Description |
122
+ |--------|-------------|
123
+ | `blog.listPosts(query?, init?)` | Paginated list (summary fields only) |
124
+ | `blog.getPostBySlug(slug, init?)` | Full post DTO |
125
+ | `blog.listCategories(init?)` | Active categories |
126
+ | `blog.listComments(postId, init?)` | Approved comments |
127
+ | `blog.createComment(postId, body)` | Create comment (throws on error) |
128
+ | `blog.getEngagement(postId, { visitorId? }, init?)` | Stats + `likedByMe` |
129
+ | `blog.toggleLike(postId, { visitorId? })` | Toggle like |
130
+ | `blog.listMappedPosts(...)` | List + `mapCmsPostToBlogPost` |
131
+ | `blog.getMappedPostBySlug(...)` | Single mapped `BlogPost` |
132
+ | `blog.listMappedCategories(...)` | Mapped categories |
133
+ | `blog.listMappedComments(...)` | Mapped comments |
134
+
135
+ ### React (`@utilsy/cms-nextjs/react`)
136
+
137
+ | Export | Description |
138
+ |--------|-------------|
139
+ | `CmsProvider` | Context for hooks |
140
+ | `useCmsClient` | Access client instance |
141
+ | `useVisitorId` / `getVisitorId` | Anonymous visitor id for likes |
142
+ | `useBlogEngagement` | Load engagement stats |
143
+ | `useBlogLike` | Like toggle + counts |
144
+ | `useBlogComments` | List + submit comments |
145
+
146
+ ## CORS
147
+
148
+ Public blog reads from the browser require either:
149
+
150
+ - Same-origin proxy (e.g. Next.js Route Handler forwarding to gateway), or
151
+ - Server Components / Route Handlers calling the SDK server-side.
152
+
153
+ Mutations (comments, likes) should run client-side with a proxy or configured CORS on the gateway.
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,184 @@
1
+ type CmsApiResponse<T> = {
2
+ message?: string;
3
+ data?: T;
4
+ success?: boolean;
5
+ };
6
+ type FetchRequestInit = RequestInit & {
7
+ next?: {
8
+ revalidate?: number | false;
9
+ tags?: string[];
10
+ };
11
+ cache?: RequestCache;
12
+ };
13
+
14
+ /** Wire DTO from CMS public blog API */
15
+ type CmsBlogPostDto = {
16
+ id: string;
17
+ slug?: string;
18
+ title?: string;
19
+ excerpt?: string;
20
+ content?: string;
21
+ data?: Record<string, unknown>;
22
+ tags?: string[];
23
+ categories?: Array<{
24
+ id: string;
25
+ name: string;
26
+ slug: string;
27
+ }>;
28
+ stats?: {
29
+ viewCount: number;
30
+ likeCount: number;
31
+ commentCount: number;
32
+ };
33
+ createdAt?: string;
34
+ published_at?: string;
35
+ featured_image?: unknown;
36
+ };
37
+ type CmsBlogCommentDto = {
38
+ id: string;
39
+ name: string;
40
+ message: string;
41
+ avatarUrl?: string;
42
+ postedAt?: string;
43
+ replyComment?: Array<{
44
+ id: string;
45
+ userId: string;
46
+ message: string;
47
+ postedAt?: string;
48
+ }>;
49
+ };
50
+ type CmsBlogCategoryDto = {
51
+ id: string;
52
+ name: string;
53
+ slug: string;
54
+ description?: string;
55
+ sortOrder?: number;
56
+ };
57
+ type CmsBlogPostsPage = {
58
+ docs: CmsBlogPostDto[];
59
+ total: number;
60
+ page?: number;
61
+ limit?: number;
62
+ };
63
+ type BlogAuthor = {
64
+ name: string;
65
+ avatarInitials?: string;
66
+ };
67
+ type BlogPost = {
68
+ postId: string;
69
+ slug: string;
70
+ title: string;
71
+ excerpt: string;
72
+ category: string;
73
+ tags: string[];
74
+ publishedAt: string;
75
+ readMinutes: number;
76
+ author: BlogAuthor;
77
+ imageSrc?: string;
78
+ imageAlt?: string;
79
+ body: string;
80
+ stats?: {
81
+ viewCount: number;
82
+ likeCount: number;
83
+ commentCount: number;
84
+ };
85
+ };
86
+ type BlogCommentReply = {
87
+ id: string;
88
+ userId: string;
89
+ message: string;
90
+ postedAt?: string;
91
+ };
92
+ type BlogComment = {
93
+ id: string;
94
+ name: string;
95
+ message: string;
96
+ avatarUrl?: string;
97
+ postedAt?: string;
98
+ replies: BlogCommentReply[];
99
+ };
100
+ type BlogCategory = {
101
+ id: string;
102
+ name: string;
103
+ slug: string;
104
+ description?: string;
105
+ sortOrder?: number;
106
+ };
107
+ type BlogEngagement = {
108
+ viewCount: number;
109
+ likeCount: number;
110
+ commentCount: number;
111
+ likedByMe: boolean;
112
+ };
113
+ type BlogLikeResult = {
114
+ liked: boolean;
115
+ likeCount: number;
116
+ };
117
+ type CreateBlogCommentInput = {
118
+ message: string;
119
+ guestName?: string;
120
+ guestEmail?: string;
121
+ parentId?: string;
122
+ };
123
+ type CreateBlogCommentResult = {
124
+ id: string;
125
+ status: string;
126
+ };
127
+ type ListBlogPostsQuery = {
128
+ page?: number;
129
+ limit?: number;
130
+ search?: string;
131
+ tag?: string;
132
+ };
133
+
134
+ type BlogRequestContext = {
135
+ baseUrl: string;
136
+ blogBasePath: string;
137
+ siteId?: string;
138
+ fetchFn: typeof fetch;
139
+ defaultHeaders: Record<string, string>;
140
+ };
141
+ declare function createBlogApi(ctx: BlogRequestContext): {
142
+ listPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<CmsBlogPostsPage | null>;
143
+ getPostBySlug(slug: string, init?: FetchRequestInit): Promise<CmsBlogPostDto | null>;
144
+ listCategories(init?: FetchRequestInit): Promise<CmsBlogCategoryDto[] | null>;
145
+ listComments(postId: string, init?: FetchRequestInit): Promise<CmsBlogCommentDto[] | null>;
146
+ createComment(postId: string, body: CreateBlogCommentInput): Promise<CreateBlogCommentResult>;
147
+ getEngagement(postId: string, options?: {
148
+ visitorId?: string;
149
+ }, init?: FetchRequestInit): Promise<BlogEngagement | null>;
150
+ toggleLike(postId: string, options?: {
151
+ visitorId?: string;
152
+ }): Promise<BlogLikeResult>;
153
+ listMappedPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<{
154
+ posts: BlogPost[];
155
+ total: number;
156
+ page: number | undefined;
157
+ limit: number | undefined;
158
+ } | null>;
159
+ getMappedPostBySlug(slug: string, init?: FetchRequestInit): Promise<BlogPost | null>;
160
+ listMappedCategories(init?: FetchRequestInit): Promise<BlogCategory[] | null>;
161
+ listMappedComments(postId: string, init?: FetchRequestInit): Promise<BlogComment[] | null>;
162
+ };
163
+ type BlogApi = ReturnType<typeof createBlogApi>;
164
+
165
+ type CmsClientConfig = {
166
+ /** Base URL, e.g. https://cms-gateway.example.com */
167
+ baseUrl: string;
168
+ /** CMS site Mongo id — appended as ?siteId= on every blog request */
169
+ siteId?: string;
170
+ /**
171
+ * Path prefix before /public/blog.
172
+ * Default '' for gateway-cms direct (/public/blog/...).
173
+ * Use '/api/backend/cms' when routing through the main API gateway.
174
+ */
175
+ pathPrefix?: string;
176
+ fetch?: typeof fetch;
177
+ defaultHeaders?: Record<string, string>;
178
+ };
179
+ type CmsClient = {
180
+ blog: BlogApi;
181
+ };
182
+ declare function createCmsClient(config: CmsClientConfig): CmsClient;
183
+
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 };
@@ -0,0 +1,184 @@
1
+ type CmsApiResponse<T> = {
2
+ message?: string;
3
+ data?: T;
4
+ success?: boolean;
5
+ };
6
+ type FetchRequestInit = RequestInit & {
7
+ next?: {
8
+ revalidate?: number | false;
9
+ tags?: string[];
10
+ };
11
+ cache?: RequestCache;
12
+ };
13
+
14
+ /** Wire DTO from CMS public blog API */
15
+ type CmsBlogPostDto = {
16
+ id: string;
17
+ slug?: string;
18
+ title?: string;
19
+ excerpt?: string;
20
+ content?: string;
21
+ data?: Record<string, unknown>;
22
+ tags?: string[];
23
+ categories?: Array<{
24
+ id: string;
25
+ name: string;
26
+ slug: string;
27
+ }>;
28
+ stats?: {
29
+ viewCount: number;
30
+ likeCount: number;
31
+ commentCount: number;
32
+ };
33
+ createdAt?: string;
34
+ published_at?: string;
35
+ featured_image?: unknown;
36
+ };
37
+ type CmsBlogCommentDto = {
38
+ id: string;
39
+ name: string;
40
+ message: string;
41
+ avatarUrl?: string;
42
+ postedAt?: string;
43
+ replyComment?: Array<{
44
+ id: string;
45
+ userId: string;
46
+ message: string;
47
+ postedAt?: string;
48
+ }>;
49
+ };
50
+ type CmsBlogCategoryDto = {
51
+ id: string;
52
+ name: string;
53
+ slug: string;
54
+ description?: string;
55
+ sortOrder?: number;
56
+ };
57
+ type CmsBlogPostsPage = {
58
+ docs: CmsBlogPostDto[];
59
+ total: number;
60
+ page?: number;
61
+ limit?: number;
62
+ };
63
+ type BlogAuthor = {
64
+ name: string;
65
+ avatarInitials?: string;
66
+ };
67
+ type BlogPost = {
68
+ postId: string;
69
+ slug: string;
70
+ title: string;
71
+ excerpt: string;
72
+ category: string;
73
+ tags: string[];
74
+ publishedAt: string;
75
+ readMinutes: number;
76
+ author: BlogAuthor;
77
+ imageSrc?: string;
78
+ imageAlt?: string;
79
+ body: string;
80
+ stats?: {
81
+ viewCount: number;
82
+ likeCount: number;
83
+ commentCount: number;
84
+ };
85
+ };
86
+ type BlogCommentReply = {
87
+ id: string;
88
+ userId: string;
89
+ message: string;
90
+ postedAt?: string;
91
+ };
92
+ type BlogComment = {
93
+ id: string;
94
+ name: string;
95
+ message: string;
96
+ avatarUrl?: string;
97
+ postedAt?: string;
98
+ replies: BlogCommentReply[];
99
+ };
100
+ type BlogCategory = {
101
+ id: string;
102
+ name: string;
103
+ slug: string;
104
+ description?: string;
105
+ sortOrder?: number;
106
+ };
107
+ type BlogEngagement = {
108
+ viewCount: number;
109
+ likeCount: number;
110
+ commentCount: number;
111
+ likedByMe: boolean;
112
+ };
113
+ type BlogLikeResult = {
114
+ liked: boolean;
115
+ likeCount: number;
116
+ };
117
+ type CreateBlogCommentInput = {
118
+ message: string;
119
+ guestName?: string;
120
+ guestEmail?: string;
121
+ parentId?: string;
122
+ };
123
+ type CreateBlogCommentResult = {
124
+ id: string;
125
+ status: string;
126
+ };
127
+ type ListBlogPostsQuery = {
128
+ page?: number;
129
+ limit?: number;
130
+ search?: string;
131
+ tag?: string;
132
+ };
133
+
134
+ type BlogRequestContext = {
135
+ baseUrl: string;
136
+ blogBasePath: string;
137
+ siteId?: string;
138
+ fetchFn: typeof fetch;
139
+ defaultHeaders: Record<string, string>;
140
+ };
141
+ declare function createBlogApi(ctx: BlogRequestContext): {
142
+ listPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<CmsBlogPostsPage | null>;
143
+ getPostBySlug(slug: string, init?: FetchRequestInit): Promise<CmsBlogPostDto | null>;
144
+ listCategories(init?: FetchRequestInit): Promise<CmsBlogCategoryDto[] | null>;
145
+ listComments(postId: string, init?: FetchRequestInit): Promise<CmsBlogCommentDto[] | null>;
146
+ createComment(postId: string, body: CreateBlogCommentInput): Promise<CreateBlogCommentResult>;
147
+ getEngagement(postId: string, options?: {
148
+ visitorId?: string;
149
+ }, init?: FetchRequestInit): Promise<BlogEngagement | null>;
150
+ toggleLike(postId: string, options?: {
151
+ visitorId?: string;
152
+ }): Promise<BlogLikeResult>;
153
+ listMappedPosts(query?: ListBlogPostsQuery, init?: FetchRequestInit): Promise<{
154
+ posts: BlogPost[];
155
+ total: number;
156
+ page: number | undefined;
157
+ limit: number | undefined;
158
+ } | null>;
159
+ getMappedPostBySlug(slug: string, init?: FetchRequestInit): Promise<BlogPost | null>;
160
+ listMappedCategories(init?: FetchRequestInit): Promise<BlogCategory[] | null>;
161
+ listMappedComments(postId: string, init?: FetchRequestInit): Promise<BlogComment[] | null>;
162
+ };
163
+ type BlogApi = ReturnType<typeof createBlogApi>;
164
+
165
+ type CmsClientConfig = {
166
+ /** Base URL, e.g. https://cms-gateway.example.com */
167
+ baseUrl: string;
168
+ /** CMS site Mongo id — appended as ?siteId= on every blog request */
169
+ siteId?: string;
170
+ /**
171
+ * Path prefix before /public/blog.
172
+ * Default '' for gateway-cms direct (/public/blog/...).
173
+ * Use '/api/backend/cms' when routing through the main API gateway.
174
+ */
175
+ pathPrefix?: string;
176
+ fetch?: typeof fetch;
177
+ defaultHeaders?: Record<string, string>;
178
+ };
179
+ type CmsClient = {
180
+ blog: BlogApi;
181
+ };
182
+ declare function createCmsClient(config: CmsClientConfig): CmsClient;
183
+
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 };