@roottale/cms-mcp 0.41.1 → 0.42.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 1cdc365: 어드민 도메인 변경 반영 — 문서·예시의 `mysite.roottale.com` 을 `admin.roottale.com` 으로 갱신 (키 발급·웹훅 등록 메뉴 경로 안내 포함). 기존 주소는 308 redirect 로 계속 동작.
8
+
3
9
  ## 0.41.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -30,7 +30,7 @@ RootTale CMS 연동 MCP 서버 — AI 코딩 에이전트(Claude Code, Cursor
30
30
 
31
31
  `ROOTTALE_API_KEY`는 선택입니다 — 문서·예시 코드 tool은 키 없이 동작하고,
32
32
  `listPublishedPosts` / `getPublishedPost` (연동 검증) tool만 키가 필요합니다.
33
- 키 발급: 어드민(`mysite.roottale.com`) **설정 > 사이트 연결 키**.
33
+ 키 발급: 어드민(`admin.roottale.com`) **설정 > 사이트 연결 키**.
34
34
 
35
35
  Claude Code:
36
36
 
package/dist/index.js CHANGED
@@ -279,7 +279,7 @@ function registerTools(server2) {
279
279
  }
280
280
 
281
281
  // src/server.ts
282
- var VERSION = true ? "0.41.1" : "dev";
282
+ var VERSION = true ? "0.42.0" : "dev";
283
283
  var SERVER_INSTRUCTIONS = `
284
284
  roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uAE30 \uC704\uD55C
285
285
  \uD1B5\uD569 \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7\uACF5\uAC1C API \uC870\uD68C tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
package/docs/blog.md CHANGED
@@ -52,7 +52,16 @@ export default function BlogPage() {
52
52
  > <RootTaleBlogList apiKey={apiKey} collection="blog" collections={COLLECTIONS} showCategoryFilter />
53
53
  > ```
54
54
  >
55
- > 자세한 내용은 `collections.md` 참고.
55
+ > **`/blog` 가 선언 섹션이 아니라 "나머지 전부"인 사이트**(증상/질환/치료 같은
56
+ > 아카이브 스트림만 선언)는 반대로 `collections`+**`excludeCollections`** 로 어느
57
+ > 스트림에도 안 속한 글만 렌더하세요:
58
+ >
59
+ > ```tsx
60
+ > <RootTaleBlogList apiKey={apiKey} collections={ARCHIVE_COLLECTIONS} excludeCollections showCategoryFilter />
61
+ > ```
62
+ >
63
+ > 자세한 내용과 `excludeCollections` 오적용 주의(`blog` 자체가 선언 섹션인 사이트에
64
+ > 쓰면 목록이 빈다)는 `collections.md` 참고.
56
65
 
57
66
  ### 상세 페이지
58
67
 
@@ -120,6 +129,40 @@ export default async function PostPage({
120
129
  경우 `@roottale/cms-core` 의 `extractFaqEntries(doc)` / `faqPageJsonLd(entries)`
121
130
  로 동일한 JSON-LD 를 생성할 수 있습니다.
122
131
 
132
+ ### 다국어 (ADR-0052 A안, W4-6 PR C1/C2)
133
+
134
+ 번역 사이트(어드민에서 "번역 추가"로 언어판을 만든 글)는 `RootTaleBlogList`/
135
+ `RootTaleBlogPost`/`RootTalePage` 에 `locale` prop 을 넘기면 그 언어판만
136
+ fetch 합니다(`fetchPosts`/`fetchPost` 의 `locale` 쿼리 그대로 전달). 미지정
137
+ 시 서버가 사이트 기본 locale 만 반환합니다(하위호환).
138
+
139
+ ```tsx
140
+ // app/[locale]/blog/[slug]/page.tsx — starter 참조 구현 패턴
141
+ const { locale, slug } = await params;
142
+ <RootTaleBlogPost
143
+ apiKey={process.env.ROOTTALE_API_KEY!}
144
+ slugOrId={slug}
145
+ locale={locale}
146
+ showTableOfContents
147
+ />;
148
+ ```
149
+
150
+ - **URL 전략**(권장, starter 참조 구현) — 기본 locale 은 prefix 없는 기존
151
+ 경로(`/blog/{slug}`) 그대로 두고, 나머지 locale 만 `app/[locale]/...` 로
152
+ 별도 트리를 둡니다(`app/[locale]/blog/`·`app/[locale]/blog/[slug]/`·
153
+ `app/[locale]/[slug]/`). 사이트 활성 locale 목록은 별도 API 없이
154
+ `fetchBlogSettings().sitemap.locales`(SSoT = `sites.settingsJson.locales`)
155
+ 로 얻습니다 — 첫 원소가 기본 locale.
156
+ - `<head>` hreflang · 사이트맵 hreflang 은 "SEO 연동" 문서(`seo.md`)의
157
+ "다국어 (hreflang)" 절 참고.
158
+ - **UI 문자열**(cms-i18n, ADR-0034 §3) — `RootTaleBlogList.emptyMessage`/
159
+ `allCategoriesLabel`, `RootTaleBlogPost.relatedPostsTitle`/미발견 안내,
160
+ `RootTalePage` 미발견 안내, `RootTaleTableOfContents.title` 은 명시 prop
161
+ 을 안 주면 `locale` prop 기준으로 자동 번역됩니다(`@roottale/cms-i18n`
162
+ ko/en 사전, `ja`/`zh-hans` 등 사전 없는 locale 은 ko 로 fallback). 글
163
+ 본문·admin UI 는 이 자동 번역 대상이 아닙니다(사람이 직접 작성/ADR-0052
164
+ 범위 밖).
165
+
123
166
  ### 고정 페이지 (회사소개 등)
124
167
 
125
168
  어드민의 고정 페이지(`type: "page"`)는 `RootTalePage`로 렌더링합니다 — 블로그
@@ -223,7 +266,7 @@ export async function generateMetadata({
223
266
 
224
267
  입력은 `{ title, description?, date?, image?, seo? }` 형태면 되고
225
268
  (`getPost` 의 `BlogPostMeta` 가 그대로 호환), 원본 post 를 쓸 땐
226
- `seo: (post.metaJson as { seo?: ... }).seo` 로 넘기세요.
269
+ `seo`로 넘길 값은 `(post.metaJson as { seo?: ... }).seo`입니다.
227
270
 
228
271
  SEO 오버라이드 필드: `title`, `description`, `canonical`, `ogImage`,
229
272
  `noindex`, `nofollow`.
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: 콘텐츠 유형 (Collections) — 공지·블로그 URL 분리
2
+ title: 콘텐츠 유형 (Collections)
3
3
  description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등 여러 섹션으로 나누는 법. 섹션=글의 collection_key, 카테고리=섹션 안의 주제. URL 구조, 어드민 설정, 연동 코드, slug·301·OG.
4
4
  ---
5
5
 
@@ -55,7 +55,7 @@ description: 같은 글 풀을 공지 게시판(/notice)·블로그(/blog) 등
55
55
  > 이전 버전은 *카테고리로 섹션을 추론*했지만, 지금은 섹션이 글에 **명시**됩니다.
56
56
  > 카테고리는 더 이상 어느 섹션에 속하는지를 결정하지 않습니다(주제 아카이브 전용).
57
57
 
58
- ## 어드민에서 설정 (`mysite.roottale.com`)
58
+ ## 어드민에서 설정 (`admin.roottale.com`)
59
59
 
60
60
  **설정 > 콘텐츠 유형** 에서 섹션을 정의합니다. 빈 상태의 "공지 + 블로그 한 번에 만들기"
61
61
  버튼으로 표준 두 섹션을 한 번에 만들 수 있습니다. 각 섹션은:
@@ -198,6 +198,41 @@ export default function BlogPage() {
198
198
  `RootTaleBlogCategories` 도 같은 `collection`+`collections` 를 받아 그 섹션의 주제만
199
199
  집계합니다. 카테고리 칩/사이드바를 섹션별로 나눌 때 쓰세요.
200
200
 
201
+ #### `/blog` 가 선언 섹션이 *아닌* 사이트 — `excludeCollections` (네거티브 스코프)
202
+
203
+ 증상/질환/치료처럼 **아카이브 전용 스트림만 선언하고, 나머지("미소속") 글을
204
+ `/blog` 로 보여주는** 사이트도 있습니다. 이런 사이트는 `blog` 라는 섹션 자체가
205
+ 없으므로 `collection="blog"` 로 스코프할 수 없습니다 — 대신 `collections`+
206
+ **`excludeCollections`** 로 "어느 스트림에도 안 속한 글만" 골라냅니다.
207
+
208
+ ```tsx
209
+ // app/blog/page.tsx — 증상/질환/치료 스트림은 따로, 나머지는 블로그
210
+ import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
211
+ import { ARCHIVE_COLLECTIONS } from "@/lib/collections"; // symptoms/conditions/treatments
212
+
213
+ export default function BlogPage() {
214
+ return (
215
+ <RootTaleBlogList
216
+ apiKey={process.env.ROOTTALE_API_KEY!}
217
+ collections={ARCHIVE_COLLECTIONS}
218
+ excludeCollections
219
+ showCategoryFilter
220
+ />
221
+ );
222
+ }
223
+ ```
224
+
225
+ ⚠️ **`blog` 자체가 선언 섹션인 사이트에는 쓰지 마세요.** `collections` 목록에
226
+ `{ key: "blog", ... }` 가 있고 글의 `collectionKey === "blog"` 라면, 그 글은
227
+ "소속 글"로 판정되어 `excludeCollections` 가 오히려 **전부 제외**해 버립니다(빈
228
+ 목록). 그런 사이트는 위 "섹션 목록 페이지" 예시처럼 포지티브 `collection="blog"`
229
+ 가 정경로입니다.
230
+
231
+ 규칙:
232
+
233
+ - `collection` 과 `excludeCollections` 동시 지정 = **throw**(포지티브/네거티브 배타).
234
+ - `collections` 없이 `excludeCollections` 만 지정 = **throw**(조용한 no-op 금지).
235
+
201
236
  > 동적 basePath(어드민에서 자유 편집)나 catch-all 라우트를 쓰면 `collection` 값을
202
237
  > 요청 경로에서 판정해 넘기세요(아래 "동적 basePath" 참고).
203
238
 
@@ -5,7 +5,7 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
5
5
 
6
6
  # 주소 이동 (커스텀 리다이렉트) 연동
7
7
 
8
- 어드민(mysite.roottale.com)의 **설정 > 주소 이동**에서 운영자가 정의한 임의
8
+ 어드민(admin.roottale.com)의 **설정 > 주소 이동**에서 운영자가 정의한 임의
9
9
  경로 이동 규칙(`/old-event → /promo`)을 사이트 미들웨어로 적용합니다. 코드
10
10
  수정 없이 고객이 직접 규칙을 추가·수정·삭제할 수 있습니다.
11
11
 
@@ -7,7 +7,7 @@ description: API 키 발급, 환경변수 설정, 패키지 설치, 첫 콘텐
7
7
 
8
8
  ## 1. API 키 발급
9
9
 
10
- 1. 어드민(`mysite.roottale.com`) 로그인
10
+ 1. 어드민(`admin.roottale.com`) 로그인
11
11
  2. **설정 > 사이트 연결 키** 메뉴로 이동
12
12
  3. 새 키 발급 — 권한 선택:
13
13
  - **read** (기본): 발행된 콘텐츠 조회만. 외부 사이트 연동은 이걸로 충분
package/docs/menus.md CHANGED
@@ -5,7 +5,7 @@ description: 어드민 "디자인 > 메뉴"에서 관리하는 네비게이션
5
5
 
6
6
  # 메뉴 (네비게이션) 연동
7
7
 
8
- 어드민(mysite.roottale.com)의 **디자인 > 메뉴**에서 저장한 네비게이션 트리를
8
+ 어드민(admin.roottale.com)의 **디자인 > 메뉴**에서 저장한 네비게이션 트리를
9
9
  사이트의 헤더·푸터에 렌더합니다. 고객이 직접 메뉴 항목(이름·주소·순서·하위
10
10
  항목)을 바꿀 수 있어 코드 수정 없이 네비가 갱신됩니다.
11
11
 
package/docs/overview.md CHANGED
@@ -5,12 +5,12 @@ description: 연동 아키텍처, 단일 API 키 모델, 패키지 구성, 문
5
5
 
6
6
  # RootTale CMS 연동 개요
7
7
 
8
- RootTale CMS는 어드민(`mysite.roottale.com`)에서 콘텐츠를 작성·발행하고, 외부
8
+ RootTale CMS는 어드민(`admin.roottale.com`)에서 콘텐츠를 작성·발행하고, 외부
9
9
  고객 사이트(자체 도메인의 Next.js/Astro 등)가 공개 API(`api.roottale.com`)로
10
10
  콘텐츠를 가져가는 헤드리스 구조입니다.
11
11
 
12
12
  ```
13
- 어드민 (mysite.roottale.com) 고객 사이트 (예: example.com)
13
+ 어드민 (admin.roottale.com) 고객 사이트 (예: example.com)
14
14
  글 작성·발행 ──────────┐
15
15
  ▼
16
16
  api.roottale.com ◀── Bearer rtlk_cust_* ── 콘텐츠/설정 조회
package/docs/seo.md CHANGED
@@ -105,19 +105,33 @@ export default async function AuthorArchive({
105
105
  - `RootTaleBlogList`에 `author={slug}` 를 주면 그 작가의 글만 가져옵니다
106
106
  (`GET /v1/cms/public/posts?author={slug}`).
107
107
 
108
- ### 다국어 (hreflang)
109
-
110
- 번역 페이지가 있는 사이트는 어드민 **설정 > 블로그 > 사이트맵**의 "다국어 로케일"에
111
- 언어 코드를 입력하면(예: `ko, ja, en`), 사이트맵의 모든 항목에 `hreflang` 대체 언어
112
- 링크(`<xhtml:link rel="alternate">`)가 붙습니다.
113
-
114
- - **첫 번째** 로케일이 기본 언어(현재 URL 그대로) + `x-default`.
115
- - 나머지는 **경로 접두사**로 매핑됩니다 — `ko`가 기본이면 `ja`는
116
- `/ja/blog/{slug}`, `en`은 `/en/blog/{slug}`.
117
- - 같은 slug에 언어 접두사만 붙는 1:1 구조를 가정합니다. **실제 번역 페이지를
118
- 그 경로(`/ja/...`)에 서빙하는 것은 사이트 쪽 책임**입니다 — 사이트맵은 검색엔진에
119
- 대체 언어를 알릴 뿐입니다.
120
- - 비워두면 단일 언어로 동작합니다(기본).
108
+ ### 다국어 (hreflang) — ADR-0052 A안, 콘텐츠 다국어
109
+
110
+ 번역 글이 있는 사이트는 어드민 **설정 > 블로그 > 사이트맵**의 "다국어 로케일"에
111
+ 언어 코드를 입력합니다(예: `ko, ja, en`, 첫 번째 = 기본 locale). 사이트맵의
112
+ `blog` 섹션(`GET /sitemap/blog.xml`)이 **글 단위 실제 번역 데이터**로 hreflang
113
+ 대체 언어 링크(`<xhtml:link rel="alternate">`)를 계산합니다:
114
+
115
+ - 공개 API(`GET /v1/cms/public/posts`)가 글마다 `translations`(발행된 언어판
116
+ 목록, 자기 자신 포함)를 내려줍니다. 언어판이 없는 글(자기 자신 1건뿐)은
117
+ hreflang alternates 자체가 생략됩니다 — 존재하지 않는 URL 을 검색엔진에
118
+ 알리는 사고를 원천 차단합니다(옛 slug 1:1 path-prefix 가정 제거).
119
+ - **기본 locale** = prefix 없는 경로 + `x-default`. 나머지는 **경로 접두사**
120
+ (`ko`가 기본이면 `ja` 언어판은 `/ja/blog/{그-언어판-실제-slug}`) — 언어판마다
121
+ **독립된 slug**를 가질 수 있습니다(동일 slug 강제 아님).
122
+ - **실제 번역 라우트를 그 경로(`/ja/...`)에 서빙하는 것은 사이트 쪽 책임**
123
+ 입니다 — starter 참조 구현은 `app/[locale]/blog/`·`app/[locale]/blog/[slug]/`·
124
+ `app/[locale]/[slug]/`(기본 locale 은 prefix 없는 기존 경로 그대로).
125
+ - **`static`/`categories`/`authors` 섹션은 hreflang alternates 를 붙이지
126
+ 않습니다** — v1 은 블로그 글 + CMS `[slug]` 페이지만 번역 대상이라(nav 정적
127
+ 경로·카테고리 아카이브·작가 아카이브는 locale 라우트 자체가 없음), 옛 방식대로
128
+ path-prefix 를 씌우면 존재하지 않는 URL 의 hreflang 이 됩니다.
129
+ - 로케일을 비워두면(또는 기본 locale 조차 발행 글 0건이면) 단일 언어로
130
+ 동작합니다(alternates 미부착, fail-closed).
131
+
132
+ 글 상세 `<head>` hreflang 은 `buildPostMetadata`(아래 "글 메타데이터" 절 참고)의
133
+ `locales`/`translations` 옵션으로 계산합니다 — 사이트맵과 같은 원본 데이터
134
+ (`translations[]`)를 씁니다.
121
135
 
122
136
  ## 다중 스트림 (collections) — 공지·블로그 분리
123
137
 
@@ -244,6 +258,37 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
244
258
  canonical 을 생략합니다(섹션 없는 글은 상세·sitemap에서 제외되는 규칙과 동일).
245
259
  단일 블로그 사이트는 기존처럼 `path: "/blog/" + post.slug` 만 주면 됩니다.
246
260
 
261
+ ### 다국어 hreflang (ADR-0052 A안, W4-6 PR C1)
262
+
263
+ 번역 사이트는 `translations`(글 응답의 `translations[]`)와 `locales` 옵션을
264
+ 넘기면 `<head>` 에 양방향 hreflang(+ `x-default`)이 자동으로 붙습니다:
265
+
266
+ ```tsx
267
+ import { buildPostMetadata } from "@roottale/cms-renderer-next/routes";
268
+
269
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
270
+ const { locale, slug } = await params; // app/[locale]/blog/[slug]/page.tsx
271
+ const post = await getPost(slug, locale);
272
+ if (!post) return {};
273
+ return buildPostMetadata(
274
+ { ...post, translations: post.translations },
275
+ {
276
+ siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
277
+ path: `/${locale}/blog/${post.slug}`,
278
+ locales: { defaultLocale: "ko" }, // 사이트 활성 locale 의 첫 원소
279
+ },
280
+ );
281
+ }
282
+ ```
283
+
284
+ - `pathFor` 를 안 주면 기본값 `기본locale → /blog/{slug}`, `그 외 → /{locale}/blog/{slug}`.
285
+ 블로그가 아닌 라우트(CMS `[slug]` 페이지 등)는 `pathFor` override 필수 —
286
+ `buildHreflangLanguages(translations, siteUrl, { defaultLocale, pathFor })`
287
+ 를 직접 호출하면 OG `type:"article"` 없이 plain `alternates` 만 조립할 수
288
+ 있습니다.
289
+ - `translations` 가 없거나(구 서버) 자기 자신 1건뿐(번역 없음)이면 `languages`
290
+ 자체가 생략됩니다 — 단일 언어 사이트는 기존 canonical-only 동작 그대로.
291
+
247
292
  ## slug 변경 시 301 리다이렉트
248
293
 
249
294
  글 slug를 바꿔도 옛 URL이 깨지지 않습니다. API가 slug history로 글을 찾아
@@ -407,9 +452,10 @@ export default async function RootLayout({ children }) {
407
452
  - `sameAs`: 어드민에 입력한 네이버플레이스·구글 비즈니스 프로필·카카오 채널
408
453
  등의 URL — 검색엔진이 동일 사업장임을 연결합니다.
409
454
 
410
- 네이버플레이스(new.smartplace.naver.com)와 구글 비즈니스 프로필
411
- (business.google.com) 등록 자체는 공개 API가 없어 사장님이 직접 해야 하며,
412
- 어드민 화면에 등록 안내와 프로필 URL 입력란이 있습니다.
455
+ 네이버플레이스([new.smartplace.naver.com](https://new.smartplace.naver.com))와
456
+ 구글 비즈니스 프로필([business.google.com](https://business.google.com)) 등록
457
+ 자체는 공개 API가 없어 사장님이 직접 해야 하며, 어드민 화면에 등록 안내와
458
+ 프로필 URL 입력란이 있습니다.
413
459
 
414
460
  ## Fleet 프로브 (운영 가시성)
415
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.41.1",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS integration MCP server — bundled integration docs, Next.js example code, and public API lookup tools. Run with: npx @roottale/cms-mcp",
6
6
  "bin": {