@roottale/cms-client 0.1.0 → 0.1.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.
- package/CHANGELOG.md +23 -0
- package/dist/server.d.ts +88 -0
- package/dist/server.js +99 -0
- package/dist/server.js.map +1 -0
- package/package.json +10 -5
- package/src/server.ts +0 -214
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @roottale/cms-client
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dist build 도입 — `.ts` 소스 직접 publish 폐기.
|
|
8
|
+
|
|
9
|
+
기존 (0.1.0/0.2.0): `exports` 가 `./src/server.ts` 가리킴 → 외부 Next.js
|
|
10
|
+
Turbopack/Webpack 이 npm 모듈에서 TS 자동 컴파일 안 해 build 실패 (customer
|
|
11
|
+
site 가 `transpilePackages` 명시해야 했음).
|
|
12
|
+
|
|
13
|
+
수정: `tsup` 으로 `dist/*.js` + `dist/*.d.ts` (ESM) 출력. `exports` 가 dist
|
|
14
|
+
가리킴. customer site 측 `transpilePackages` 불필요.
|
|
15
|
+
- `cms-client@0.1.1` — ESM `dist/server.js` + types
|
|
16
|
+
- `cms-core@0.2.1` — ESM `dist/index.js` + types
|
|
17
|
+
- `cms-renderer-next@0.2.1` — ESM `dist/{server,index}.js` + types + `dist/cms-public.css`
|
|
18
|
+
- `server.tsx` 의 `import "./styles/cms-public.css"` 제거 — customer 가
|
|
19
|
+
`@roottale/cms-renderer-next/styles` 로 명시 import (README 정합).
|
|
20
|
+
- `cms-renderer-astro@0.2.1` — ESM `dist/index.js` + types
|
|
21
|
+
|
|
22
|
+
후속 (customer site PR):
|
|
23
|
+
- roottale-web / kjmtax / theoneulsan 의 `next.config` 의 `transpilePackages`
|
|
24
|
+
에서 `@roottale/cms-*` 제거. `pnpm update @roottale/cms-client @roottale/cms-renderer-next` 로 patch 적용.
|
|
25
|
+
|
|
3
26
|
## 0.1.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.
|
|
3
|
+
*
|
|
4
|
+
* 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.
|
|
5
|
+
* Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).
|
|
6
|
+
*
|
|
7
|
+
* 사용 예 (Next.js server component):
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { fetchPosts } from "@roottale/cms-client/server";
|
|
10
|
+
*
|
|
11
|
+
* const posts = await fetchPosts({
|
|
12
|
+
* apiKey: process.env.ROOTTALE_API_KEY!,
|
|
13
|
+
* limit: 10,
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).
|
|
18
|
+
* Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.
|
|
19
|
+
*/
|
|
20
|
+
type CmsPostType = "post" | "page";
|
|
21
|
+
interface CmsPostContent {
|
|
22
|
+
/** UUIDv7. */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Owning tenant id. */
|
|
25
|
+
tenantId: string;
|
|
26
|
+
/** Owning site id (ADR-0034 P4 dual-key). */
|
|
27
|
+
siteId: string;
|
|
28
|
+
/** WP post type. */
|
|
29
|
+
type: CmsPostType;
|
|
30
|
+
/** Lowercase kebab-case slug (1-120 chars). */
|
|
31
|
+
slug: string;
|
|
32
|
+
title: string;
|
|
33
|
+
excerpt: string | null;
|
|
34
|
+
/** Media id (resolved by renderer / consumer to URL). */
|
|
35
|
+
featuredMediaId: string | null;
|
|
36
|
+
authorId: string | null;
|
|
37
|
+
/** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */
|
|
38
|
+
bodyJson: Record<string, unknown>;
|
|
39
|
+
/** Public API only exposes `published`. */
|
|
40
|
+
status: "published";
|
|
41
|
+
/** Per-post meta (SEO overrides, canonical, og, ...). */
|
|
42
|
+
metaJson: Record<string, unknown>;
|
|
43
|
+
/** ISO 8601. */
|
|
44
|
+
publishedAt: string;
|
|
45
|
+
/** ISO 8601. */
|
|
46
|
+
createdAt: string;
|
|
47
|
+
/** ISO 8601. */
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
}
|
|
50
|
+
interface CmsPostContentPage {
|
|
51
|
+
items: CmsPostContent[];
|
|
52
|
+
nextCursor: string | null;
|
|
53
|
+
hasMore: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface FetchPostsOptions {
|
|
56
|
+
/** `rtlk_cust_*` API key. Required. */
|
|
57
|
+
apiKey: string;
|
|
58
|
+
/** Override the API base URL. Defaults to `https://api.roottale.com`. */
|
|
59
|
+
baseUrl?: string;
|
|
60
|
+
/** Page size, 1-100, default 20. */
|
|
61
|
+
limit?: number;
|
|
62
|
+
/** Opaque pagination cursor returned from previous call. */
|
|
63
|
+
cursor?: string;
|
|
64
|
+
/** Filter by post type (`post` or `page`). Default = both. */
|
|
65
|
+
type?: CmsPostType;
|
|
66
|
+
/** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */
|
|
67
|
+
siteId?: string;
|
|
68
|
+
/** Optional AbortSignal. */
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}
|
|
71
|
+
interface FetchPostOptions {
|
|
72
|
+
apiKey: string;
|
|
73
|
+
baseUrl?: string;
|
|
74
|
+
/** Slug (kebab-case) or UUIDv7 id. */
|
|
75
|
+
slugOrId: string;
|
|
76
|
+
/** Override the auto-resolved site id. */
|
|
77
|
+
siteId?: string;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
declare class CmsApiError extends Error {
|
|
81
|
+
readonly status: number;
|
|
82
|
+
readonly code: string;
|
|
83
|
+
constructor(message: string, status: number, code: string);
|
|
84
|
+
}
|
|
85
|
+
declare function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage>;
|
|
86
|
+
declare function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null>;
|
|
87
|
+
|
|
88
|
+
export { CmsApiError, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type FetchPostOptions, type FetchPostsOptions, fetchPost, fetchPosts };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://api.roottale.com";
|
|
3
|
+
var CmsApiError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, status, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "CmsApiError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
async function fetchPosts(options) {
|
|
14
|
+
assertServer();
|
|
15
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
18
|
+
if (options.cursor) params.set("cursor", options.cursor);
|
|
19
|
+
if (options.type) params.set("type", options.type);
|
|
20
|
+
if (options.siteId) params.set("site_id", options.siteId);
|
|
21
|
+
const qs = params.toString();
|
|
22
|
+
const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
method: "GET",
|
|
25
|
+
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
26
|
+
signal: options.signal
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw await toApiError(response);
|
|
30
|
+
}
|
|
31
|
+
const json = await response.json();
|
|
32
|
+
return {
|
|
33
|
+
items: json.items.map(fromWire),
|
|
34
|
+
hasMore: json.has_more,
|
|
35
|
+
nextCursor: json.next_cursor
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function fetchPost(options) {
|
|
39
|
+
assertServer();
|
|
40
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
41
|
+
const params = new URLSearchParams();
|
|
42
|
+
if (options.siteId) params.set("site_id", options.siteId);
|
|
43
|
+
const qs = params.toString();
|
|
44
|
+
const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method: "GET",
|
|
47
|
+
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
48
|
+
signal: options.signal
|
|
49
|
+
});
|
|
50
|
+
if (response.status === 404) return null;
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw await toApiError(response);
|
|
53
|
+
}
|
|
54
|
+
const json = await response.json();
|
|
55
|
+
return fromWire(json);
|
|
56
|
+
}
|
|
57
|
+
function fromWire(wire) {
|
|
58
|
+
return {
|
|
59
|
+
id: wire.id,
|
|
60
|
+
tenantId: wire.tenant_id,
|
|
61
|
+
siteId: wire.site_id,
|
|
62
|
+
type: wire.type,
|
|
63
|
+
slug: wire.slug,
|
|
64
|
+
title: wire.title,
|
|
65
|
+
excerpt: wire.excerpt,
|
|
66
|
+
featuredMediaId: wire.featured_media_id,
|
|
67
|
+
authorId: wire.author_id,
|
|
68
|
+
bodyJson: wire.body_json,
|
|
69
|
+
status: wire.status,
|
|
70
|
+
metaJson: wire.meta_json,
|
|
71
|
+
publishedAt: wire.published_at,
|
|
72
|
+
createdAt: wire.created_at,
|
|
73
|
+
updatedAt: wire.updated_at
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function toApiError(response) {
|
|
77
|
+
let code = "request_failed";
|
|
78
|
+
let message = `RootTale CMS API error: ${response.status}`;
|
|
79
|
+
try {
|
|
80
|
+
const body = await response.json();
|
|
81
|
+
if (body.code) code = body.code;
|
|
82
|
+
if (body.message) message = body.message;
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
return new CmsApiError(message, response.status, code);
|
|
86
|
+
}
|
|
87
|
+
function assertServer() {
|
|
88
|
+
if (typeof window !== "undefined") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"@roottale/cms-client/server must not run in the browser. Move this call to a Server Component, getServerSideProps, or a Route Handler. Shipping API keys to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
CmsApiError,
|
|
96
|
+
fetchPost,
|
|
97
|
+
fetchPosts
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n authorId: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n signal?: AbortSignal;\n}\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n signal: options.signal,\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return fromWire(json);\n}\n\ntype CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n slug: string;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n author_id: string | null;\n body_json: Record<string, unknown>;\n status: \"published\";\n meta_json: Record<string, unknown>;\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n authorId: wire.author_id,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n\nasync function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RootTale CMS Public API server-side fetch client (Bearer rtlk_cust_* auth). SSR-only — refuses to run in the browser to prevent key leak (ADR-0023 §5.1 #15). Pairs with @roottale/cms-renderer-next / @roottale/cms-renderer-astro.",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/server.js",
|
|
7
|
+
"types": "./dist/server.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
"./server":
|
|
9
|
+
"./server": {
|
|
10
|
+
"types": "./dist/server.d.ts",
|
|
11
|
+
"import": "./dist/server.js",
|
|
12
|
+
"default": "./dist/server.js"
|
|
13
|
+
},
|
|
9
14
|
"./package.json": "./package.json"
|
|
10
15
|
},
|
|
11
16
|
"files": [
|
|
12
|
-
"
|
|
17
|
+
"dist/",
|
|
13
18
|
"README.md",
|
|
14
19
|
"CHANGELOG.md"
|
|
15
20
|
],
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"directory": "packages/cms-client"
|
|
38
43
|
},
|
|
39
44
|
"scripts": {
|
|
40
|
-
"build": "
|
|
45
|
+
"build": "tsup",
|
|
41
46
|
"type-check": "tsc --noEmit",
|
|
42
47
|
"test": "vitest run --passWithNoTests"
|
|
43
48
|
}
|
package/src/server.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.
|
|
3
|
-
*
|
|
4
|
-
* 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.
|
|
5
|
-
* Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).
|
|
6
|
-
*
|
|
7
|
-
* 사용 예 (Next.js server component):
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { fetchPosts } from "@roottale/cms-client/server";
|
|
10
|
-
*
|
|
11
|
-
* const posts = await fetchPosts({
|
|
12
|
-
* apiKey: process.env.ROOTTALE_API_KEY!,
|
|
13
|
-
* limit: 10,
|
|
14
|
-
* });
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).
|
|
18
|
-
* Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export type CmsPostType = "post" | "page";
|
|
22
|
-
|
|
23
|
-
export interface CmsPostContent {
|
|
24
|
-
/** UUIDv7. */
|
|
25
|
-
id: string;
|
|
26
|
-
/** Owning tenant id. */
|
|
27
|
-
tenantId: string;
|
|
28
|
-
/** Owning site id (ADR-0034 P4 dual-key). */
|
|
29
|
-
siteId: string;
|
|
30
|
-
/** WP post type. */
|
|
31
|
-
type: CmsPostType;
|
|
32
|
-
/** Lowercase kebab-case slug (1-120 chars). */
|
|
33
|
-
slug: string;
|
|
34
|
-
title: string;
|
|
35
|
-
excerpt: string | null;
|
|
36
|
-
/** Media id (resolved by renderer / consumer to URL). */
|
|
37
|
-
featuredMediaId: string | null;
|
|
38
|
-
authorId: string | null;
|
|
39
|
-
/** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */
|
|
40
|
-
bodyJson: Record<string, unknown>;
|
|
41
|
-
/** Public API only exposes `published`. */
|
|
42
|
-
status: "published";
|
|
43
|
-
/** Per-post meta (SEO overrides, canonical, og, ...). */
|
|
44
|
-
metaJson: Record<string, unknown>;
|
|
45
|
-
/** ISO 8601. */
|
|
46
|
-
publishedAt: string;
|
|
47
|
-
/** ISO 8601. */
|
|
48
|
-
createdAt: string;
|
|
49
|
-
/** ISO 8601. */
|
|
50
|
-
updatedAt: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface CmsPostContentPage {
|
|
54
|
-
items: CmsPostContent[];
|
|
55
|
-
nextCursor: string | null;
|
|
56
|
-
hasMore: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const DEFAULT_BASE_URL = "https://api.roottale.com";
|
|
60
|
-
|
|
61
|
-
export interface FetchPostsOptions {
|
|
62
|
-
/** `rtlk_cust_*` API key. Required. */
|
|
63
|
-
apiKey: string;
|
|
64
|
-
/** Override the API base URL. Defaults to `https://api.roottale.com`. */
|
|
65
|
-
baseUrl?: string;
|
|
66
|
-
/** Page size, 1-100, default 20. */
|
|
67
|
-
limit?: number;
|
|
68
|
-
/** Opaque pagination cursor returned from previous call. */
|
|
69
|
-
cursor?: string;
|
|
70
|
-
/** Filter by post type (`post` or `page`). Default = both. */
|
|
71
|
-
type?: CmsPostType;
|
|
72
|
-
/** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */
|
|
73
|
-
siteId?: string;
|
|
74
|
-
/** Optional AbortSignal. */
|
|
75
|
-
signal?: AbortSignal;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface FetchPostOptions {
|
|
79
|
-
apiKey: string;
|
|
80
|
-
baseUrl?: string;
|
|
81
|
-
/** Slug (kebab-case) or UUIDv7 id. */
|
|
82
|
-
slugOrId: string;
|
|
83
|
-
/** Override the auto-resolved site id. */
|
|
84
|
-
siteId?: string;
|
|
85
|
-
signal?: AbortSignal;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export class CmsApiError extends Error {
|
|
89
|
-
readonly status: number;
|
|
90
|
-
readonly code: string;
|
|
91
|
-
constructor(message: string, status: number, code: string) {
|
|
92
|
-
super(message);
|
|
93
|
-
this.name = "CmsApiError";
|
|
94
|
-
this.status = status;
|
|
95
|
-
this.code = code;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {
|
|
100
|
-
assertServer();
|
|
101
|
-
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
102
|
-
const params = new URLSearchParams();
|
|
103
|
-
if (options.limit !== undefined) params.set("limit", String(options.limit));
|
|
104
|
-
if (options.cursor) params.set("cursor", options.cursor);
|
|
105
|
-
if (options.type) params.set("type", options.type);
|
|
106
|
-
if (options.siteId) params.set("site_id", options.siteId);
|
|
107
|
-
const qs = params.toString();
|
|
108
|
-
const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
|
|
109
|
-
|
|
110
|
-
const response = await fetch(url, {
|
|
111
|
-
method: "GET",
|
|
112
|
-
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
113
|
-
signal: options.signal,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw await toApiError(response);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const json = (await response.json()) as {
|
|
121
|
-
items: CmsPostWire[];
|
|
122
|
-
has_more: boolean;
|
|
123
|
-
next_cursor: string | null;
|
|
124
|
-
};
|
|
125
|
-
return {
|
|
126
|
-
items: json.items.map(fromWire),
|
|
127
|
-
hasMore: json.has_more,
|
|
128
|
-
nextCursor: json.next_cursor,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {
|
|
133
|
-
assertServer();
|
|
134
|
-
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
135
|
-
const params = new URLSearchParams();
|
|
136
|
-
if (options.siteId) params.set("site_id", options.siteId);
|
|
137
|
-
const qs = params.toString();
|
|
138
|
-
const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
|
|
139
|
-
|
|
140
|
-
const response = await fetch(url, {
|
|
141
|
-
method: "GET",
|
|
142
|
-
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
143
|
-
signal: options.signal,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (response.status === 404) return null;
|
|
147
|
-
if (!response.ok) {
|
|
148
|
-
throw await toApiError(response);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const json = (await response.json()) as CmsPostWire;
|
|
152
|
-
return fromWire(json);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
type CmsPostWire = {
|
|
156
|
-
id: string;
|
|
157
|
-
tenant_id: string;
|
|
158
|
-
site_id: string;
|
|
159
|
-
type: CmsPostType;
|
|
160
|
-
slug: string;
|
|
161
|
-
title: string;
|
|
162
|
-
excerpt: string | null;
|
|
163
|
-
featured_media_id: string | null;
|
|
164
|
-
author_id: string | null;
|
|
165
|
-
body_json: Record<string, unknown>;
|
|
166
|
-
status: "published";
|
|
167
|
-
meta_json: Record<string, unknown>;
|
|
168
|
-
published_at: string;
|
|
169
|
-
created_at: string;
|
|
170
|
-
updated_at: string;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
function fromWire(wire: CmsPostWire): CmsPostContent {
|
|
174
|
-
return {
|
|
175
|
-
id: wire.id,
|
|
176
|
-
tenantId: wire.tenant_id,
|
|
177
|
-
siteId: wire.site_id,
|
|
178
|
-
type: wire.type,
|
|
179
|
-
slug: wire.slug,
|
|
180
|
-
title: wire.title,
|
|
181
|
-
excerpt: wire.excerpt,
|
|
182
|
-
featuredMediaId: wire.featured_media_id,
|
|
183
|
-
authorId: wire.author_id,
|
|
184
|
-
bodyJson: wire.body_json,
|
|
185
|
-
status: wire.status,
|
|
186
|
-
metaJson: wire.meta_json,
|
|
187
|
-
publishedAt: wire.published_at,
|
|
188
|
-
createdAt: wire.created_at,
|
|
189
|
-
updatedAt: wire.updated_at,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function toApiError(response: Response): Promise<CmsApiError> {
|
|
194
|
-
let code = "request_failed";
|
|
195
|
-
let message = `RootTale CMS API error: ${response.status}`;
|
|
196
|
-
try {
|
|
197
|
-
const body = (await response.json()) as { code?: string; message?: string };
|
|
198
|
-
if (body.code) code = body.code;
|
|
199
|
-
if (body.message) message = body.message;
|
|
200
|
-
} catch {
|
|
201
|
-
/* ignore body parse */
|
|
202
|
-
}
|
|
203
|
-
return new CmsApiError(message, response.status, code);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function assertServer(): void {
|
|
207
|
-
if (typeof window !== "undefined") {
|
|
208
|
-
throw new Error(
|
|
209
|
-
"@roottale/cms-client/server must not run in the browser. " +
|
|
210
|
-
"Move this call to a Server Component, getServerSideProps, or a Route Handler. " +
|
|
211
|
-
"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).",
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|