@roottale/cms-mcp 0.53.1 → 0.55.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/CHANGELOG.md +45 -0
- package/README.md +17 -9
- package/dist/index.js +660 -45
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +64 -7
- package/docs/blog.md +93 -0
- package/docs/collections.md +31 -1
- package/docs/content-models-and-exposures.md +274 -32
- package/docs/getting-started.md +20 -10
- package/examples/nextjs/app/preview/post/[id]/page.tsx +70 -0
- package/examples/nextjs/lib/blog.ts +22 -0
- package/examples/nextjs/lib/content-models.ts +12 -0
- package/package.json +2 -2
package/docs/getting-started.md
CHANGED
|
@@ -137,10 +137,11 @@ console.log(page.items.map((p) => p.slug));
|
|
|
137
137
|
|
|
138
138
|
`401` 에러(`invalid_key`)면 키 값/환경변수 로딩을 확인하세요.
|
|
139
139
|
|
|
140
|
-
## 5. MCP로
|
|
140
|
+
## 5. MCP로 전체 CMS 자동화
|
|
141
141
|
|
|
142
|
-
MCP 설정의 `ROOTTALE_API_KEY`에 `
|
|
143
|
-
있습니다.
|
|
142
|
+
MCP 설정의 `ROOTTALE_API_KEY`에 `full_management` 키를 넣으면 모델·필드·항목·
|
|
143
|
+
노출·미디어 도구를 모두 쓸 수 있습니다. 글과 이미지만 관리한다면 기존
|
|
144
|
+
`read_write` 키로 충분합니다.
|
|
144
145
|
|
|
145
146
|
1. `getSiteKnowledge`로 브랜드 보이스와 금지어 확인
|
|
146
147
|
2. `uploadCmsMedia`로 썸네일 또는 본문 삽화 업로드
|
|
@@ -148,6 +149,11 @@ MCP 설정의 `ROOTTALE_API_KEY`에 `read_write` 키를 넣으면 다음 tool을
|
|
|
148
149
|
4. 필요하면 `setCmsPostTerms`로 카테고리·태그 연결
|
|
149
150
|
5. 검토 후 `publishCmsPost`로 발행
|
|
150
151
|
|
|
152
|
+
정보 모델을 새로 자동화할 때는 `createCmsContentModel` →
|
|
153
|
+
`createCmsFieldGroup` → `createCmsPost(modelKey, fieldValues)` 순서로 진행합니다.
|
|
154
|
+
배너·팝업은 `listCmsExposureSlots`로 FRONT 계약을 먼저 읽고
|
|
155
|
+
`createCmsExposure` → `publishCmsExposure` 순서로 진행합니다.
|
|
156
|
+
|
|
151
157
|
썸네일은 업로드 응답의 `id`를 `featuredMediaId`에 넣습니다. 본문 삽화는
|
|
152
158
|
응답의 `url`을 Tiptap image 노드에 넣습니다.
|
|
153
159
|
|
|
@@ -175,31 +181,35 @@ export ROOTTALE_API_KEY=rtlk_cust_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
175
181
|
npx -y @roottale/cms-mcp cli media upload ./thumbnail.webp \
|
|
176
182
|
--alt "글 대표 이미지"
|
|
177
183
|
|
|
178
|
-
npx -y @roottale/cms-mcp cli
|
|
184
|
+
npx -y @roottale/cms-mcp cli entries create \
|
|
179
185
|
--title "새 글" \
|
|
180
186
|
--slug "new-post" \
|
|
181
187
|
--body-file ./post.json \
|
|
182
188
|
--featured-media-id "<업로드 응답의 id>"
|
|
183
189
|
|
|
184
|
-
npx -y @roottale/cms-mcp cli
|
|
190
|
+
npx -y @roottale/cms-mcp cli entries publish "<글 id>"
|
|
185
191
|
```
|
|
186
192
|
|
|
187
|
-
상위 `cli --help`는 `
|
|
193
|
+
상위 `cli --help`는 `models`, `fields`, `entries`, `exposure-slots`,
|
|
194
|
+
`exposures`, `media` 그룹을 보여줍니다. 전체 하위 명령은
|
|
188
195
|
다음 도움말에서 확인하세요.
|
|
189
196
|
|
|
190
197
|
```bash
|
|
191
|
-
npx -y @roottale/cms-mcp cli
|
|
198
|
+
npx -y @roottale/cms-mcp cli entries --help
|
|
199
|
+
npx -y @roottale/cms-mcp cli models --help
|
|
200
|
+
npx -y @roottale/cms-mcp cli exposures --help
|
|
192
201
|
npx -y @roottale/cms-mcp cli media --help
|
|
193
202
|
|
|
194
203
|
# 특정 명령의 모든 옵션
|
|
195
|
-
npx -y @roottale/cms-mcp cli
|
|
204
|
+
npx -y @roottale/cms-mcp cli entries create --help
|
|
196
205
|
npx -y @roottale/cms-mcp cli media upload --help
|
|
197
206
|
```
|
|
198
207
|
|
|
199
|
-
|
|
208
|
+
항목 명령은 `list`, `get`, `create`, `update`, `publish`, `unpublish`,
|
|
209
|
+
`set-terms`, `delete`,
|
|
200
210
|
미디어 명령은 `list`, `upload`, `update`, `delete`를 제공합니다.
|
|
201
211
|
|
|
202
|
-
내부 운영 저장소에서는 같은 기능을 `rt cms
|
|
212
|
+
내부 운영 저장소에서는 같은 기능을 `rt cms entries ...`,
|
|
203
213
|
`rt cms media ...` 명령으로도 실행할 수 있습니다.
|
|
204
214
|
|
|
205
215
|
## 다음 단계
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 글 미리보기 — ROOT-ADMIN 편집기의 '미리보기'·'공유 링크'가 여는 주소.
|
|
2
|
+
// 관리자 발급 토큰(1시간, 글 1건 전용)으로 편집 중인 내용을 받아 발행 글과
|
|
3
|
+
// **같은 템플릿**(RootTaleBlogPost)으로 그린다 → 미리보기 = 발행 결과.
|
|
4
|
+
// 라우트를 배포한 뒤 관리자 설정 › 외부 연결 › "글 미리보기 여는 곳"에서
|
|
5
|
+
// "홈페이지에서 열기"를 켜면 편집기가 이 주소를 연다.
|
|
6
|
+
import type { Metadata } from "next";
|
|
7
|
+
import { notFound } from "next/navigation";
|
|
8
|
+
import {
|
|
9
|
+
fetchPostPreview,
|
|
10
|
+
isPreviewExpiredError,
|
|
11
|
+
} from "@roottale/cms-client/server";
|
|
12
|
+
import {
|
|
13
|
+
RootTaleBlogPost,
|
|
14
|
+
RootTalePreviewNotice,
|
|
15
|
+
} from "@roottale/cms-renderer-next/server";
|
|
16
|
+
import { buildPreviewMetadata } from "@roottale/cms-renderer-next/routes";
|
|
17
|
+
|
|
18
|
+
// 초안이 ISR·CDN 에 남으면 안 된다 — 항상 동적 렌더.
|
|
19
|
+
export const dynamic = "force-dynamic";
|
|
20
|
+
export const revalidate = 0;
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
params: Promise<{ id: string }>;
|
|
24
|
+
searchParams: Promise<{ token?: string }>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function loadPreview(token: string) {
|
|
28
|
+
return fetchPostPreview({
|
|
29
|
+
apiKey: process.env.ROOTTALE_API_KEY!,
|
|
30
|
+
baseUrl: process.env.ROOTTALE_API_BASE,
|
|
31
|
+
token,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
|
|
36
|
+
const { token } = await searchParams;
|
|
37
|
+
const post = token ? await loadPreview(token).catch(() => null) : null;
|
|
38
|
+
// 항상 noindex/nofollow — 초안이 색인되거나 발행 주소와 중복되면 안 된다.
|
|
39
|
+
return buildPreviewMetadata({ title: post?.title });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function PostPreviewPage({ params, searchParams }: Props) {
|
|
43
|
+
const { id } = await params;
|
|
44
|
+
const { token } = await searchParams;
|
|
45
|
+
if (!token) notFound(); // 주소만으로는 아무것도 보이지 않는다.
|
|
46
|
+
|
|
47
|
+
let expiresAt: string | undefined;
|
|
48
|
+
try {
|
|
49
|
+
const post = await loadPreview(token);
|
|
50
|
+
if (!post || post.id !== id) notFound(); // 토큰은 글 1건 전용
|
|
51
|
+
expiresAt = post.preview.expiresAt;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!isPreviewExpiredError(error)) throw error; // 만료는 컴포넌트가 안내
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<main className="container">
|
|
58
|
+
<RootTalePreviewNotice expiresAt={expiresAt} />
|
|
59
|
+
{/* 발행 글 상세(app/blog/[slug]/page.tsx)와 같은 props 를 쓰되
|
|
60
|
+
slugOrId 대신 previewToken 만 넘긴다. */}
|
|
61
|
+
<RootTaleBlogPost
|
|
62
|
+
apiKey={process.env.ROOTTALE_API_KEY!}
|
|
63
|
+
baseUrl={process.env.ROOTTALE_API_BASE}
|
|
64
|
+
previewToken={token}
|
|
65
|
+
relatedPostsCount={3}
|
|
66
|
+
breadcrumb={{ siteUrl: process.env.NEXT_PUBLIC_SITE_URL }}
|
|
67
|
+
/>
|
|
68
|
+
</main>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
BLOG_SETTINGS_CACHE_TAG,
|
|
5
5
|
fetchBlogSettings,
|
|
6
|
+
fetchCategoryCounts,
|
|
6
7
|
fetchPost,
|
|
7
8
|
type CmsPostContent,
|
|
8
9
|
} from "@roottale/cms-client/server";
|
|
@@ -31,6 +32,7 @@ export interface BlogPostMeta {
|
|
|
31
32
|
categorySlug: string; // "" = 카테고리 미지정 — /blog/categories/{slug} 허브 필터링용
|
|
32
33
|
tags: { name: string; slug: string }[];
|
|
33
34
|
image: string | null;
|
|
35
|
+
authorProfileId?: string | null;
|
|
34
36
|
authorName?: string;
|
|
35
37
|
authorSlug?: string | null; // /blog/author/{slug} — 미발급 작가는 null
|
|
36
38
|
seo?: {
|
|
@@ -60,6 +62,7 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
|
|
|
60
62
|
?.filter((t) => t.taxonomy === "tag")
|
|
61
63
|
.map((t) => ({ name: t.name, slug: t.slug })) ?? [],
|
|
62
64
|
image: post.featuredImageUrl ?? null,
|
|
65
|
+
authorProfileId: post.authorProfileId,
|
|
63
66
|
authorName: post.authorName ?? undefined,
|
|
64
67
|
authorSlug: post.authorSlug,
|
|
65
68
|
seo: meta.seo as BlogPostMeta["seo"],
|
|
@@ -96,6 +99,25 @@ export async function getPostsByCategory(
|
|
|
96
99
|
return posts.filter((p) => p.categorySlug === categorySlug);
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
// 카테고리 허브의 소개문·검색 문구·공유 이미지는 카테고리 집계 응답에 함께 옵니다.
|
|
103
|
+
// 구버전 API와 이미지 미설정을 고려해 nullable 값으로 다룹니다.
|
|
104
|
+
export async function getCategoryArchiveMeta(categorySlug: string) {
|
|
105
|
+
const { categories } = await fetchCategoryCounts({
|
|
106
|
+
apiKey: getApiKey(),
|
|
107
|
+
baseUrl,
|
|
108
|
+
excludeCollections: true,
|
|
109
|
+
});
|
|
110
|
+
const category = categories.find((item) => item.slug === categorySlug);
|
|
111
|
+
if (!category) return null;
|
|
112
|
+
return {
|
|
113
|
+
title: category.seoTitle ?? `${category.name} 글 모음`,
|
|
114
|
+
description: category.seoDescription ?? category.description,
|
|
115
|
+
image: category.imageUrl
|
|
116
|
+
? { url: category.imageUrl, alt: category.imageAlt ?? category.name }
|
|
117
|
+
: null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
// 색인 위생 — 어드민 "내 사이트 > 카테고리 > 검색에 이 분류 페이지 노출" 을 켠
|
|
100
122
|
// 분류인지. 켜지 않은 분류의 모음 페이지는 noindex + 사이트맵 제외가 됩니다
|
|
101
123
|
// (seo.md "색인 위생" 절).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fetchContentModels, fetchPosts } from "@roottale/cms-client/server";
|
|
2
|
+
|
|
3
|
+
const apiKey = process.env.ROOTTALE_API_KEY!;
|
|
4
|
+
|
|
5
|
+
export async function getTeamMembers() {
|
|
6
|
+
const models = await fetchContentModels({ apiKey });
|
|
7
|
+
const model = models.models.find((candidate) => candidate.key === "team-member");
|
|
8
|
+
if (!model || model.preset !== "entity") return [];
|
|
9
|
+
|
|
10
|
+
const response = await fetchPosts({ apiKey, modelKey: model.key, limit: 100 });
|
|
11
|
+
return response.items;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "RootTale CMS MCP server and CLI for
|
|
5
|
+
"description": "RootTale CMS MCP server and CLI for models, entries, exposures, media, integration docs, and public API access.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"roottale-cms-mcp": "dist/index.js"
|
|
8
8
|
},
|