@roottale/cms-mcp 0.69.0 → 0.70.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,15 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.70.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 49145e4: 콘텐츠 공개·색인 정책의 기존 단일 when 조건을 유지하면서, 공개 필드가 모두 일치하는 all과 모두 일치할 때만 검사를 제외하는 unlessAll을 추가합니다. 각 그룹은 equality 조건 1~4개만 허용하고 중첩하지 않으며, 기존 공개 필드 참조·검사 개수 한도에 포함합니다.
8
+
9
+ 명시적인 방송 기록 조건이 모두 충족된 경우에만 검수·출연자 조건의 예외를 적용하고, 출처 필수와 강제 검색 제외를 함께 선언하는 공개 예제를 제공합니다. 누락 값과 null은 false로 바꾸지 않으며 관리자 projection·웹훅·FRONT가 같은 정책을 사용합니다.
10
+
11
+ 글의 공개 영상 필드를 `videoThumbnail: { source: "youtube", field, when? }`로 연결합니다. 선언한 모델에만 안전한 대표 이미지 다음으로 정확한 YouTube ID의 썸네일을 사용하며, 수동 SEO와 기존 무옵션 모델의 이미지 의미는 유지합니다. 같은 공개 필드와 조건을 관리자 자동값·버전·검증과 FRONT 카드/메타데이터에 재사용할 수 있습니다.
12
+
3
13
  ## 0.69.0
4
14
 
5
15
  ### 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.69.0" : "dev";
1858
+ var VERSION = true ? "0.70.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/seo.md CHANGED
@@ -69,6 +69,24 @@ export async function generateMetadata() {
69
69
 
70
70
  공유 이미지 자동값은 대표 이미지 → 선언한 본문 첫 이미지·이전 이미지 순입니다. `useFeaturedImage: false` 또는 분류의 `useTermImage: false`로 기존 화면에서 사용하지 않던 콘텐츠 이미지를 제외할 수 있습니다. `defaultRules.defaults`는 콘텐츠 값이 없을 때 사용할 코드 기본값으로, 사이트 공통값보다 먼저 적용됩니다. 글에 고정 공유 이미지를 쓰려면 `useFeaturedImage: false`와 `defaults.ogImage`를 함께 선언하고 본문·이전 이미지 규칙은 켜지 않습니다. `defaultRules.fallback`은 사이트 공통값 뒤에 적용됩니다.
71
71
 
72
+ SDK 0.70.0부터 글의 공개 영상 필드를 이미지 자동값으로 연결할 수 있습니다.
73
+ `videoThumbnail: { source: "youtube", field: "video_url", when: { field: "content_format", equals: "media" } }`를
74
+ 등록하고 FRONT에도 같은 규칙과 `fields: post.fields`를 전달합니다. 이 옵션을 선언한
75
+ 모델은 안전한 절대 HTTPS 대표 이미지 → 조건에 맞는 YouTube 썸네일 → 기존
76
+ 본문·이전 이미지 → 코드·사이트 기본값 순으로 계산합니다. 대표 이미지가 HTTP·상대
77
+ 주소·인증정보 포함 URL이면 영상 후보를 사용하며, 수동 SEO 이미지에는 이 변환을
78
+ 적용하지 않습니다. 옵션 없는 모델의 기존 이미지 처리는 유지됩니다.
79
+
80
+ YouTube 입력은 인증정보·공백·제어문자·역슬래시 없는 HTTPS URL만 받습니다.
81
+ `youtu.be/<id>` 또는 `youtube.com`·`www.youtube.com`·`m.youtube.com`의
82
+ `watch?v=<id>`·`shorts/<id>`·`embed/<id>`에서 영문·숫자·`_`·`-`로 된 11자 ID만
83
+ 읽고 `https://i.ytimg.com/vi/<id>/hqdefault.jpg`를 만듭니다. 시작 시각은 이미지에
84
+ 영향을 주지 않습니다. 임의 호스트·이미지 URL 형식은 선언할 수 없습니다.
85
+ 카드에서도 같은 자동 썸네일이 필요하면 core의
86
+ `metadataVideoThumbnail(rule.videoThumbnail, post.fields)`를 사용합니다.
87
+ 영상·조건·같이 선언한 텍스트 토큰은 공개 필드 투영을 사용하며 비공개 필드나
88
+ 원본 ACF를 넘기지 않습니다. 글 자동값 전용 옵션이며 분류에는 선언하지 않습니다.
89
+
72
90
  검색 차단을 유지해야 하는 동적 모델은 `defaultRules[modelKey].robots: { forceNoindex: true, forceNofollow: true }`를 선언하고 같은 `robots`를 `resolveMetadata`나 페이지 빌더에 전달합니다. 직접 입력한 `false`로 이 제한을 해제할 수 없습니다. 기존 모델 소속과 실제 FRONT 분류 경로가 다른 경우 `metadata.terms.archivePaths: { notice: "/blog/:slug" }`처럼 등록 모델별 내부 경로를 선언합니다. 이 값은 메타데이터 조회·편집·재검증 주소만 맞추며 공개 라우트나 콘텐츠 소속을 변경하지 않습니다. 마지막 `:slug` 하나만 지원합니다.
73
91
 
74
92
  `fetchCategoryCounts`는 API의 `category_index_activated`를 `categoryIndexActivated`로 보존합니다. 색인 전환을 요구하는 계약에서는 이 값이 `true`일 때만 `hubPromoted` 기반 정책을 적용하고 이전 정책은 그때까지 유지합니다. 분류 메타데이터 조회에도 `tags: [METADATA_CACHE_TAG]`를 전달할 수 있습니다.
@@ -122,12 +140,46 @@ export const blogQuery = {
122
140
 
123
141
  필드 검사는 `equals`·`notEquals`·`oneOf`·`text`·`date`·`safeHttps`·`allowedPath`와
124
142
  한 단계 repeater의 `some`만 지원합니다. `when: { field, equals }`로 특정 값일 때만
125
- 검사할 수 있습니다. 임의 함수·정규식·중첩 repeater·관계형 값은 정책 입력이 아닙니다.
143
+ 검사할 수 있습니다. SDK 0.70.0부터는 `when: { all: [...], unlessAll: [...] }`도
144
+ 지원합니다. `all`은 조건이 모두 일치해야 하고, `unlessAll`은 조건이 모두 일치할
145
+ 때만 검사를 생략합니다. 두 그룹 중 하나 이상을 선언하며 각 그룹은 equality 조건
146
+ 1~4개만 받습니다. 빈 그룹·중첩·단일 조건과 그룹의 혼합은 허용하지 않습니다.
147
+ 조건의 필드도 공개 필드 64개 한도에 포함합니다. `visibility`·`indexing` 각각의
148
+ 검사 24개 한도는 그대로입니다. `all`과 `unlessAll`을 함께 쓰면 두 제한을 모두
149
+ 충족해야 합니다. 누락·`null`은 명시적인 `false`와 같지 않습니다.
150
+ 임의 함수·정규식·중첩 repeater·관계형 값은 정책 입력이 아닙니다.
126
151
  `post.fields`의 공개 값만 사용하고 비공개 ACF를 전달하지 마세요. 누락 값은
127
152
  `undefined`이며 `notEquals: false`는 누락도 통과합니다. `oneOf`의
128
153
  `allowEmpty: true`는 누락·`null`·빈 문자열·`false`·`0`도 허용하므로 필수 필드에는
129
154
  해당 옵션을 사용하지 않습니다.
130
155
 
156
+ 예를 들어 방송 기록을 `content_format === "media"`·`broadcast_record === true`·
157
+ `medical_content === false`인 경우로 한정한다면, 다음 조건들을 **같은 전체 정책**의
158
+ 검수·출연자·출처 검사에 연결합니다. 예외일 때만 일부 검사를 생략하고, 출처 필수와
159
+ 강제 검색 제외는 유지합니다. 이 조건 선언만으로 글을 공개하지는 않습니다.
160
+
161
+ ```ts
162
+ // 실행 예제 lib/content-policy.ts가 같은 조건을 제공합니다.
163
+ const record = [
164
+ { field: "content_format", equals: "media" },
165
+ { field: "broadcast_record", equals: true },
166
+ { field: "medical_content", equals: false },
167
+ ];
168
+ const reviewWhen = { unlessAll: record };
169
+ const performerWhen = {
170
+ all: [{ field: "content_format", equals: "media" }], unlessAll: record,
171
+ };
172
+ const recordSourcesWhen = { all: record };
173
+ // indexing에 추가하면 예외 방송 기록은 수동 noindex:false로 풀 수 없습니다.
174
+ const recordNoindexCheck = {
175
+ field: "broadcast_record", op: "notEquals", value: true,
176
+ when: { all: [
177
+ { field: "content_format", equals: "media" },
178
+ { field: "medical_content", equals: false },
179
+ ] },
180
+ };
181
+ ```
182
+
131
183
  `bodyContent`는 표시 가능한 HTML 또는 JSON 텍스트를 확인하고, 명시한 경우에만
132
184
  요약·대표 이미지·`additionalUrl`을 대체 콘텐츠로 허용합니다. `bodyText`는 사용할
133
185
  HTML이 있으면 그 텍스트를, 없으면 JSON의 `text` 노드를 읽습니다. 이미지뿐인
@@ -7,6 +7,7 @@ import {
7
7
  type MetadataDefaultsDeclaration,
8
8
  type MetadataValues,
9
9
  } from "@roottale/cms-client/server";
10
+ import { contentPolicySafeHttpsUrl, metadataVideoThumbnail } from "@roottale/cms-core";
10
11
 
11
12
  /** Register this same declaration in the deployed site's metadata.posts contract. */
12
13
  export const contentMetadataDefaults: MetadataDefaultsDeclaration = {
@@ -20,6 +21,7 @@ export const contentMetadataDefaults: MetadataDefaultsDeclaration = {
20
21
  defaults: { ogImage: "/images/logo.png", ogImageAlt: "사무소 로고" },
21
22
  },
22
23
  blog: {
24
+ videoThumbnail: { source: "youtube", field: "video_url", when: { field: "content_format", equals: "media" } },
23
25
  description: {
24
26
  normalizeWhitespace: true, maxLength: 160, lengthUnit: "code-points", ellipsis: "…",
25
27
  bodyFallback: {}, fallback: "%s에 관한 안내입니다.",
@@ -29,6 +31,13 @@ export const contentMetadataDefaults: MetadataDefaultsDeclaration = {
29
31
  },
30
32
  };
31
33
 
34
+ /** Card image uses the same declared public field; it does not copy site SEO defaults. */
35
+ export function contentCardImage(post: CmsPostContent): string | undefined {
36
+ const modelKey = post.modelKey ?? post.collectionKey ?? "blog";
37
+ const rule = contentMetadataDefaults.posts?.defaultRules?.[modelKey];
38
+ return contentPolicySafeHttpsUrl(post.featuredImageUrl) ?? metadataVideoThumbnail(rule?.videoThumbnail, post.fields);
39
+ }
40
+
32
41
  /** Metadata cleanup applies to automatic values only. Stored SEO and canonical remain separate. */
33
42
  export function contentMetadata(post: CmsPostContent, siteUrl: string, siteDefaults: MetadataValues): MetadataValues {
34
43
  const modelKey = post.modelKey ?? post.collectionKey ?? "blog";
@@ -1,8 +1,10 @@
1
- // @roottale/cms-core를 사이트의 직접 의존성에 추가합니다(SDK 0.69.0+).
1
+ // @roottale/cms-core를 사이트의 직접 의존성에 추가합니다(SDK 0.70.0+).
2
2
  import {
3
+ ContentPolicyWhenSchema,
3
4
  evaluateContentPolicy,
4
5
  resolvePostPublicPath,
5
6
  type ContentPolicy,
7
+ type ContentPolicyCheck,
6
8
  type ContentQueryCandidate,
7
9
  type MetadataRobotsPolicy,
8
10
  type PublicPathContext,
@@ -25,6 +27,29 @@ export const registeredContentPolicies:
25
27
 
26
28
  export type PolicyPost = CmsPostContent & { path: string | null };
27
29
 
30
+ // 예외 조건도 등록한 정책 안에 둡니다. 이 상수만으로 글을 공개하지 않습니다.
31
+ // 아래 세 값이 정확히 일치할 때만 방송 기록이며 누락/null은 false가 아닙니다.
32
+ const broadcastRecordFields = [
33
+ { field: "content_format", equals: "media" },
34
+ { field: "broadcast_record", equals: true },
35
+ { field: "medical_content", equals: false },
36
+ ];
37
+ export const broadcastRecordWhen = ContentPolicyWhenSchema.parse({ all: broadcastRecordFields });
38
+ export const reviewExceptBroadcastRecordWhen = ContentPolicyWhenSchema.parse({ unlessAll: broadcastRecordFields });
39
+ export const performerExceptBroadcastRecordWhen = ContentPolicyWhenSchema.parse({
40
+ all: [{ field: "content_format", equals: "media" }],
41
+ unlessAll: broadcastRecordFields,
42
+ });
43
+
44
+ /** Add to the full indexing policy; direct noindex:false cannot override a failed check. */
45
+ export const broadcastRecordNoindexCheck: ContentPolicyCheck = {
46
+ field: "broadcast_record", op: "notEquals", value: true,
47
+ when: { all: [
48
+ { field: "content_format", equals: "media" },
49
+ { field: "medical_content", equals: false },
50
+ ] },
51
+ };
52
+
28
53
  // 문서의 compatibilityCollectionKey: null인 blog 모델 예제입니다.
29
54
  // 경로 계산에는 글에 붙은 분류만이 아닌 모델의 전체 graph가 필요합니다.
30
55
  // null collectionKey는 기존 blog 범위이며 응답 원본은 재작성하지 않습니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS MCP server and CLI for models, entries, exposures, media, integration docs, and public API access.",
6
6
  "bin": {