@roottale/cms-mcp 0.41.1 → 0.44.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/docs/overview.md CHANGED
@@ -5,12 +5,21 @@ 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
+ ## 권장 연동 순서
13
+
14
+ 1. `getting-started.md` — 키 발급 + 환경 설정
15
+ 2. `blog.md` — `/blog` 목록·상세 페이지
16
+ 3. `revalidation-webhooks.md` — 웹훅 등록 (발행 → 즉시 반영)
17
+ 4. `seo.md` — RSS·사이트맵·동적 OG 이미지
18
+ 5. `inquiries.md` — 상담문의 폼 (선택)
19
+ 6. `menus.md` — 어드민 관리 네비게이션 (선택)
20
+
12
21
  ```
13
- 어드민 (mysite.roottale.com) 고객 사이트 (예: example.com)
22
+ 어드민 (admin.roottale.com) 고객 사이트 (예: example.com)
14
23
  글 작성·발행 ──────────┐
15
24
  ▼
16
25
  api.roottale.com ◀── Bearer rtlk_cust_* ── 콘텐츠/설정 조회
@@ -53,12 +62,3 @@ RootTale CMS는 어드민(`mysite.roottale.com`)에서 콘텐츠를 작성·발
53
62
  | `seo.md` | RSS 피드, 사이트맵, JSON-LD, 동적 OG 이미지, 공개 검색, fleet 프로브 |
54
63
  | `theme-and-settings.md` | 디자인 토큰, 블로그 표시 설정, 분석 태그 |
55
64
  | `api-reference.md` | HTTP API 레퍼런스 (비 JS 스택용 raw 엔드포인트) |
56
-
57
- ## 권장 연동 순서
58
-
59
- 1. `getting-started.md` — 키 발급 + 환경 설정
60
- 2. `blog.md` — `/blog` 목록·상세 페이지
61
- 3. `revalidation-webhooks.md` — 웹훅 등록 (발행 → 즉시 반영)
62
- 4. `seo.md` — RSS·사이트맵·동적 OG 이미지
63
- 5. `inquiries.md` — 상담문의 폼 (선택)
64
- 6. `menus.md` — 어드민 관리 네비게이션 (선택)
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
 
@@ -41,10 +41,12 @@ export default async function PostPage({ params }: Props) {
41
41
 
42
42
  return (
43
43
  <main>
44
+ <h1>{post.title}</h1>
44
45
  <RootTaleBlogPost
45
46
  apiKey={process.env.ROOTTALE_API_KEY!}
46
47
  baseUrl={process.env.ROOTTALE_API_BASE}
47
48
  slugOrId={post.id}
49
+ showTitle={false}
48
50
  />
49
51
  </main>
50
52
  );
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.41.1",
3
+ "version": "0.44.0",
4
4
  "type": "module",
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",
5
+ "description": "RootTale CMS MCP server and CLI for post publishing, media uploads, integration docs, and public API access.",
6
6
  "bin": {
7
7
  "roottale-cms-mcp": "dist/index.js"
8
8
  },
@@ -16,6 +16,8 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.0.0",
19
+ "commander": "^13.0.0",
20
+ "ky": "^1.14.0",
19
21
  "zod": "^3.23.8"
20
22
  },
21
23
  "devDependencies": {