@roottale/cms-mcp 0.36.0 → 0.39.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,48 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.39.0
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: linked cms-\* 그룹 버전 정렬 — cms-media(0.27.0)·cms-mcp(0.37.0) 이
8
+ 부분 릴리스로 그룹(0.38.0)에서 드리프트. 6개 linked 패키지를 한 릴리스에 묶어
9
+ 공통 버전으로 재정렬한다.
10
+
11
+ ## 0.37.0
12
+
13
+ ### Minor Changes
14
+
15
+ - de62ad1: 작가 아카이브 페이지 + 작가 사이트맵 (blanche식 /blog/author/{slug})
16
+ - `fetchAuthors()` 추가 (`GET /v1/cms/public/authors`) — slug가 있고 발행 글이 1건
17
+ 이상인 작가 목록(slug·이름·이미지·소개·글 수·최근 수정일). 어드민 **설정 > 팀**에서
18
+ 작가 slug를 발급한다(site 내 unique).
19
+ - `RootTaleBlogList` 에 `author` prop, `fetchPosts` 에 `author` 옵션 추가 —
20
+ `GET /v1/cms/public/posts?author={slug}` 로 그 작가의 발행 글만 렌더(작가 아카이브용).
21
+ - `createSitemapIndex` 에 `authors` 하위 사이트맵(`/sitemap/authors.xml`) 추가 — 어드민
22
+ **설정 > 블로그 > 사이트맵**의 "작가 사이트맵"(`authors`) 토글이 켜졌을 때만 인덱스에
23
+ 포함. `authorBasePath` 옵션(기본 `/blog/author`)으로 경로 커스터마이즈.
24
+ - starter 에 `app/blog/author/[slug]/page.tsx` 추가 + cms-mcp seo.md 문서 갱신.
25
+
26
+ - c77238d: 사이트맵 다국어 hreflang (스캐폴딩)
27
+ - `createSitemapIndex` 가 사이트맵 설정의 `locales`(예 `["ko","ja","en"]`)가 있으면
28
+ 모든 섹션(static/blog/categories/authors) entry 에 `alternates.languages`(hreflang)
29
+ 를 단다. 첫 로케일 = 기본(현 URL) + `x-default`, 나머지는 path-prefix(`/ja/...`).
30
+ 어드민 **설정 > 블로그 > 사이트맵** 의 "다국어 로케일"에서 입력.
31
+ - 같은 slug + 언어 접두사 1:1 을 가정하는 스캐폴딩 — 실제 per-locale 페이지 서빙은
32
+ 사이트 책임(번역 콘텐츠 모델 도입 시 재방문). 비우면 단일 언어(기본).
33
+ - 어드민 사이트맵 카드에 "작가 사이트맵"(authors) 토글도 노출 — 작가 사이트맵을
34
+ UI 에서 켤 수 있다.
35
+
36
+ - 36e2dae: 사이트맵 인덱스 분리 + 블로그 이미지 색인 (blanche식)
37
+ - `createSitemapIndex(config, staticRoutes, opts)` 추가 — Next 16 `generateSitemaps`
38
+ 로 `/sitemap.xml`(인덱스) + `/sitemap/static.xml`·`/sitemap/blog.xml`·
39
+ `/sitemap/categories.xml` 하위 사이트맵으로 분리. 50k URL/50MB 한도 확장 + 검색엔진
40
+ 섹션별 크롤. 소비자는 `export { generateSitemaps }; export default sitemap;`.
41
+ - 블로그 글 entry 에 대표 이미지(`featuredImageUrl`)를 `<image:image>` 로 색인 —
42
+ 어드민 **설정 > 블로그 > 사이트맵**의 토글(`blogImages`)로 제어. `RootTaleBlogSettings.sitemap`
43
+ (`/v1/cms/public/blog-settings` 응답)에 사이트맵 정책(`blogImages`/`locales`/`authors`) 추가.
44
+ - `changeFrequency`/`priority` 는 더 이상 emit 하지 않음(Google 무시 — `loc`+`lastmod` +이미지만). 레거시 `createSitemap`(단일 평면)은 deprecate 되었으나 하위호환 유지.
45
+
3
46
  ## 0.36.0
4
47
 
5
48
  ### Patch Changes
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.36.0" : "dev";
282
+ var VERSION = true ? "0.39.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.
@@ -115,10 +115,13 @@ export const COLLECTIONS: RouteCollection[] = [
115
115
 
116
116
  ```ts
117
117
  // app/sitemap.ts
118
- import { createSitemap } from "@roottale/cms-renderer-next/routes";
119
- export default createSitemap({ apiKey, siteUrl, title, collections: COLLECTIONS }, [
120
- /* 정적 경로 */
121
- ]);
118
+ import { createSitemapIndex } from "@roottale/cms-renderer-next/routes";
119
+ const { generateSitemaps, sitemap } = createSitemapIndex(
120
+ { apiKey, siteUrl, title, collections: COLLECTIONS },
121
+ [ /* 정적 경로 */ ],
122
+ );
123
+ export { generateSitemaps };
124
+ export default sitemap;
122
125
 
123
126
  // app/feed.xml/route.ts
124
127
  export const dynamic = "force-dynamic";
@@ -144,7 +147,12 @@ async function getCollections(): Promise<RouteCollection[]> {
144
147
  }
145
148
  }
146
149
 
147
- export default createSitemap({ apiKey, siteUrl, title, collections: getCollections }, [ ]);
150
+ const { generateSitemaps, sitemap } = createSitemapIndex(
151
+ { apiKey, siteUrl, title, collections: getCollections },
152
+ [ ],
153
+ );
154
+ export { generateSitemaps };
155
+ export default sitemap;
148
156
  export const GET = createFeedRoute({ apiKey, siteUrl, title, collections: getCollections });
149
157
  ```
150
158
 
package/docs/seo.md CHANGED
@@ -29,13 +29,18 @@ export const GET = createFeedRoute({
29
29
 
30
30
  ## 사이트맵
31
31
 
32
+ `createSitemapIndex`는 **사이트맵 인덱스**(`/sitemap.xml`)와 섹션별 하위 사이트맵
33
+ (`/sitemap/static.xml`·`/sitemap/blog.xml`·`/sitemap/categories.xml`)을 만듭니다.
34
+ 하나의 거대한 파일 대신 섹션별로 나뉘어 검색엔진이 더 잘 크롤하고, 50,000 URL/50MB
35
+ 한도에도 안전합니다. 발행 글·카테고리는 자동 포함됩니다.
36
+
32
37
  ```ts
33
38
  // app/sitemap.ts
34
- import { createSitemap } from "@roottale/cms-renderer-next/routes";
39
+ import { createSitemapIndex } from "@roottale/cms-renderer-next/routes";
35
40
 
36
41
  const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
37
42
 
38
- export default createSitemap(
43
+ const { generateSitemaps, sitemap } = createSitemapIndex(
39
44
  {
40
45
  apiKey: process.env.ROOTTALE_API_KEY!,
41
46
  apiBase: process.env.ROOTTALE_API_BASE,
@@ -43,14 +48,77 @@ export default createSitemap(
43
48
  title: "예시 사이트",
44
49
  },
45
50
  [
46
- // 정적 경로 — 발행 글 URL은 자동 추가됨
47
- { url: SITE_URL, changeFrequency: "weekly", priority: 1.0 },
48
- { url: `${SITE_URL}/blog`, changeFrequency: "weekly", priority: 0.7 },
49
- { url: `${SITE_URL}/contact`, changeFrequency: "monthly", priority: 0.9 },
51
+ // 정적 경로 — 발행 글·카테고리 URL은 자동 추가됨
52
+ { url: SITE_URL },
53
+ { url: `${SITE_URL}/blog` },
54
+ { url: `${SITE_URL}/contact` },
50
55
  ],
51
56
  );
57
+
58
+ // Next 16: 인덱스를 만들려면 generateSitemaps·default 를 둘 다 export.
59
+ export { generateSitemaps };
60
+ export default sitemap;
52
61
  ```
53
62
 
63
+ - **블로그 이미지 색인** — 글에 대표 이미지가 있으면 `<image:image>`로 함께 색인합니다.
64
+ 어드민 **설정 > 블로그 > 사이트맵**에서 켜고 끌 수 있어요(기본 켜짐).
65
+ - `changeFrequency`/`priority`는 Google이 무시하므로 더 이상 내보내지 않습니다(`loc` +
66
+ `lastmod` + 이미지만).
67
+ - 단일 평면 사이트맵이 필요하면 레거시 `createSitemap`(default export 하나)도 그대로
68
+ 동작하지만, 신규 사이트는 `createSitemapIndex`를 권장합니다.
69
+
70
+ ### 작가 아카이브 (`/blog/author/{slug}`)
71
+
72
+ 어드민 **설정 > 팀**에서 작가에게 주소(slug)를 발급하면, 그 작가의 글 모음 페이지와
73
+ 작가 사이트맵(`/sitemap/authors.xml`)을 만들 수 있습니다. 어드민 **설정 > 블로그 >
74
+ 사이트맵**에서 "작가 사이트맵"을 켜면 인덱스에 `authors` 섹션이 추가됩니다.
75
+
76
+ ```ts
77
+ // app/blog/author/[slug]/page.tsx
78
+ import { notFound } from "next/navigation";
79
+ import { fetchAuthors } from "@roottale/cms-client/server";
80
+ import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
81
+
82
+ export default async function AuthorArchive({
83
+ params,
84
+ }: {
85
+ params: Promise<{ slug: string }>;
86
+ }) {
87
+ const { slug } = await params;
88
+ const authors = await fetchAuthors({ apiKey: process.env.ROOTTALE_API_KEY! });
89
+ const author = authors.find((a) => a.slug === slug);
90
+ if (!author) notFound();
91
+
92
+ return (
93
+ <>
94
+ <h1>{author.name}</h1>
95
+ {author.bio ? <p>{author.bio}</p> : null}
96
+ {/* author= 로 그 작가의 발행 글만 렌더 */}
97
+ <RootTaleBlogList apiKey={process.env.ROOTTALE_API_KEY!} author={slug} />
98
+ </>
99
+ );
100
+ }
101
+ ```
102
+
103
+ - `fetchAuthors()` 는 slug가 있고 발행 글이 1건 이상인 작가만 반환합니다
104
+ (`GET /v1/cms/public/authors`).
105
+ - `RootTaleBlogList`에 `author={slug}` 를 주면 그 작가의 글만 가져옵니다
106
+ (`GET /v1/cms/public/posts?author={slug}`).
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
+ - 비워두면 단일 언어로 동작합니다(기본).
121
+
54
122
  ## 다중 스트림 (collections) — 공지·블로그 분리
55
123
 
56
124
  같은 글 풀을 공지 게시판(`/notice`) + 블로그(`/blog`) 등 여러 섹션으로 나눠 서로 다른
@@ -88,6 +88,53 @@ const config = await fetchAnalyticsConfig({
88
88
  `enabled: true`인 태그만 렌더링하세요. 태그 ID는 어드민에서 변경될 수
89
89
  있으므로 하드코딩하지 말고 본 API로 조회하는 것을 권장합니다.
90
90
 
91
+ ## 조회수 / first-party 비콘
92
+
93
+ RootTale 비콘은 쿠키리스 first-party 분석(방문수·클릭)과 **글별 조회수**를
94
+ 수집합니다. **API 키 하나로** 동작합니다 — 별도 사이트 ID 환경변수가 필요 없습니다.
95
+ `fetchAnalyticsConfig`가 돌려주는 `siteId`를 비콘에 그대로 넘기세요.
96
+
97
+ ```tsx
98
+ // app/layout.tsx (Next.js) — 서버 컴포넌트
99
+ import { renderBeaconScript } from "@roottale/analytics-runtime";
100
+ import { fetchAnalyticsConfig } from "@roottale/cms-client/server";
101
+
102
+ const cfg = await fetchAnalyticsConfig({ apiKey: process.env.ROOTTALE_API_KEY! });
103
+ // ...<body> 안에:
104
+ <script
105
+ dangerouslySetInnerHTML={{
106
+ __html: renderBeaconScript({
107
+ collectUrl: "https://api.roottale.com/v1/collect",
108
+ siteId: cfg.siteId, // ← API 키에서 유도. 별도 env 불필요.
109
+ }),
110
+ }}
111
+ />
112
+ ```
113
+
114
+ **글별 조회수**가 정확히 집계되려면, 글 상세 페이지가 자신의 글 ID를
115
+ `<meta name="rt:content-id">`로 노출해야 합니다. 비콘이 이 값을 읽어 조회를 해당
116
+ 글에 귀속시킵니다(URL·경로가 바뀌어도 안정적).
117
+
118
+ ```tsx
119
+ // app/blog/[slug]/page.tsx — generateMetadata
120
+ export async function generateMetadata({ params }): Promise<Metadata> {
121
+ const post = await fetchPost({ apiKey: process.env.ROOTTALE_API_KEY!, slugOrId: slug });
122
+ return {
123
+ title: post.title,
124
+ other: { "rt:content-id": post.id }, // ← 조회수 식별자
125
+ };
126
+ }
127
+ ```
128
+
129
+ > `@roottale/cms-renderer-next`의 `buildPostMetadata(post, …)`를 쓰면 이 meta가
130
+ > **자동으로** 들어갑니다(별도 작업 불필요). 프레임워크 무관 환경(Astro 등)에서는
131
+ > `@roottale/cms-client/server`의 `contentIdMeta(post.id)`가 같은 `<meta>` 태그
132
+ > 문자열을 만들어 줍니다.
133
+
134
+ 수집은 익명·쿠키리스이며 비콘은 클릭(`data-track`)과 pageview만 보냅니다. 봇
135
+ 트래픽은 서버에서 제외됩니다. 공개 사이트에 "조회 N"을 표시하는 옵션은 어드민의
136
+ 사이트 설정에서 켤 수 있습니다(켜면 글 응답에 `view_count`가 포함됩니다).
137
+
91
138
  ## 사이트 지식 — 브랜드 보이스 (AI 에이전트용)
92
139
 
93
140
  이 사이트의 **브랜드 보이스**(어조·톤·화자)와 **용어 규칙**(금지어·교정어)을
@@ -13,8 +13,18 @@ export const POST = createRevalidateRoute({
13
13
  apiKey: process.env.ROOTTALE_API_KEY!,
14
14
  apiBase: process.env.ROOTTALE_API_BASE,
15
15
  revalidate: revalidateBlogPath,
16
- // 홈에 최신 글 섹션이 있으면 "/" 포함 — 글 변경 시 홈도 함께 갱신
17
- alsoRevalidate: ["/feed.xml", "/sitemap.xml", "/blog", "/"],
16
+ // 사이트맵 인덱스 + 분리된 하위 사이트맵(static/blog/categories)을 모두 무효화.
17
+ // 홈에 최신 글 섹션이 있으면 "/" 포함 — 글 변경 시 홈도 함께 갱신.
18
+ alsoRevalidate: [
19
+ "/feed.xml",
20
+ "/sitemap.xml",
21
+ "/sitemap/static.xml",
22
+ "/sitemap/blog.xml",
23
+ "/sitemap/categories.xml",
24
+ "/sitemap/authors.xml",
25
+ "/blog",
26
+ "/",
27
+ ],
18
28
  });
19
29
 
20
30
  export function GET(): Response {
@@ -1,19 +1,20 @@
1
- // 사이트맵 — 정적 경로 + 발행 글 URL 자동 포함.
2
- import { createSitemap } from "@roottale/cms-renderer-next/routes";
1
+ // 사이트맵 — 인덱스 분리(/sitemap.xml → /sitemap/static.xml·blog·categories).
2
+ // 정적 경로 + 발행 글·카테고리 자동 포함. 블로그 글 대표 이미지는 admin 토글에 따라
3
+ // <image:image> 로 함께 색인(어드민 설정 > 블로그 > 사이트맵).
4
+ import { createSitemapIndex } from "@roottale/cms-renderer-next/routes";
3
5
 
4
6
  const SITE_URL =
5
7
  process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") || "https://example.com";
6
8
 
7
- export default createSitemap(
9
+ const { generateSitemaps, sitemap } = createSitemapIndex(
8
10
  {
9
11
  apiKey: process.env.ROOTTALE_API_KEY!,
10
12
  apiBase: process.env.ROOTTALE_API_BASE,
11
13
  siteUrl: SITE_URL,
12
14
  title: "예시 사이트",
13
15
  },
14
- [
15
- { url: SITE_URL, changeFrequency: "weekly", priority: 1.0 },
16
- { url: `${SITE_URL}/blog`, changeFrequency: "weekly", priority: 0.7 },
17
- { url: `${SITE_URL}/contact`, changeFrequency: "monthly", priority: 0.9 },
18
- ],
16
+ [{ url: SITE_URL }, { url: `${SITE_URL}/blog` }, { url: `${SITE_URL}/contact` }],
19
17
  );
18
+
19
+ export { generateSitemaps };
20
+ export default sitemap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.36.0",
3
+ "version": "0.39.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": {