@roottale/cms-mcp 0.67.0 → 0.68.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 +14 -0
- package/dist/index.js +1 -1
- package/docs/api-reference.md +2 -1
- package/docs/custom-redirects.md +1 -1
- package/docs/seo.md +11 -1
- package/examples/nextjs/lib/content-metadata.ts +43 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @roottale/cms-mcp
|
|
2
2
|
|
|
3
|
+
## 0.68.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d7130d8: 기존 고객 사이트의 검색·공유 기본값을 선언형 계약으로 보존합니다. 문자열 커스텀 필드 템플릿, 본문 문단 대체값, Unicode와 단어 경계 말줄임, 고정 이미지 기본값, 모델별 강제 robots와 분류 공개 경로를 지원합니다. 수동 SEO 설정은 우선하며 강제 검색 차단은 유지합니다.
|
|
8
|
+
|
|
9
|
+
분류 조회 SDK에 색인 전환 상태와 추가 캐시 태그를 연결하고 외부 연동 문서·실행 예제를 함께 제공합니다.
|
|
10
|
+
|
|
11
|
+
## 0.67.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 7eeb714: 공통 메타데이터·주소 이동 편집 조건 문서에 루트테일의 사이트별 기능 허용·제한을 반영하고, 메타데이터 수정 API의 403 코드(`upgrade_required`·`site_denied`·`billing_locked`)를 명시한다.
|
|
16
|
+
|
|
3
17
|
## 0.67.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -1855,7 +1855,7 @@ function registerTools(server) {
|
|
|
1855
1855
|
}
|
|
1856
1856
|
|
|
1857
1857
|
// src/server.ts
|
|
1858
|
-
var VERSION = true ? "0.
|
|
1858
|
+
var VERSION = true ? "0.68.0" : "dev";
|
|
1859
1859
|
var SERVER_INSTRUCTIONS = `
|
|
1860
1860
|
roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
|
|
1861
1861
|
\uAE00\xB7\uC378\uB124\uC77C\xB7\uBCF8\uBB38 \uC774\uBBF8\uC9C0\uB97C \uC790\uB3D9\uD654\uD558\uAE30 \uC704\uD55C \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7API tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
|
package/docs/api-reference.md
CHANGED
|
@@ -32,7 +32,8 @@ MCP tool 또는 공개 CLI를 사용합니다.
|
|
|
32
32
|
`GET /v1/cms/metadata/{kind}/{key}`로 현재 버전과 지원 필드를 확인합니다.
|
|
33
33
|
`kind`는 `route | post | term`입니다. `PATCH`는
|
|
34
34
|
`{ "expected_version": "조회한 version", "patch": { "title": null, "noindex": false } }`
|
|
35
|
-
형식이며 수정은 Pro/Premium과 대상별 쓰기 scope가 모두 필요합니다.
|
|
35
|
+
형식이며 수정은 Pro/Premium(또는 루트테일의 사이트별 허용)과 대상별 쓰기 scope가 모두 필요합니다.
|
|
36
|
+
편집이 닫혀 있으면 403 `upgrade_required`(요금제)·`site_denied`(사이트별 제한)·`billing_locked`(구독 중단)를 반환합니다.
|
|
36
37
|
`site_id`·`locale`은 선택입니다. 본문만 수정하면 저장된 SEO를 보존합니다.
|
|
37
38
|
고정 경로 공개 조회는 `GET /v1/cms/public/metadata?route_key=home&locale=ko`이며
|
|
38
39
|
`{ "site_id": "…", "metadata": null }` 또는 camelCase `PublicRouteMetadata`를 반환합니다.
|
package/docs/custom-redirects.md
CHANGED
|
@@ -9,7 +9,7 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
|
|
|
9
9
|
경로 이동 규칙(`/old-event → /promo`)을 사이트 Proxy로 적용합니다. 코드
|
|
10
10
|
수정 없이 고객이 직접 규칙을 추가·수정·삭제할 수 있습니다.
|
|
11
11
|
|
|
12
|
-
수동 등록·수정·삭제·CSV 적용·되돌리기는 **Pro/Premium
|
|
12
|
+
수동 등록·수정·삭제·CSV 적용·되돌리기는 **Pro/Premium** 또는 루트테일이 해당 사이트에 주소 이동을 허용한 경우에 사용할 수 있습니다.
|
|
13
13
|
기본 요금제로 내려가도 기존 규칙은 계속 적용되고 읽기·내보내기를 유지합니다.
|
|
14
14
|
일반 글 주소 변경에 따른 자동 301은 기본 기능입니다. 공개 조회와 Proxy에
|
|
15
15
|
요금제 검사를 추가하면 기존 주소가 끊어지므로 읽기에는 등급 제한을 두지 마세요.
|
package/docs/seo.md
CHANGED
|
@@ -31,7 +31,7 @@ export const GET = createFeedRoute({
|
|
|
31
31
|
|
|
32
32
|
## 공통 메타데이터
|
|
33
33
|
|
|
34
|
-
ROOT-ADMIN **설정 → 검색·공유 표시**에서 홈·코드 페이지·목록 첫 화면·글·분류를 관리합니다. 제목·설명·공유 이미지·이미지 대체 텍스트·`noindex`·`nofollow`를 사이트가 지원하는 범위에서 편집합니다. 등록·수정·초기화는 **Pro/Premium
|
|
34
|
+
ROOT-ADMIN **설정 → 검색·공유 표시**에서 홈·코드 페이지·목록 첫 화면·글·분류를 관리합니다. 제목·설명·공유 이미지·이미지 대체 텍스트·`noindex`·`nofollow`를 사이트가 지원하는 범위에서 편집합니다. 등록·수정·초기화는 **Pro/Premium** 또는 루트테일이 해당 사이트에 이 기능을 허용한 경우에 가능합니다. 루트테일이 사이트별로 제한하면 요금제와 관계없이 편집이 닫힙니다. 기본 요금제로 내려가거나 허용이 풀려도 기존 설정의 공개 적용과 읽기·내보내기는 유지됩니다. 일반 본문 편집과 대표 이미지 변경은 이 제한과 별개입니다.
|
|
35
35
|
|
|
36
36
|
본문이 없는 페이지는 콘텐츠 계약 `metadata.routes`에 안정적인 `routeKey`를 등록합니다. 코드 화면은 `source: { kind: "code", path: "/about" }`, 기존 본문 페이지는 `{ kind: "page", pageKey: "about" }`, 모델 목록은 `{ kind: "model-list", modelKey: "column" }`으로 선언합니다. CMS 페이지를 메타데이터 저장 용도로 새로 만들 필요가 없습니다. 주소를 바꿔도 같은 페이지의 `routeKey`는 유지합니다. SDK 설치만으로 편집을 열지는 않으며 실제 배포 계약·연동 증거가 필요합니다.
|
|
37
37
|
|
|
@@ -63,6 +63,16 @@ export async function generateMetadata() {
|
|
|
63
63
|
|
|
64
64
|
글은 공개 응답의 `metaJson.seo`, 분류는 `seoTitle/seoDescription/seoOgImage/seoOgImageAlt/seoNofollow`를 같은 빌더의 `overrides`에 연결합니다. 분류 색인 여부는 기존 `hubPromoted` 정책을 유지합니다. 글마다 경로 메타데이터 API를 추가 호출하지 않습니다. 본문이 있는 CMS 페이지는 `lib/page-metadata.ts` 예시처럼 기존 글 원본을 사용합니다.
|
|
65
65
|
|
|
66
|
+
동적 기본값은 등록 계약의 `metadata.posts/terms.defaultRules[modelKey]`와 FRONT의 `formatMetadataDefaults(input, metadata)`에 같은 선언을 사용합니다. `title/description.source: "template"`은 `{{title}}`과 `{{field:필드이름}}`만 치환합니다. `fields`에는 공개 글의 `post.fields`를 전달하며 문자열 필드만 사용합니다. 객체·배열·누락 필드는 빈 문자열로 처리하고, 필드 안의 토큰을 다시 해석하지 않습니다. `normalizeWhitespace: true`로 비어 있는 선택 필드 사이의 공백을 정리합니다. 실행식·정규식은 받지 않습니다. 실행 가능한 연동 예시는 `examples/nextjs/lib/content-metadata.ts`입니다.
|
|
67
|
+
|
|
68
|
+
요약이 없을 때 본문 문단을 사용하는 사이트는 `description.bodyFallback: {}`와 `bodyParagraphs: metadataBodyParagraphs(post.bodyJson)`를 연결합니다. `preferContaining`은 포함 문구가 있는 첫 문단을 우선하고, 없으면 첫 문단을 사용합니다. `stripLeadingLabel: "conclusion"`은 이 본문 대체값에서 기존 한국어 “결론부터 말씀드리면/말씀드릴게요” 도입부를 제거합니다. `maxLength`·`ellipsis`에 `lengthUnit: "code-points"`를 더하면 이모지를 중간에서 자르지 않습니다. `wordBoundaryMinLength`는 잘린 문자열의 마지막 공백이 지정한 UTF-16 위치 이상일 때 그 공백에서 자릅니다. `titleTemplateMaxLength`는 사이트 접미사를 붙인 자동 제목이 이 UTF-16 길이를 넘을 때 원래 자동 제목을 유지합니다. 직접 입력한 SEO 문구에는 이 변환을 적용하지 않습니다.
|
|
69
|
+
|
|
70
|
+
공유 이미지 자동값은 대표 이미지 → 선언한 본문 첫 이미지·이전 이미지 순입니다. `useFeaturedImage: false` 또는 분류의 `useTermImage: false`로 기존 화면에서 사용하지 않던 콘텐츠 이미지를 제외할 수 있습니다. `defaultRules.defaults`는 콘텐츠 값이 없을 때 사용할 코드 기본값으로, 사이트 공통값보다 먼저 적용됩니다. 글에 고정 공유 이미지를 쓰려면 `useFeaturedImage: false`와 `defaults.ogImage`를 함께 선언하고 본문·이전 이미지 규칙은 켜지 않습니다. `defaultRules.fallback`은 사이트 공통값 뒤에 적용됩니다.
|
|
71
|
+
|
|
72
|
+
검색 차단을 유지해야 하는 동적 모델은 `defaultRules[modelKey].robots: { forceNoindex: true, forceNofollow: true }`를 선언하고 같은 `robots`를 `resolveMetadata`나 페이지 빌더에 전달합니다. 직접 입력한 `false`로 이 제한을 해제할 수 없습니다. 기존 모델 소속과 실제 FRONT 분류 경로가 다른 경우 `metadata.terms.archivePaths: { notice: "/blog/:slug" }`처럼 등록 모델별 내부 경로를 선언합니다. 이 값은 메타데이터 조회·편집·재검증 주소만 맞추며 공개 라우트나 콘텐츠 소속을 변경하지 않습니다. 마지막 `:slug` 하나만 지원합니다.
|
|
73
|
+
|
|
74
|
+
`fetchCategoryCounts`는 API의 `category_index_activated`를 `categoryIndexActivated`로 보존합니다. 색인 전환을 요구하는 계약에서는 이 값이 `true`일 때만 `hubPromoted` 기반 정책을 적용하고 이전 정책은 그때까지 유지합니다. 분류 메타데이터 조회에도 `tags: [METADATA_CACHE_TAG]`를 전달할 수 있습니다.
|
|
75
|
+
|
|
66
76
|
조회에는 `METADATA_CACHE_TAG`, `SITE_METADATA_DEFAULTS_CACHE_TAG`, `routeMetadataCacheTag(routeKey, locale)`가 자동 부착됩니다. 기본 재조회 주기는 300초이며 `revalidate`로 조정할 수 있습니다. 기존 서명 `theme.updated`와 `createRevalidateRoute`가 공통 메타데이터 태그·루트 layout·영향 경로를 무효화합니다. 수신부에 `(tag) => revalidateTag(tag, { expire: 0 })`를 전달해야 합니다. 공개 읽기·SSR·Proxy에는 요금제 조건을 넣지 마세요.
|
|
67
77
|
|
|
68
78
|
사이트맵도 같은 최종 `noindex`·대표 경로 정책을 사용하고 활성 리다이렉트 출발 주소를 제외해야 합니다. 목록의 페이지 번호·필터별 정책은 사이트 라우트에서 계산합니다. 메타데이터 저장 성공·웹훅 수신 성공·실제 HTML 반영 확인은 서로 다른 상태입니다.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatMetadataDefaults,
|
|
3
|
+
metadataBodyParagraphs,
|
|
4
|
+
readMetadataValues,
|
|
5
|
+
resolveMetadata,
|
|
6
|
+
type CmsPostContent,
|
|
7
|
+
type MetadataDefaultsDeclaration,
|
|
8
|
+
type MetadataValues,
|
|
9
|
+
} from "@roottale/cms-client/server";
|
|
10
|
+
|
|
11
|
+
/** Register this same declaration in the deployed site's metadata.posts contract. */
|
|
12
|
+
export const contentMetadataDefaults: MetadataDefaultsDeclaration = {
|
|
13
|
+
posts: {
|
|
14
|
+
titleTemplates: { expert: "%s | 예시 사무소", blog: "%s | 예시 사무소" },
|
|
15
|
+
defaultRules: {
|
|
16
|
+
expert: {
|
|
17
|
+
title: { source: "template", template: "{{title}} {{field:role}}" },
|
|
18
|
+
description: { source: "template", template: "{{field:summary}} {{title}}의 담당 분야", normalizeWhitespace: true },
|
|
19
|
+
useFeaturedImage: false,
|
|
20
|
+
defaults: { ogImage: "/images/logo.png", ogImageAlt: "사무소 로고" },
|
|
21
|
+
},
|
|
22
|
+
blog: {
|
|
23
|
+
description: {
|
|
24
|
+
normalizeWhitespace: true, maxLength: 160, lengthUnit: "code-points", ellipsis: "…",
|
|
25
|
+
bodyFallback: {}, fallback: "%s에 관한 안내입니다.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Metadata cleanup applies to automatic values only. Stored SEO and canonical remain separate. */
|
|
33
|
+
export function contentMetadata(post: CmsPostContent, siteUrl: string, siteDefaults: MetadataValues): MetadataValues {
|
|
34
|
+
const modelKey = post.modelKey ?? post.collectionKey ?? "blog";
|
|
35
|
+
const rule = contentMetadataDefaults.posts?.defaultRules?.[modelKey];
|
|
36
|
+
const defaults = formatMetadataDefaults({
|
|
37
|
+
kind: "posts", modelKey, title: post.title, description: post.excerpt,
|
|
38
|
+
fields: post.fields, bodyParagraphs: metadataBodyParagraphs(post.bodyJson),
|
|
39
|
+
featuredImage: post.featuredImageUrl,
|
|
40
|
+
}, contentMetadataDefaults);
|
|
41
|
+
return resolveMetadata({ defaults, siteDefaults, overrides: readMetadataValues(post.metaJson.seo),
|
|
42
|
+
fallback: rule?.fallback, robots: rule?.robots, imageBaseUrl: siteUrl });
|
|
43
|
+
}
|
package/package.json
CHANGED