@ecomcx/ecomweb-theme-sdk 1.28.0 → 1.29.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/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PostContentDoc, CommentPage, Menu, Post, ResolveResult, Site, Term, WidgetInstance } from "./types.js";
1
+ import type { Author, PostContentDoc, CommentPage, Media, Menu, Post, ResolveResult, Site, Term, WidgetInstance } from "./types.js";
2
2
  /**
3
3
  * Client gọi Public API của CMS (dùng next-fetch-kit: chạy được cả SSR lẫn CSR,
4
4
  * tự gộp request trùng trong cùng một lần render, timeout + retry khi lỗi mạng).
@@ -86,10 +86,21 @@ export interface PostQuery {
86
86
  sticky?: boolean;
87
87
  /** Kèm nội dung đầy đủ (post.content). Mặc định không — danh sách chỉ cần tiêu đề, sapo, ảnh, giúp trang nhẹ và nhanh. */
88
88
  withContent?: boolean;
89
+ /**
90
+ * `relevance` = theo độ khớp với `search` (trang tìm kiếm dùng mặc định); `rand` = ngẫu nhiên (kết quả vẫn được
91
+ * cache, đổi khi cache hết hạn hoặc nội dung đổi).
92
+ */
89
93
  page?: number;
90
94
  perPage?: number;
91
- orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count";
95
+ orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count" | "id" | "slug" | "rand" | "relevance";
92
96
  order?: "asc" | "desc";
97
+ /** Ngày đăng từ (gồm cả mốc), ISO-8601 hoặc Date — vd sự kiện / tin tức từ đầu năm học. */
98
+ after?: string | Date;
99
+ /** Ngày đăng trước (không gồm mốc). */
100
+ before?: string | Date;
101
+ /** Năm đăng (UTC); kèm `month` 1–12 để lấy một tháng — trang lưu trữ. */
102
+ year?: number;
103
+ month?: number;
93
104
  /**
94
105
  * Sắp xếp theo một khoá meta đã khai báo `filterable` (vd `"gia"`, `"job_deadline"`) — thay cho `orderBy`.
95
106
  * Khoá chưa khai báo trả 422 (không có chỉ mục thì sắp xếp phải quét toàn bảng).
@@ -102,6 +113,95 @@ export declare const getPosts: ({ withContent, ...q }?: PostQuery) => Promise<{
102
113
  page: number;
103
114
  totalPages: number;
104
115
  }>;
116
+ /** Một trang của danh sách công khai (term, tác giả, media). */
117
+ export interface ListPage<T> {
118
+ items: T[];
119
+ total: number;
120
+ page: number;
121
+ totalPages: number;
122
+ }
123
+ /** Một giá trị facet: slug term (facet phân loại) hoặc giá trị meta, kèm số bài. */
124
+ export interface FacetValue {
125
+ value: string;
126
+ count: number;
127
+ termId?: number | null;
128
+ name?: string | null;
129
+ parentId?: number | null;
130
+ }
131
+ export interface FacetResult {
132
+ total: number;
133
+ facets: Record<string, FacetValue[]>;
134
+ }
135
+ /**
136
+ * Đếm số bài theo từng giá trị (bộ lọc có số đếm: "Khoa CNTT (12)") trên đúng tập bài mà {@link getPosts} trả về với
137
+ * cùng điều kiện. `facets`: tên phân loại công khai ("category") và/hoặc `"meta:khoá"` (khoá khai báo `filterable`).
138
+ * Tối đa 5 facet × 100 giá trị, đếm trong PostgreSQL.
139
+ * @example const { facets } = await getFacets(["khoa", "meta:hoc-ky"], { type: "khoa-hoc", taxQuery })
140
+ */
141
+ export declare function getFacets(facets: string[], query?: Omit<PostQuery, "page" | "perPage" | "withContent" | "orderBy" | "order" | "orderByMeta">): Promise<FacetResult>;
142
+ export interface TermQuery {
143
+ taxonomy: string;
144
+ locale?: string;
145
+ /** Tìm theo tên, không phân biệt dấu. */
146
+ search?: string;
147
+ /** Chỉ term con trực tiếp của term này; 0 = chỉ term gốc. */
148
+ parent?: number;
149
+ include?: number[];
150
+ exclude?: number[];
151
+ hideEmpty?: boolean;
152
+ /** Chỉ term có meta khoá này (và đúng giá trị nếu có metaValue). */
153
+ metaKey?: string;
154
+ metaValue?: string;
155
+ orderBy?: "name" | "slug" | "count" | "id";
156
+ order?: "asc" | "desc";
157
+ /** perPage 1–500 (mặc định 100). */
158
+ page?: number;
159
+ perPage?: number;
160
+ /** Tham số thêm cho plugin (filter terms.query đọc trong Query). */
161
+ params?: Record<string, string>;
162
+ }
163
+ /** Danh sách term có phân trang, lọc theo cha / từ khoá / meta, sắp xếp — bản đầy đủ của {@link getTerms}. */
164
+ export declare const queryTerms: ({ params, ...q }: TermQuery) => Promise<ListPage<Term>>;
165
+ export interface AuthorQuery {
166
+ locale?: string;
167
+ /** Tìm theo tên hiển thị, không phân biệt dấu. */
168
+ search?: string;
169
+ include?: number[];
170
+ exclude?: number[];
171
+ /** Lọc theo meta công khai (khoá khai báo `UserMeta` Public), vd metaKey "khoa". Khoá không công khai → 422. */
172
+ metaKey?: string;
173
+ metaValue?: string;
174
+ orderBy?: "name" | "post_count" | "id";
175
+ order?: "asc" | "desc";
176
+ /** perPage 1–100 (mặc định 20). */
177
+ page?: number;
178
+ perPage?: number;
179
+ /** Tham số thêm cho plugin (filter authors.query), vd plugin danh bạ thêm vai trò "giang-vien". */
180
+ params?: Record<string, string>;
181
+ }
182
+ /**
183
+ * Danh sách tác giả công khai (trang "Đội ngũ", danh bạ): mặc định thành viên có bài đã đăng; plugin mở rộng qua
184
+ * filter authors.query. Chỉ có tên, slug, ảnh, tiểu sử, meta công khai — không bao giờ có email.
185
+ */
186
+ export declare const getAuthors: ({ params, ...q }?: AuthorQuery) => Promise<ListPage<Author>>;
187
+ export interface MediaQuery {
188
+ /** Tìm theo tiêu đề, alt, chú thích, tên file — không phân biệt dấu. */
189
+ search?: string;
190
+ /** Tiền tố kiểu file: "image/", "video/", "application/pdf". */
191
+ mime?: string;
192
+ folder?: number;
193
+ include?: number[];
194
+ exclude?: number[];
195
+ orderBy?: "date" | "title" | "id" | "size";
196
+ order?: "asc" | "desc";
197
+ /** perPage 1–100 (mặc định 20). */
198
+ page?: number;
199
+ perPage?: number;
200
+ /** Tham số thêm cho plugin (filter media.query). */
201
+ params?: Record<string, string>;
202
+ }
203
+ /** Danh sách media (thư viện ảnh, tài liệu tải về) — plugin giới hạn / mở rộng qua filter media.query. */
204
+ export declare const getMedia: ({ params, ...q }?: MediaQuery) => Promise<ListPage<Media>>;
105
205
  export declare const getPost: (type: string, slug: string, locale?: string) => Promise<Post | null>;
106
206
  export declare const getTerms: (taxonomy: string, hideEmpty?: boolean, locale?: string) => Promise<Term[]>;
107
207
  /** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
@@ -117,8 +217,13 @@ export declare const getPattern: (id: number) => Promise<PostContentDoc | null>;
117
217
  export declare const getMenu: (location: string, locale?: string) => Promise<Menu | null>;
118
218
  export declare const getMenuById: (id: number) => Promise<Menu | null>;
119
219
  export declare const getWidgets: (area: string, locale?: string) => Promise<WidgetInstance[]>;
120
- /** Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận). */
121
- export declare const getComments: (postId: number, page?: number) => Promise<CommentPage>;
220
+ /**
221
+ * Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận).
222
+ * Bài có mật khẩu (core >= 1.15): CMS chỉ trả bình luận khi có token mở khoá — unlock = giá trị cookie cms-unlock (không cache).
223
+ */
224
+ export declare const getComments: (postId: number, page?: number, unlock?: string) => Promise<CommentPage>;
225
+ /** Cookie mở khoá bài có mật khẩu của request hiện tại (server component); ngoài request → undefined. */
226
+ export declare function requestUnlock(): Promise<string | undefined>;
122
227
  export declare const getArchives: (locale?: string) => Promise<{
123
228
  year: number;
124
229
  month: number;
package/dist/api.js CHANGED
@@ -121,7 +121,7 @@ export const getPosts = async ({ withContent, ...q } = {}) => {
121
121
  // hợp đồng (api/theme-sdk.txt) nên thêm tên vào tham số là đổi một dòng đã ký (CI coi là phá hợp đồng).
122
122
  const { taxQuery, metaQuery, ...rest } = q;
123
123
  return cmsFetch("/posts", { tags: ["posts"], query: clean({
124
- ...rest,
124
+ ...postQueryParams(rest),
125
125
  // Bộ lọc phức tạp đi dạng JSON — core phân tích và áp thẳng vào câu SQL (không lọc bằng bộ nhớ).
126
126
  taxQuery: taxQuery?.length ? JSON.stringify(taxQuery) : undefined,
127
127
  metaQuery: metaQuery?.clauses?.length ? JSON.stringify(metaQuery) : undefined,
@@ -130,6 +130,36 @@ export const getPosts = async ({ withContent, ...q } = {}) => {
130
130
  }) });
131
131
  };
132
132
  const loc = async (locale) => locale ?? (await getRequestLocale());
133
+ /** Tham số đơn giản của PostQuery → query string (Date → ISO). */
134
+ function postQueryParams(q) {
135
+ const iso = (d) => (d instanceof Date ? d.toISOString() : d);
136
+ return { ...q, after: iso(q.after), before: iso(q.before) };
137
+ }
138
+ /**
139
+ * Đếm số bài theo từng giá trị (bộ lọc có số đếm: "Khoa CNTT (12)") trên đúng tập bài mà {@link getPosts} trả về với
140
+ * cùng điều kiện. `facets`: tên phân loại công khai ("category") và/hoặc `"meta:khoá"` (khoá khai báo `filterable`).
141
+ * Tối đa 5 facet × 100 giá trị, đếm trong PostgreSQL.
142
+ * @example const { facets } = await getFacets(["khoa", "meta:hoc-ky"], { type: "khoa-hoc", taxQuery })
143
+ */
144
+ export async function getFacets(facets, query = {}) {
145
+ const { taxQuery, metaQuery, ...rest } = query;
146
+ return cmsFetch("/posts/facets", { tags: ["posts"], query: clean({
147
+ ...postQueryParams(rest),
148
+ facets: facets.join(","),
149
+ taxQuery: taxQuery?.length ? JSON.stringify(taxQuery) : undefined,
150
+ metaQuery: metaQuery?.clauses?.length ? JSON.stringify(metaQuery) : undefined,
151
+ locale: rest.locale ?? (await getRequestLocale()),
152
+ }) });
153
+ }
154
+ /** Danh sách term có phân trang, lọc theo cha / từ khoá / meta, sắp xếp — bản đầy đủ của {@link getTerms}. */
155
+ export const queryTerms = async ({ params, ...q }) => cmsFetch("/terms", { tags: ["terms"], query: clean({ ...params, ...q, locale: await loc(q.locale) }) });
156
+ /**
157
+ * Danh sách tác giả công khai (trang "Đội ngũ", danh bạ): mặc định thành viên có bài đã đăng; plugin mở rộng qua
158
+ * filter authors.query. Chỉ có tên, slug, ảnh, tiểu sử, meta công khai — không bao giờ có email.
159
+ */
160
+ export const getAuthors = async ({ params, ...q } = {}) => cmsFetch("/authors", { tags: ["authors"], query: clean({ ...params, ...q, locale: await loc(q.locale) }) });
161
+ /** Danh sách media (thư viện ảnh, tài liệu tải về) — plugin giới hạn / mở rộng qua filter media.query. */
162
+ export const getMedia = async ({ params, ...q } = {}) => cmsFetch("/media", { tags: ["media"], query: clean({ ...params, ...q }) });
133
163
  export const getPost = async (type, slug, locale) => cmsFetch(`/posts/${type}/${encodeURIComponent(slug)}`, { tags: ["posts"], query: clean({ locale: await loc(locale) }) }).catch(() => null);
134
164
  export const getTerms = async (taxonomy, hideEmpty = true, locale) => cmsFetch(`/terms/${taxonomy}`, { tags: ["terms"], query: clean({ hideEmpty, locale: await loc(locale) }) });
135
165
  /** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
@@ -155,7 +185,22 @@ export const getPattern = (id) => cmsFetch(`/patterns/${id}`, { tags: ["patterns
155
185
  export const getMenu = async (location, locale) => cmsFetch(`/menus/${location}`, { tags: ["menus"], query: clean({ locale: await loc(locale) }) });
156
186
  export const getMenuById = (id) => cmsFetch(`/menus/id/${id}`, { tags: ["menus"] });
157
187
  export const getWidgets = async (area, locale) => cmsFetch(`/widgets/${area}`, { tags: ["widgets"], query: clean({ locale: await loc(locale) }) });
158
- /** Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận). */
159
- export const getComments = (postId, page = 1) => cmsFetch(`/comments/${postId}`, { tags: [`comments:${postId}`], query: page > 1 ? { page } : undefined });
188
+ /**
189
+ * Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận).
190
+ * Bài có mật khẩu (core >= 1.15): CMS chỉ trả bình luận khi có token mở khoá — unlock = giá trị cookie cms-unlock (không cache).
191
+ */
192
+ export const getComments = (postId, page = 1, unlock) => cmsFetch(`/comments/${postId}`, {
193
+ tags: [`comments:${postId}`], noStore: !!unlock, query: page > 1 ? { page } : undefined, headers: unlock ? { [UNLOCK_HEADER]: unlock } : undefined,
194
+ });
195
+ /** Cookie mở khoá bài có mật khẩu của request hiện tại (server component); ngoài request → undefined. */
196
+ export async function requestUnlock() {
197
+ try {
198
+ const { cookies } = await import("next/headers");
199
+ return (await cookies()).get(UNLOCK_COOKIE)?.value;
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
160
205
  export const getArchives = async (locale) => cmsFetch("/archives", { tags: ["posts"], query: clean({ locale: await loc(locale) }) });
161
206
  export const getLanguages = () => cmsFetch("/languages", { tags: ["site"] });
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { getArchives, getComments, getMenu, getMenuById, getPosts, getSite, getTerms, getWidgets } from "./api.js";
2
+ import { getArchives, getComments, getMenu, getMenuById, getPosts, getSite, getTerms, getWidgets, requestUnlock } from "./api.js";
3
3
  import { findWidget, getSlotItems } from "./runtime.js";
4
4
  import { cx, formatDate, pageLink, pageLinkWithQuery } from "./utils.js";
5
5
  import { Img } from "./image.js";
@@ -134,7 +134,8 @@ export async function Comments({ post, title }) {
134
134
  return null;
135
135
  if (post.passwordProtected && !post.content)
136
136
  return null; // bài có mật khẩu: ẩn bình luận tới khi mở khoá
137
- const [site, page, t] = await Promise.all([getSite(), getComments(post.id), getT()]);
137
+ const unlock = post.passwordProtected ? await requestUnlock() : undefined;
138
+ const [site, page, t] = await Promise.all([getSite(), getComments(post.id, 1, unlock), getT()]);
138
139
  const comments = page?.items ?? [];
139
140
  const dateSite = { dateFormat: site.dateFormat, timezone: site.timezone, locale: site.locale };
140
141
  title ??= t("comments.title");
package/dist/types.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface Author {
11
11
  slug: string;
12
12
  avatarUrl?: string | null;
13
13
  bio?: string | null;
14
+ /** Meta công khai của thành viên (plugin khai báo `UserMeta` Public), vd `{ khoa: "cntt" }`. */
15
+ meta?: JsonObject;
16
+ /** Số bài đã đăng — chỉ có trong danh sách {@link getAuthors}. */
17
+ postCount?: number | null;
18
+ /** Dữ liệu plugin thêm qua filter api.author.response. */
19
+ extra?: JsonObject;
14
20
  }
15
21
  export interface MediaSize {
16
22
  url: string;
@@ -34,6 +40,8 @@ export interface Media {
34
40
  /** Điểm lấy nét 0–1 (object-position). */
35
41
  focalX?: number | null;
36
42
  focalY?: number | null;
43
+ title?: string;
44
+ createdAt?: string;
37
45
  }
38
46
  export interface Term {
39
47
  id: number;
@@ -285,6 +293,20 @@ export interface ThemeManifest {
285
293
  }>;
286
294
  /** Nguồn ngoài website theme cần nạp (CSP): { "style-src": ["https://fonts.googleapis.com"], "font-src": ["https://fonts.gstatic.com"] }. */
287
295
  csp?: Partial<Record<"script-src" | "style-src" | "img-src" | "font-src" | "connect-src" | "frame-src" | "media-src" | "worker-src" | "form-action", string[]>>;
296
+ /**
297
+ * Kiểu nội dung của theme (CMS ≥ 1.15, như plugin.json — giống functions.php của WordPress):
298
+ * { "su-kien": { "label": "Sự kiện", "rewriteSlug": "su-kien", "taxonomies": ["khoa"] } }.
299
+ * Đăng ký khi theme (hoặc theme con) đang dùng; không đè kiểu của core/plugin.
300
+ */
301
+ postTypes?: Record<string, Record<string, JsonValue>>;
302
+ /** Phân loại của theme: { "khoa": { "label": "Khoa", "hierarchical": true, "postTypes": ["su-kien"] } }. */
303
+ taxonomies?: Record<string, Record<string, JsonValue>>;
304
+ /** Meta bài của theme: { "ngay-bat-dau": { "filterable": true, "indexType": "date" } } — lọc/sắp xếp/facet được. */
305
+ postMeta?: Record<string, Record<string, JsonValue>>;
306
+ /** Meta thành viên: { "hoc-vi": { "public": true } } — hiện trong hồ sơ tác giả (Author.meta). */
307
+ userMeta?: Record<string, {
308
+ public?: boolean;
309
+ }>;
288
310
  [key: string]: unknown;
289
311
  }
290
312
  type Loader<T> = () => Promise<{
@@ -435,6 +457,14 @@ export interface FilterMap {
435
457
  "sitemap.entries": (entries: SitemapEntry[], ctx: {
436
458
  site: Site;
437
459
  }) => SitemapEntry[] | Promise<SitemapEntry[]>;
460
+ /**
461
+ * Đổi thứ tự / thêm tên template trước khi theme chọn (core ≥ 1.15) — vd plugin "Sự kiện" thêm "plugin-su-kien" lên đầu
462
+ * cho kiểu nội dung của nó và khai báo template đó trong `definePlugin({ templates })`. Theme có template trùng tên vẫn thắng.
463
+ */
464
+ "template.hierarchy": (templates: string[], ctx: {
465
+ site: Site;
466
+ data: ResolveResult;
467
+ }) => string[] | Promise<string[]>;
438
468
  /** Thêm/bớt class của thẻ <body> (vd "logged-in", "has-promo-bar") */
439
469
  "body.class": (classes: string[], ctx: {
440
470
  site: Site;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomcx/ecomweb-theme-sdk",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "SDK để viết theme và phần frontend của plugin cho Ecomweb",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",