@roottale/cms-mcp 0.69.0 → 0.70.1
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 +16 -0
- package/dist/index.js +1 -1
- package/docs/content-models-and-exposures.md +19 -0
- package/docs/custom-redirects.md +8 -1
- package/docs/seo.md +57 -1
- package/examples/nextjs/cms/site-link-rules.example.json +12 -0
- package/examples/nextjs/lib/content-metadata.ts +9 -0
- package/examples/nextjs/lib/content-policy.ts +26 -1
- package/examples/nextjs/lib/route-metadata.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @roottale/cms-mcp
|
|
2
2
|
|
|
3
|
+
## 0.70.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7cef947: FRONT 전용 상세 주소를 쓰는 콘텐츠의 ROOT-ADMIN 사이트 열기 규칙과 Next.js 계약 예시를 추가합니다. 저장 주소·공개 API·리다이렉트는 변경하지 않습니다.
|
|
8
|
+
|
|
9
|
+
## 0.70.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 49145e4: 콘텐츠 공개·색인 정책의 기존 단일 when 조건을 유지하면서, 공개 필드가 모두 일치하는 all과 모두 일치할 때만 검사를 제외하는 unlessAll을 추가합니다. 각 그룹은 equality 조건 1~4개만 허용하고 중첩하지 않으며, 기존 공개 필드 참조·검사 개수 한도에 포함합니다.
|
|
14
|
+
|
|
15
|
+
명시적인 방송 기록 조건이 모두 충족된 경우에만 검수·출연자 조건의 예외를 적용하고, 출처 필수와 강제 검색 제외를 함께 선언하는 공개 예제를 제공합니다. 누락 값과 null은 false로 바꾸지 않으며 관리자 projection·웹훅·FRONT가 같은 정책을 사용합니다.
|
|
16
|
+
|
|
17
|
+
글의 공개 영상 필드를 `videoThumbnail: { source: "youtube", field, when? }`로 연결합니다. 선언한 모델에만 안전한 대표 이미지 다음으로 정확한 YouTube ID의 썸네일을 사용하며, 수동 SEO와 기존 무옵션 모델의 이미지 의미는 유지합니다. 같은 공개 필드와 조건을 관리자 자동값·버전·검증과 FRONT 카드/메타데이터에 재사용할 수 있습니다.
|
|
18
|
+
|
|
3
19
|
## 0.69.0
|
|
4
20
|
|
|
5
21
|
### 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.70.1" : "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.
|
|
@@ -517,3 +517,22 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|
|
517
517
|
- `GET /v1/cms/public/posts?model_key=team-member`
|
|
518
518
|
- `GET /v1/cms/public/categories?collection_key=faq` (`id`, `parent_id` 포함)
|
|
519
519
|
- `GET /v1/cms/public/exposures?slot_key=global-popup&path=%2Fabout`
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
## FRONT 전용 상세 주소로 사이트 열기
|
|
523
|
+
|
|
524
|
+
한 모델의 일부 형식만 별도 상세 화면을 쓰면 사이트 콘텐츠 계약의 해당 `models` 항목에 `siteLinkRules`를 선언합니다. 예를 들어 `blog`의 `content_format=media`는 다음처럼 연결합니다.
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"modelKey": "blog",
|
|
529
|
+
"preset": "article",
|
|
530
|
+
"siteLinkRules": [
|
|
531
|
+
{ "field": "content_format", "equals": "media", "detailPath": "/news/media/:slug" }
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
ROOT-ADMIN 글 목록·편집 화면의 공개/검수 사이트 열기가 저장된 `metaJson.acf` 값과 규칙을 대조합니다. 해당 형식만 선언한 내부 상세 주소로 열며, 다른 형식은 기존 주소를 유지합니다. 규칙이 충돌하거나 저장 주소가 없는 글에는 링크를 만들지 않습니다. 예시는 `examples/nextjs/cms/site-link-rules.example.json`에 있습니다.
|
|
537
|
+
|
|
538
|
+
이 선언은 글의 저장 주소·공개 API·주소 이력·리다이렉트를 바꾸지 않습니다. FRONT는 저장 주소를 검증하고 실제 상세·canonical·목록을 같은 전용 경로로 구현해야 합니다. 해당 FRONT 배포를 확인한 뒤 계약을 등록하세요. 관리자 링크를 고치기 위해 폐기한 개발 주소를 다시 열지 않습니다.
|
package/docs/custom-redirects.md
CHANGED
|
@@ -38,6 +38,13 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
|
|
|
38
38
|
기존 캐시가 있으면 오래된 규칙을 우선 사용하고(stale-first), 캐시가
|
|
39
39
|
없으면 트래픽을 막지 않고 통과시킵니다(fail-soft).
|
|
40
40
|
|
|
41
|
+
Proxy는 모든 페이지 요청 앞에서 실행되므로 API 장애가 사이트 전체를 멈추지
|
|
42
|
+
않도록 다음과 같이 동작합니다.
|
|
43
|
+
|
|
44
|
+
- 규칙 조회는 최대 `timeoutMs`(기본 1.5초)만 기다립니다.
|
|
45
|
+
- 캐시가 만료돼도 기존 규칙이 있으면 기다리지 않고 쓰고, 갱신은 뒤에서 한 번만 합니다.
|
|
46
|
+
- 조회가 실패하면 30초 동안 다시 조회하지 않고, 그동안 기존 규칙(없으면 통과)을 씁니다.
|
|
47
|
+
|
|
41
48
|
```ts
|
|
42
49
|
// proxy.ts
|
|
43
50
|
import { NextResponse } from "next/server";
|
|
@@ -45,7 +52,7 @@ import { createRedirectMiddleware } from "@roottale/cms-renderer-next/routes";
|
|
|
45
52
|
|
|
46
53
|
const redirects = createRedirectMiddleware({
|
|
47
54
|
apiKey: process.env.ROOTTALE_API_KEY!,
|
|
48
|
-
// apiBase, siteId, cacheTtlMs 는 선택.
|
|
55
|
+
// apiBase, siteId, cacheTtlMs, timeoutMs 는 선택.
|
|
49
56
|
});
|
|
50
57
|
|
|
51
58
|
export async function proxy(req: Request) {
|
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]`를 전달할 수 있습니다.
|
|
@@ -85,6 +103,10 @@ SDK 0.69.0의 `ContentPolicy`·`ContentQuery`는 `@roottale/cms-core`에서 제
|
|
|
85
103
|
`visibility`를 통과한 글만 화면에 표시하고, `indexing`까지 통과한 글만 검색에
|
|
86
104
|
허용합니다. 결과의 `visible`과 `indexable`은 별개이며 `robots`를 메타데이터
|
|
87
105
|
빌더에 전달해야 직접 설정한 `noindex: false`가 강제 제한을 풀지 못합니다.
|
|
106
|
+
관리자·공개 메타데이터 응답의 `robots.noindexReasons`는 같은 공통 스키마로
|
|
107
|
+
검증한 차단 사유 코드입니다. `robots` 전체를 보존해 전달하고, 사유 코드로
|
|
108
|
+
강제 제한을 해제하지 마세요. `version`은 정책 의존 값이 포함될 수 있는
|
|
109
|
+
불투명 문자열이므로 그대로 전달하며 숫자로 변환하거나 증가시키지 않습니다.
|
|
88
110
|
|
|
89
111
|
```ts
|
|
90
112
|
import type { ContentPolicy, ContentQuery } from "@roottale/cms-core";
|
|
@@ -122,12 +144,46 @@ export const blogQuery = {
|
|
|
122
144
|
|
|
123
145
|
필드 검사는 `equals`·`notEquals`·`oneOf`·`text`·`date`·`safeHttps`·`allowedPath`와
|
|
124
146
|
한 단계 repeater의 `some`만 지원합니다. `when: { field, equals }`로 특정 값일 때만
|
|
125
|
-
검사할 수 있습니다.
|
|
147
|
+
검사할 수 있습니다. SDK 0.70.0부터는 `when: { all: [...], unlessAll: [...] }`도
|
|
148
|
+
지원합니다. `all`은 조건이 모두 일치해야 하고, `unlessAll`은 조건이 모두 일치할
|
|
149
|
+
때만 검사를 생략합니다. 두 그룹 중 하나 이상을 선언하며 각 그룹은 equality 조건
|
|
150
|
+
1~4개만 받습니다. 빈 그룹·중첩·단일 조건과 그룹의 혼합은 허용하지 않습니다.
|
|
151
|
+
조건의 필드도 공개 필드 64개 한도에 포함합니다. `visibility`·`indexing` 각각의
|
|
152
|
+
검사 24개 한도는 그대로입니다. `all`과 `unlessAll`을 함께 쓰면 두 제한을 모두
|
|
153
|
+
충족해야 합니다. 누락·`null`은 명시적인 `false`와 같지 않습니다.
|
|
154
|
+
임의 함수·정규식·중첩 repeater·관계형 값은 정책 입력이 아닙니다.
|
|
126
155
|
`post.fields`의 공개 값만 사용하고 비공개 ACF를 전달하지 마세요. 누락 값은
|
|
127
156
|
`undefined`이며 `notEquals: false`는 누락도 통과합니다. `oneOf`의
|
|
128
157
|
`allowEmpty: true`는 누락·`null`·빈 문자열·`false`·`0`도 허용하므로 필수 필드에는
|
|
129
158
|
해당 옵션을 사용하지 않습니다.
|
|
130
159
|
|
|
160
|
+
예를 들어 방송 기록을 `content_format === "media"`·`broadcast_record === true`·
|
|
161
|
+
`medical_content === false`인 경우로 한정한다면, 다음 조건들을 **같은 전체 정책**의
|
|
162
|
+
검수·출연자·출처 검사에 연결합니다. 예외일 때만 일부 검사를 생략하고, 출처 필수와
|
|
163
|
+
강제 검색 제외는 유지합니다. 이 조건 선언만으로 글을 공개하지는 않습니다.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// 실행 예제 lib/content-policy.ts가 같은 조건을 제공합니다.
|
|
167
|
+
const record = [
|
|
168
|
+
{ field: "content_format", equals: "media" },
|
|
169
|
+
{ field: "broadcast_record", equals: true },
|
|
170
|
+
{ field: "medical_content", equals: false },
|
|
171
|
+
];
|
|
172
|
+
const reviewWhen = { unlessAll: record };
|
|
173
|
+
const performerWhen = {
|
|
174
|
+
all: [{ field: "content_format", equals: "media" }], unlessAll: record,
|
|
175
|
+
};
|
|
176
|
+
const recordSourcesWhen = { all: record };
|
|
177
|
+
// indexing에 추가하면 예외 방송 기록은 수동 noindex:false로 풀 수 없습니다.
|
|
178
|
+
const recordNoindexCheck = {
|
|
179
|
+
field: "broadcast_record", op: "notEquals", value: true,
|
|
180
|
+
when: { all: [
|
|
181
|
+
{ field: "content_format", equals: "media" },
|
|
182
|
+
{ field: "medical_content", equals: false },
|
|
183
|
+
] },
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
131
187
|
`bodyContent`는 표시 가능한 HTML 또는 JSON 텍스트를 확인하고, 명시한 경우에만
|
|
132
188
|
요약·대표 이미지·`additionalUrl`을 대체 콘텐츠로 허용합니다. `bodyText`는 사용할
|
|
133
189
|
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.
|
|
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 범위이며 응답 원본은 재작성하지 않습니다.
|
|
@@ -20,6 +20,7 @@ export async function routeMetadata(
|
|
|
20
20
|
return buildPageMetadata({
|
|
21
21
|
...route,
|
|
22
22
|
fallback: options.fallback,
|
|
23
|
+
// noindexReasons를 포함한 공통 정책을 보존하며 직접 false로 강제 차단을 풀지 않는다.
|
|
23
24
|
robots: { ...route?.robots, forceNoindex: options.forceNoindex || route?.robots?.forceNoindex },
|
|
24
25
|
}, {
|
|
25
26
|
canonical: options.canonical,
|
package/package.json
CHANGED