@roottale/cms-mcp 0.73.0 → 0.74.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 +12 -0
- package/dist/index.js +1 -1
- package/docs/api-reference.md +32 -0
- package/docs/blog.md +7 -0
- package/docs/inquiries.md +5 -1
- package/docs/revalidation-webhooks.md +1 -0
- package/docs/seo.md +40 -0
- package/examples/nextjs/app/api/revalidate/route.ts +2 -2
- package/examples/nextjs/lib/route-metadata.ts +14 -5
- package/examples/nextjs/lib/sync-external-inquiries.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @roottale/cms-mcp
|
|
2
2
|
|
|
3
|
+
## 0.74.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e243cd: 블로그가 아닌 컬렉션(공지·일정 등)의 상세 화면에 `RootTaleBlogPost`를 쓸 때 `categoryHref`를 지정해야 분류 링크가 기본값 `/blog/categories/{slug}`로 나가 404가 되지 않는다는 안내를 블로그 문서에 추가합니다.
|
|
8
|
+
- e9b4447: 병원 영업 문의의 상담 예약·실제 방문·노쇼·예약 취소 상태를 외부 동기화 계약과 예제에 추가합니다.
|
|
9
|
+
- 3dedb53: tenant-api 공개 CMS GET의 Server-Timing 진단 헤더를 HTTP 문서에 안내합니다. 응답 본문·SDK 호출법은 유지합니다.
|
|
10
|
+
- ee30023: 사이트의 등록된 공개 경로 메타데이터를 한 번에 읽는 `fetchSiteRouteMetadata`를 추가합니다.
|
|
11
|
+
기존 단건 조회와 오류·기본값·정책 계약을 유지하고 메타데이터 공통 캐시 태그를 사용합니다.
|
|
12
|
+
외부 Next.js 예제에 렌더 공유 로더와 기본값 대체를 연결했습니다.
|
|
13
|
+
API를 먼저 배포한 뒤 SDK 발행과 사이트 업그레이드를 진행해야 합니다.
|
|
14
|
+
|
|
3
15
|
## 0.73.0
|
|
4
16
|
|
|
5
17
|
### 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.74.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
|
@@ -15,6 +15,14 @@ description: 공개 API raw 엔드포인트 — JS 외 스택이나 저수준
|
|
|
15
15
|
`@roottale/cms-client`를 사용하세요. 전체 CMS 관리는 아래 HTTP API,
|
|
16
16
|
MCP tool 또는 공개 CLI를 사용합니다.
|
|
17
17
|
|
|
18
|
+
tenant-api의 `GET /v1/cms/public/*` 응답에는 진단용 `Server-Timing` 헤더가 있습니다.
|
|
19
|
+
`total`, `auth`, `site`는 밀리초이며, `/metadata`와 `/metadata/routes`는 `context_read`, `context_parse`,
|
|
20
|
+
`tx`, `policy`, `projection`도 제공합니다. 같은 구간은 시간 합계와 `desc="count=N"`으로
|
|
21
|
+
표시하고, 실행하지 않은 구간은 count=0입니다. `contract`는 조회한 계약의 DB 저장 크기(압축 포함)
|
|
22
|
+
합계와 횟수입니다. 중첩 구간의 시간을 모두 더하지 마세요. Workers의 타이머 제약으로
|
|
23
|
+
CPU 구간은 0에 가까울 수 있습니다. 응답 본문·상태·캐시 규칙은 그대로이며, 이 헤더를
|
|
24
|
+
애플리케이션의 성공 판정이나 캐시 키로 사용하지 마세요.
|
|
25
|
+
|
|
18
26
|
글쓰기·미디어 자동화는 `read_write` API 키가 필요합니다. 모델·필드·노출까지
|
|
19
27
|
관리하려면 `full_management` 키를 권장합니다. `read` 키는 공개
|
|
20
28
|
콘텐츠뿐 아니라 관리 API의 초안·예약·비공개 글과 미디어 목록도 조회할 수
|
|
@@ -571,6 +579,30 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
|
|
|
571
579
|
연결된 CMS 홈이 초안이거나 소유 경로가 제거되면 `null`을 반환합니다.
|
|
572
580
|
새 연동은 `fetchRouteMetadata`와 공통 빌더를 사용하세요(`seo.md` 참고).
|
|
573
581
|
|
|
582
|
+
## GET /v1/cms/public/metadata/routes
|
|
583
|
+
|
|
584
|
+
계약에 등록된 공개 경로의 검색·공유 메타데이터를 한 스냅샷에서 조회합니다.
|
|
585
|
+
인증과 `cms:read` scope는 단건 `/metadata?route_key=`와 같습니다.
|
|
586
|
+
선택 쿼리는 `locale`(생략 시 사이트 기본 언어), `site_id`입니다.
|
|
587
|
+
사이트 범위 키는 다른 사이트를 지정할 수 없고 tenant 범위도 유지됩니다.
|
|
588
|
+
|
|
589
|
+
```json
|
|
590
|
+
{
|
|
591
|
+
"site_id": "site-id",
|
|
592
|
+
"routes": [{
|
|
593
|
+
"routeKey": "home", "locale": "ko", "path": "/", "version": "0",
|
|
594
|
+
"overrides": {}, "defaults": { "title": "홈" },
|
|
595
|
+
"effective": { "title": "홈" }, "supportedFields": ["title"]
|
|
596
|
+
}]
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
각 항목은 단건 응답의 `metadata`와 같은 형식이며 `fallback`·`robots`도 보존합니다.
|
|
601
|
+
등록 순서로 반환하고 단건이 `null`인 경로는 제외합니다. 공개 대상이 없으면 `routes: []`이며
|
|
602
|
+
조회 장애는 오류 응답입니다. 구독 하향·미납에도 공개 읽기를 유지하고
|
|
603
|
+
`Cache-Control: private, no-store`를 반환합니다. SDK의 서버 측 Next fetch cache와는 별도입니다.
|
|
604
|
+
SDK는 `fetchSiteRouteMetadata(options)`이며 배포 순서는 API → SDK 발행 → 사이트입니다.
|
|
605
|
+
|
|
574
606
|
## GET /v1/cms/public/patterns
|
|
575
607
|
|
|
576
608
|
공통 블록(여러 글의 같은 자리에 붙는 재사용 본문) 중 **발행 상태** 목록. 어느 글의
|
package/docs/blog.md
CHANGED
|
@@ -174,6 +174,13 @@ SDK 0.69.0부터 `categoryHref`로 H1 위 카테고리 링크를 별도로 지
|
|
|
174
174
|
|
|
175
175
|
위 `/insights/{slug}`는 해당 사이트에 실제로 구현한 분류 허브 주소여야 합니다.
|
|
176
176
|
|
|
177
|
+
공지·일정처럼 블로그가 아닌 컬렉션의 상세 화면에 `RootTaleBlogPost`를 쓸 때는
|
|
178
|
+
`categoryHref`를 꼭 지정하세요. 지정하지 않으면 분류 링크가 기본값
|
|
179
|
+
`/blog/categories/{slug}`로 나가고, 그 사이트에 해당 블로그 분류가 없으면 404가 됩니다.
|
|
180
|
+
분류별 허브가 없는 컬렉션은 목록 주소(예: `() => "/notice"`)로 보냅니다.
|
|
181
|
+
0.69.0 이전 SDK에서는 같은 함수를 `breadcrumb.categoryHref`로 넘깁니다. 이때 브레드크럼도
|
|
182
|
+
함께 표시됩니다.
|
|
183
|
+
|
|
177
184
|
편집자가 어드민 "관련 콘텐츠"에서 글을 골라 두면(유형 무관, 최대 10개) `RootTaleBlogPost` 는
|
|
178
185
|
`relatedPostsCount` 와 무관하게 **그 글들을 고른 순서대로** 글 하단에 그립니다(발행 글만, 저장된
|
|
179
186
|
공개 주소로 링크). 고른 것이 없을 때만 아래 자동 추천이 동작합니다.
|
package/docs/inquiries.md
CHANGED
|
@@ -386,7 +386,11 @@ CMS 읽기 키와 공개 폼 접수 키에는 이 권한을 추가하지 마세
|
|
|
386
386
|
시각입니다. 각 항목은 `external_id`, `received_at`, `name`, `phone`, `email`,
|
|
387
387
|
`message`, `status`가 필요하며, `extras`와 `attribution`은 선택입니다.
|
|
388
388
|
상태는 `new`, `contacting`, `contacted`, `follow_up`, `converted`, `closed`, `excluded`입니다.
|
|
389
|
-
|
|
389
|
+
`booked`(상담 예약), `visited`(실제 방문), `no_show`(노쇼), `canceled`(예약 취소)는
|
|
390
|
+
병원(`medical`) 사이트 영업 문의에서만 쓰며, 운영 DB에 새 상태 마이그레이션이
|
|
391
|
+
적용된 뒤 사용합니다. 병원 외 사이트가 이 상태를 보내면 전체 스냅샷을 `422`로
|
|
392
|
+
거부하며 기존 문의는 바뀌지 않습니다.
|
|
393
|
+
실제 성공 계약이 확인된 경우만 `converted`를 보냅니다. 병원에서는 시술·진료비 결제를 뜻합니다.
|
|
390
394
|
`excluded`는 담당자가 답변하지 않기로 결정한 **응대 제외**입니다. 아직 처리할
|
|
391
395
|
문의는 `new`, 고객에게 연락했지만 연결되지 않은 문의는 `contacting`으로 구분합니다.
|
|
392
396
|
응대 제외는 접수 건수에 포함하고 응대·전환 완료와 별도로 집계합니다. 운영 DB에
|
|
@@ -110,6 +110,7 @@ const menu = await fetchMenu({ apiKey, slug: "primary", tags: [MENUS_CACHE_TAG]
|
|
|
110
110
|
| `fetchBlogSettings` | `BLOG_SETTINGS_CACHE_TAG` | 설정 > 블로그 |
|
|
111
111
|
| `fetchCollections` | `COLLECTIONS_CACHE_TAG` | 설정 > 콘텐츠 유형 |
|
|
112
112
|
| `fetchSitePatterns` | `SITE_PATTERNS_CACHE_TAG` | 콘텐츠 도구 > 공통 블록 |
|
|
113
|
+
| `fetchSiteRouteMetadata` | `METADATA_CACHE_TAG` · `SITE_METADATA_DEFAULTS_CACHE_TAG`(자동 부착) | 등록 경로 전체·사이트맵 공유 조회 |
|
|
113
114
|
| `fetchRouteMetadata` | `METADATA_CACHE_TAG` · `SITE_METADATA_DEFAULTS_CACHE_TAG` · 경로별 태그(자동 부착) | 설정 > 검색·공유 표시 |
|
|
114
115
|
|
|
115
116
|
이들을 한 벌로 묶은 `SETTINGS_CACHE_TAGS` 배열도 있습니다. 수신 라우트는
|
package/docs/seo.md
CHANGED
|
@@ -59,6 +59,46 @@ export async function generateMetadata() {
|
|
|
59
59
|
|
|
60
60
|
`fetchRouteMetadata`는 공개 API를 고유 키로 한 번 조회합니다. `null`은 등록·연결된 경로가 없다는 정상 응답이고, 연결됐지만 직접 설정이 없으면 `overrides: {}`인 객체입니다. HTTP·네트워크·잘못된 응답은 예외입니다. 예외를 `null`로 바꿔 장기 캐시하지 마세요. 오류를 기록하고 코드 기본값으로 화면을 유지하는 예시는 `lib/route-metadata.ts`에 있습니다.
|
|
61
61
|
|
|
62
|
+
### 여러 페이지와 사이트맵에서 공유하는 조회
|
|
63
|
+
|
|
64
|
+
등록된 고정 경로를 여러 페이지나 사이트맵에서 읽을 때는 `fetchSiteRouteMetadata`를
|
|
65
|
+
사용합니다. 한 경로만 필요한 연동은 기존 `fetchRouteMetadata`를 계속 사용할 수 있습니다.
|
|
66
|
+
새 함수는 `{ site_id, routes }`를 반환하며, 각 항목은 단건 함수의 반환 객체와 같습니다.
|
|
67
|
+
계약 순서로 반환하고 단건이 `null`인 미발행·비공개·누락 경로는 제외합니다.
|
|
68
|
+
등록 경로는 계약당 최대 2,000개입니다. 일반 글·분류 전체를 조회하는 API는 아닙니다.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { cache } from "react";
|
|
72
|
+
import { fetchSiteRouteMetadata } from "@roottale/cms-client/server";
|
|
73
|
+
|
|
74
|
+
export const loadSiteRouteMetadata = cache((locale = "ko") => fetchSiteRouteMetadata({
|
|
75
|
+
apiKey: process.env.ROOTTALE_API_KEY!,
|
|
76
|
+
baseUrl: process.env.ROOTTALE_API_BASE,
|
|
77
|
+
locale,
|
|
78
|
+
revalidate: 300,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// generateMetadata와 sitemap이 같은 로더를 사용합니다.
|
|
82
|
+
const { routes } = await loadSiteRouteMetadata();
|
|
83
|
+
const about = routes.find((route) => route.routeKey === "about");
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
React `cache`는 렌더 안에서 조회를 공유하고, SDK의 Next fetch cache는 렌더 사이에서
|
|
87
|
+
공유합니다. `locale`과 `siteId`가 다른 요청은 분리됩니다. `signal`을 전달할 수 있고
|
|
88
|
+
`revalidate: 0`이면 재검증 주기를 0으로 지정합니다. 기본 주기는 300초이며
|
|
89
|
+
`METADATA_CACHE_TAG`·`SITE_METADATA_DEFAULTS_CACHE_TAG`가 자동 부착됩니다.
|
|
90
|
+
기존 웹훅의 메타데이터 태그 무효화가 이 조회에도 적용됩니다.
|
|
91
|
+
`createRevalidateRoute`에 `contentPolicies: 등록된정책 ?? {}`와 즉시 만료하는
|
|
92
|
+
`revalidateTag` 콜백을 전달해야 글·미디어·`metadata.updated`가 이 태그를 지웁니다.
|
|
93
|
+
정책이 없는 사이트도 빈 객체를 전달합니다(starter와 Next.js 예제 참고).
|
|
94
|
+
|
|
95
|
+
배포 순서는 **API → SDK 발행 → 사이트 SDK 업그레이드·배포**입니다.
|
|
96
|
+
새 API보다 SDK 호출이 먼저 적용되면 404 예외가 발생합니다. HTTP·네트워크·응답 형식
|
|
97
|
+
오류를 빈 목록으로 저장하지 마세요. 호출한 화면에서 오류를 기록하고 코드 기본값을
|
|
98
|
+
사용합니다. starter도 이 경로로 처리하며, 성공 응답이어도 공개 경로가 사이트 코드와
|
|
99
|
+
다르면 원격 값을 적용하지 않습니다. 실행 예시는 `examples/nextjs/lib/route-metadata.ts`의
|
|
100
|
+
공유 로더와 경로별 선택을 참고하세요.
|
|
101
|
+
|
|
62
102
|
해석 순서는 필드별 **직접 설정 → 콘텐츠/코드 기본값 → 사이트 기본값 → 코드 fallback**입니다. `false`는 명시값이며 강제 미리보기·비공개 제한이 우선합니다. `buildPageMetadata`는 최종 제목을 `title.absolute`로 내보내고 OG·X에 같은 문구와 이미지 대체 텍스트를 적용합니다. 기존 Googlebot 제한은 `googleBot` 옵션으로 전달합니다. canonical은 사이트 코드가 `canonical` 또는 `siteUrl`+`path`로 정하며 `canonical: null`이면 생략합니다. 기존 `buildPostMetadata`의 `seo.canonical`은 기본 유지하고, 코드 소유 사이트는 `allowCanonicalOverride: false`로 고정합니다.
|
|
63
103
|
|
|
64
104
|
글은 공개 응답의 `metaJson.seo`, 분류는 `seoTitle/seoDescription/seoOgImage/seoOgImageAlt/seoNofollow`를 같은 빌더의 `overrides`에 연결합니다. 분류 색인 여부는 기존 `hubPromoted` 정책을 유지합니다. 글마다 경로 메타데이터 API를 추가 호출하지 않습니다. 본문이 있는 CMS 페이지는 `lib/page-metadata.ts` 예시처럼 기존 글 원본을 사용합니다.
|
|
@@ -36,8 +36,8 @@ export const POST = createRevalidateRoute({
|
|
|
36
36
|
// route handler 에서 throw 하므로 쓰지 않는다.
|
|
37
37
|
revalidateTag: (tag: string) => revalidateTag(tag, { expire: 0 }),
|
|
38
38
|
// 정책을 쓰는 사이트는 배포 계약 metadata.posts.contentPolicies를 연결합니다.
|
|
39
|
-
//
|
|
40
|
-
contentPolicies: registeredContentPolicies,
|
|
39
|
+
// 정책이 없어도 빈 객체를 전달해 사이트 메타데이터 공통 태그를 갱신합니다.
|
|
40
|
+
contentPolicies: registeredContentPolicies ?? {},
|
|
41
41
|
// 실제화면 편집을 연결하고 모든 콘텐츠 fetch에 routes의
|
|
42
42
|
// CONTENT_EDITING_CACHE_TAG를 붙인 뒤 true로 켭니다.
|
|
43
43
|
// 새 글·공통 문구·분류 변경 시 관련 목록/CTA를 포함한 root layout도 갱신합니다.
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import {
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
import { fetchSiteRouteMetadata, type MetadataValues, type PublicRouteMetadata } from "@roottale/cms-client/server";
|
|
3
4
|
import { buildPageMetadata } from "@roottale/cms-renderer-next/routes";
|
|
4
5
|
|
|
6
|
+
/** Pages and sitemap can reuse this locale snapshot; Next fetch caching survives render boundaries. */
|
|
7
|
+
export const loadSiteRouteMetadata = cache((locale = "ko") => fetchSiteRouteMetadata({
|
|
8
|
+
apiKey: process.env.ROOTTALE_API_KEY!, baseUrl: process.env.ROOTTALE_API_BASE,
|
|
9
|
+
locale, revalidate: 300,
|
|
10
|
+
}));
|
|
11
|
+
|
|
5
12
|
/** fallback은 계약 manifest와 같은 사이트 정의에서 가져온다. API key는 서버 전용이다. */
|
|
6
13
|
export async function routeMetadata(
|
|
7
14
|
routeKey: string,
|
|
@@ -9,10 +16,12 @@ export async function routeMetadata(
|
|
|
9
16
|
): Promise<Metadata> {
|
|
10
17
|
let route: PublicRouteMetadata | null = null;
|
|
11
18
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
const snapshot = await loadSiteRouteMetadata();
|
|
20
|
+
route = snapshot.routes.find((entry) => entry.routeKey === routeKey) ?? null;
|
|
21
|
+
if (route && route.path !== new URL(options.canonical).pathname) {
|
|
22
|
+
console.error("메타데이터 공개 경로 불일치", { routeKey });
|
|
23
|
+
route = null;
|
|
24
|
+
}
|
|
16
25
|
} catch (error) {
|
|
17
26
|
// 오류를 "설정 없음"으로 저장하는 별도 캐시를 만들지 않는다.
|
|
18
27
|
console.error("메타데이터 조회 실패", { routeKey, error: error instanceof Error ? error.name : "unknown" });
|
|
@@ -7,7 +7,7 @@ export async function syncExternalInquiries(snapshot: {
|
|
|
7
7
|
items: Array<{
|
|
8
8
|
external_id: string; received_at: string; name: string; phone: string;
|
|
9
9
|
email: string; message: string;
|
|
10
|
-
status: "new" | "contacting" | "contacted" | "follow_up" | "converted" | "closed" | "excluded";
|
|
10
|
+
status: "new" | "contacting" | "contacted" | "follow_up" | "converted" | "closed" | "excluded" | "booked" | "visited" | "no_show" | "canceled";
|
|
11
11
|
extras?: Record<string, string>;
|
|
12
12
|
}>;
|
|
13
13
|
}) {
|
package/package.json
CHANGED