@roottale/cms-mcp 0.59.0 → 0.61.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,32 @@
1
1
  # @roottale/cms-mcp
2
2
 
3
+ ## 0.61.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 공통 에디터의 글씨 크기·서식 초기화·줄간격·문단 간격·이미지 너비·정렬·사진 설명을
8
+ 저장하고 공개 글에 출력합니다. ROOT-ADMIN에서 선택한 내용을 사이트 공통 블록으로
9
+ 저장하고 독립 복사본으로 재사용할 수 있습니다.
10
+
11
+ ## 0.60.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 6074c63: 아직 없는 콘텐츠 예약 — 관련 콘텐츠와 한 세트인 플랫폼 공통 필드. 글이 미래 글을 공개 주소
16
+ 키(`유형.분류.슬러그`)로 예약해 두면 대상이 발행되는 순간 공개 `related_posts` 에 자동 합류하고,
17
+ 발행 웹훅 `paths` 에 예약한 글의 경로가 함께 실려 참조 페이지가 즉시 갱신된다. 관리
18
+ `PATCH /v1/cms/posts/{post_id}/related` 에 `reserved_keys`(선택) 추가.
19
+
20
+ ### Patch Changes
21
+
22
+ - a5e53a6: ROOT-ADMIN의 UTC 발행 시각을 사이트 운영 시간대로 표시하는 방법과 UTC 자정 경계 확인값을 블로그 연동 문서에 추가합니다.
23
+ - 553623c: Next.js 16 연동 문서와 예시를 `middleware.ts`에서 `proxy.ts` 규약으로 갱신합니다.
24
+ - 819f01c: 작성자 사진의 초점 좌표를 작성자 콘텐츠에 두고 공개 글 렌더러가 이를 적용합니다.
25
+ 공통 글 하단 블록이 없는 기존 사이트에서는 레거시 CTA를 계속 렌더해 화면을 보존하며,
26
+ 사이트맵·이미지 모양 같은 사이트 표현은 프로젝트의 시멘틱 토큰과 코드가 소유합니다.
27
+ - 0c460bd: 계층형 콘텐츠 모델의 발행 웹훅이 모델 설정의 실제 허브·상세 주소를 보내도록 고치고,
28
+ 수신 사이트가 처리하지 못한 글 갱신을 오류로 드러내는 운영 계약을 문서화합니다.
29
+
3
30
  ## 0.59.0
4
31
 
5
32
  ### Minor Changes
package/dist/index.js CHANGED
@@ -1559,7 +1559,7 @@ function registerTools(server) {
1559
1559
  }
1560
1560
 
1561
1561
  // src/server.ts
1562
- var VERSION = true ? "0.59.0" : "dev";
1562
+ var VERSION = true ? "0.61.0" : "dev";
1563
1563
  var SERVER_INSTRUCTIONS = `
1564
1564
  roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
1565
1565
  \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.
@@ -95,9 +95,14 @@ curl https://api.roottale.com/v1/cms/posts \
95
95
  다른 사이트 글은 400. 발행 글이면 `post.updated` 웹훅이 나갑니다.
96
96
 
97
97
  ```json
98
- {"related_post_ids":["post-id-1","post-id-2"]}
98
+ {"related_post_ids":["post-id-1","post-id-2"],
99
+ "reserved_keys":["faq.headache.migraine.aura-symptoms"]}
99
100
  ```
100
101
 
102
+ `reserved_keys` = **아직 없는 콘텐츠 예약**(선택). 공개 주소 조각을 `.` 로 이은 키를 적어 두면
103
+ 대상이 그 주소로 발행되는 순간 공개 `related_posts` 뒤쪽에 자동 합류합니다. 미전달이면 예약을
104
+ 손대지 않고, 빈 배열이면 전부 해제합니다(최대 10개).
105
+
101
106
  응답은 `related_posts`(발행 여부 무관, 순서대로, `status` 포함)입니다. 공개 응답에는 그중
102
107
  **발행 글만** `related_posts` 로 나갑니다(아래 글 목록·상세 참고).
103
108
 
@@ -296,6 +301,7 @@ tenant/site 경로, 크기, 형식을 검증한 뒤 미디어를 등록합니다
296
301
  "collection_key": "blog",
297
302
  "author_profile_id": "site-author-id",
298
303
  "author_name": "공통 작성자", "author_slug": "writer",
304
+ "author_image_position_x": 50, "author_image_position_y": 50,
299
305
  "terms": [{ "taxonomy": "category", "name": "…", "slug": "…" }],
300
306
  "pattern_slots": { "post_footer": "clinic-guide" },
301
307
  "published_at": "…" } ],
@@ -313,7 +319,8 @@ tenant/site 경로, 크기, 형식을 검증한 뒤 미디어를 등록합니다
313
319
  선언된 자리(현재 `post_footer` = 글 하단)는 항상 키로 존재합니다. 글 상세·미리보기
314
320
  응답에도 같은 필드가 붙습니다 → 아래 `GET /v1/cms/public/patterns` 참고.
315
321
 
316
- `related_posts` = 편집자가 어드민 "관련 콘텐츠"에서 고른 글(유형 무관), 고른 순서, **발행 글만**.
322
+ `related_posts` = 편집자가 어드민 "관련 콘텐츠"에서 고른 글(유형 무관, 고른 순서) + **발행된
323
+ 예약 키 대상**(그 뒤, 적은 순서), **발행 글만**·중복 제거.
317
324
  각 항목은 `{ id, title, slug, path, type, model_key, collection_key, excerpt, featured_media_url,
318
325
  published_at }` 로 카드 하나를 그릴 만큼만 담습니다. 목록·상세·미리보기 응답에 모두 붙습니다
319
326
  (구 서버는 미포함 → 빈 배열로 취급). 비어 있으면 사이트가 "같은 카테고리 최신 글" 같은
@@ -328,8 +335,9 @@ published_at }` 로 카드 하나를 그릴 만큼만 담습니다. 목록·상
328
335
 
329
336
  `author_profile_id`는 사이트 공통 공개 작성자 ID입니다. 이름·사진·소개·작가 주소는
330
337
  같은 원장의 `author_name`·`author_image_url`·`author_bio`·`author_slug`로
331
- 제공됩니다. 기존 `author_id`는 하위 호환용이므로 새 연동에서는 공개 작성자
332
- 식별자로 사용하지 마세요.
338
+ 제공됩니다. 사진 초점은 `author_image_position_x/y`(0~100, 구 서버는 `null`)이며
339
+ 이미지 모양은 사이트의 시멘틱 토큰/CSS가 정합니다. 기존 `author_id`는 하위
340
+ 호환용이므로 새 연동에서는 공개 작성자 식별자로 사용하지 마세요.
333
341
 
334
342
  ## GET /v1/cms/public/posts/{identifier}
335
343
 
@@ -408,7 +416,7 @@ fallback 네비를 렌더하세요 (`menus.md` 참고).
408
416
  ## GET /v1/cms/public/redirects
409
417
 
410
418
  어드민 "설정 > 주소 이동"에서 정의한 **활성** 커스텀 리다이렉트 규칙 전체.
411
- 사이트 미들웨어가 요청 경로를 매칭해 301/302 처리합니다. 비활성 규칙과 운영자
419
+ 사이트 Proxy가 요청 경로를 매칭해 301/302 처리합니다. 비활성 규칙과 운영자
412
420
  메모는 응답에 포함되지 않습니다. 라우트 미배포(구 서버)는 `404` — 빈 목록으로
413
421
  처리하세요. 연동은 `custom-redirects.md` 참고.
414
422
 
@@ -435,10 +443,11 @@ fallback 네비를 렌더하세요 (`menus.md` 참고).
435
443
 
436
444
  ## GET /v1/cms/public/blog-settings
437
445
 
438
- 블로그 표시 설정 (TOC·작성자·발행일·작성자 카드 표시 정책, 글 하단 CTA).
439
- `post_cta` 는 admin 에서 활성화하고 버튼 문구·링크를 채웠을 때만 객체이며,
440
- 그 외에는 `null`. `RootTaleBlogPost` 가 본문 끝에 자동으로 렌더하므로 별도
441
- 연동 코드는 필요 없다. `toc_position` 은 목차 배치(`"inline"`=본문 위 접이식,
446
+ 블로그 표시 설정 (TOC·작성자·발행일·작성자 카드 표시 정책).
447
+ `post_cta`는 공통 글 하단 블록이 없는 기존 글에서 `RootTaleBlogPost`가 적용하고,
448
+ 공통 블록이 있으면 중복을 막기 위해 생략한다. 작성자 이미지 모양과 과거 전역 초점
449
+ 필드는 호환용이며, 모양은 사이트 시멘틱 토큰/CSS, 초점은 글 응답의 작성자별 값이
450
+ 소유한다. `toc_position`은 목차 배치(`"inline"`=본문 위 접이식,
442
451
  `"sidebar"`=넓은 화면에서 본문 오른쪽 sticky, 좁은 화면은 자동 인라인)로,
443
452
  렌더러가 알아서 반영하므로 연동 코드 변경은 불필요하다.
444
453
 
package/docs/blog.md CHANGED
@@ -15,6 +15,26 @@ description: 블로그 목록/상세 페이지 구현 — 컴포넌트 빠른
15
15
  본문 렌더링은 두 경로 모두 `RootTaleBlogPost`(블록 JSON → React)를 쓰는 것을
16
16
  권장합니다. 본문 JSON 스키마를 직접 파싱하지 마세요.
17
17
 
18
+ ## 본문 서식과 재사용 블록
19
+
20
+ ROOT-ADMIN의 글씨 크기·서식 지우기·줄간격·문단 뒤 간격과 이미지 너비·정렬·사진
21
+ 설명은 공통 에디터에서 저장합니다. `textStyle.attrs.fontSize`, 문단·제목의
22
+ `lineHeight`·`paragraphSpacing`, 이미지의 `displayWidth`·`imageAlign`·`caption`을
23
+ `RenderTiptap`과 `RootTaleBlogPost`가 출력합니다. 별도 JSON 렌더러를 만들거나
24
+ 본문의 `style`을 일괄 제거하면 이 서식이 사라집니다. 크기를 지정하지 않은 기존 글은
25
+ 사이트 기본 CSS를 계속 따릅니다. 사진 원본 `width`·`height`는 유지합니다.
26
+
27
+ 관리자는 선택한 내용을 **재사용 블록** 메뉴에서 현재 사이트의 공통 블록으로 저장할
28
+ 수 있습니다. 저장에는 공통 블록 관리 권한이 필요하며, 삽입은 글을 편집할 수 있는
29
+ 사용자에게 제공됩니다. 삽입한 내용은 독립 복사본이므로 원본 블록을 수정해도 이미
30
+ 삽입한 글은 바뀌지 않습니다. 기존 글 하단의 공통 블록 배치 규칙과는 별개입니다.
31
+
32
+ 공개 사이트는 아래 패키지를 같은 릴리즈 버전으로 업데이트하고 다시 배포하세요.
33
+
34
+ ```bash
35
+ pnpm update @roottale/cms-client @roottale/cms-core @roottale/cms-renderer-next --latest
36
+ ```
37
+
18
38
  ## 빠른 경로 — 컴포넌트
19
39
 
20
40
  ### 목록 페이지
@@ -147,9 +167,10 @@ export default async function PostPage({
147
167
  렌더되지 않습니다.
148
168
 
149
169
  목차(ToC)·작성자 카드·발행일 표시는 어드민의 블로그 표시 설정으로도 제어됩니다
150
- (`theme-and-settings.md` 참고). 어드민 설정 > 블로그에서 **글 하단 CTA**(제목·설명·
151
- 버튼)를 켜면 `RootTaleBlogPost` 가 모든 글 본문 끝에 같은 CTA 블록을 자동으로
152
- 렌더합니다 — 별도 연동 코드는 필요 없습니다.
170
+ (`theme-and-settings.md` 참고). 여러 글에 같은 CTA가 필요하면 공통 블록의
171
+ `post_footer` 자리를 쓰세요. 공통 블록으로 아직 옮기지 않은 기존 사이트는
172
+ `RootTaleBlogPost`가 레거시 `postCta`를 계속 렌더하며, 공통 블록이 배치되면
173
+ 레거시 CTA를 생략해 고객 화면에 두 개가 겹치지 않습니다.
153
174
 
154
175
  #### 목차 블록 (본문 임의 위치)
155
176
 
@@ -201,6 +222,11 @@ export default async function PostPage({
201
222
  로 감쌉니다. slug 가 없는 작가는 지금처럼 plain text 로 렌더됩니다. 링크
202
223
  대상을 바꾸려면 `authorHref`:
203
224
 
225
+ 작성자 사진은 같은 작성자 원장의 `author_image_position_x/y`를 초점으로 사용합니다.
226
+ 관리자는 작성자 화면에서 사진과 초점을 함께 바꿀 수 있고, 원형·사각형 같은 모양은
227
+ 각 사이트의 시멘틱 토큰/CSS가 정합니다. 구 서버처럼 좌표가 없으면 가운데(50/50)를
228
+ 사용합니다.
229
+
204
230
  ```tsx
205
231
  <RootTaleBlogPost
206
232
  apiKey={apiKey}
@@ -417,7 +443,7 @@ export async function getPost(slug: string) {
417
443
  |---|---|
418
444
  | `id`, `slug`, `title` | 식별자·제목 |
419
445
  | `excerpt` | 요약 (목록 카드용) |
420
- | `publishedAt` | 발행 시각 (ISO) |
446
+ | `publishedAt` | 발행 시각 (UTC ISO). 화면에는 사이트 운영 시간대로 변환해서 표시 |
421
447
  | `bodyJson` | 본문 블록 JSON — `RootTaleBlogPost` 또는 `renderBlocks`로 렌더 |
422
448
  | `terms` | 분류 용어 배열 (`taxonomy: "category" \| "tag"`, `name`, `slug`) |
423
449
  | `featuredImageUrl` | 대표 이미지 |
@@ -426,6 +452,45 @@ export async function getPost(slug: string) {
426
452
  | `metaJson` | 부가 메타 — `metaJson.seo`에 SEO 오버라이드 |
427
453
  | `patternSlots` | 공통 블록 자리별 블록 key(`{ post_footer: "clinic-guide" }`) — `RootTaleBlogPost`가 자동 렌더, 자체 화면은 `fetchSitePatterns` + `selectSitePatternForSlot`(`theme-and-settings.md` "공통 블록") |
428
454
 
455
+ ### 발행일 시간대 — 서버 기본값에 맡기지 않기
456
+
457
+ ROOT-ADMIN은 발행 시점을 UTC ISO 문자열로 저장하고, 관리자 화면에서는 한국 운영
458
+ 시간대(`Asia/Seoul`)로 표시합니다. `publishedAt`은 날짜 문자열이 아니라 **한 시점**이므로,
459
+ 커스텀 UI에서 `new Date()` 뒤 서버 기본 시간대로 포맷하거나 `YYYY-MM-DD` 부분만 자르면
460
+ UTC 자정 부근의 글이 관리자보다 하루 전 날짜로 보일 수 있습니다. Vercel 같은 서버
461
+ 런타임은 UTC를 기본값으로 사용할 수 있으므로 실행 환경에 기대지 마세요.
462
+
463
+ 사이트의 운영 시간대를 한 곳에 정하고 `Intl.DateTimeFormat`에 항상 명시합니다. 한국
464
+ ROOT-ADMIN 사이트의 기본 구현은 다음과 같습니다.
465
+
466
+ ```tsx
467
+ const SITE_TIME_ZONE = "Asia/Seoul";
468
+
469
+ export function formatPublishedDate(publishedAt: string): string {
470
+ const date = new Date(publishedAt);
471
+ if (Number.isNaN(date.getTime())) return "";
472
+
473
+ return new Intl.DateTimeFormat("ko-KR", {
474
+ year: "numeric",
475
+ month: "long",
476
+ day: "numeric",
477
+ timeZone: SITE_TIME_ZONE,
478
+ }).format(date);
479
+ }
480
+
481
+ <time dateTime={post.publishedAt}>
482
+ {formatPublishedDate(post.publishedAt)}
483
+ </time>;
484
+ ```
485
+
486
+ - `dateTime`에는 API 원본 ISO 시각을 그대로 둡니다.
487
+ - 보이는 날짜에만 사이트 운영 시간대를 적용합니다.
488
+ - 목록 정렬·예약 판정은 보이는 날짜 문자열이 아니라 원본 시각으로 처리합니다.
489
+ - 최소 경계 확인값: `2026-08-22T21:40:50.666Z`는 서울에서
490
+ `2026년 8월 23일`이어야 합니다.
491
+ - 다른 국가 사이트는 `SITE_TIME_ZONE`만 해당 지역의 IANA 시간대 이름으로 바꾸고,
492
+ 목록·상세·OG 이미지 등 모든 날짜 표시가 같은 값을 사용하게 합니다.
493
+
429
494
  ### 정적 경로 사전 생성 + 메타데이터
430
495
 
431
496
  ```tsx
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  title: 주소 이동 (커스텀 리다이렉트) 연동
3
- description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로 리다이렉트를 사이트 미들웨어로 적용
3
+ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로 리다이렉트를 사이트 Proxy로 적용
4
4
  ---
5
5
 
6
6
  # 주소 이동 (커스텀 리다이렉트) 연동
7
7
 
8
8
  어드민(admin.roottale.com)의 **설정 > 주소 이동**에서 운영자가 정의한 임의
9
- 경로 이동 규칙(`/old-event → /promo`)을 사이트 미들웨어로 적용합니다. 코드
9
+ 경로 이동 규칙(`/old-event → /promo`)을 사이트 Proxy로 적용합니다. 코드
10
10
  수정 없이 고객이 직접 규칙을 추가·수정·삭제할 수 있습니다.
11
11
 
12
12
  두 종류의 주소 이동이 있습니다.
@@ -15,26 +15,26 @@ description: 어드민 "설정 > 주소 이동"에서 정의한 임의 경로
15
15
  새 주소로 이어집니다. 글 라우트에서 처리되며 별도 설정이 필요 없습니다
16
16
  (`blog.md` 의 `postRedirectPath` 참고).
17
17
  - **커스텀 리다이렉트(이 문서)** — 글이 아닌 임의 경로를 옮깁니다. 라우팅
18
- *이전* 단계인 **미들웨어**에서만 가로챌 수 있어, 아래 설정이 필요합니다.
18
+ *이전* 단계인 **Proxy**에서만 가로챌 수 있어, 아래 설정이 필요합니다.
19
19
 
20
20
  글의 공개 주소가 바뀌면(slug 변경·분류 이동·모델 규칙 변경) 플랫폼이 옛 경로 → 현재
21
21
  경로 301 을 **자동으로** 이 목록에 더합니다(`id` 가 `path-history:` 로 시작). 운영자가
22
- 같은 출발 경로 규칙을 만들었으면 운영자 규칙이 이깁니다. 미들웨어를 쓰고 있다면 별도
22
+ 같은 출발 경로 규칙을 만들었으면 운영자 규칙이 이깁니다. Proxy를 쓰고 있다면 별도
23
23
  작업 없이 옛 링크가 새 주소로 갑니다.
24
24
 
25
25
  규칙은 `GET /v1/cms/public/redirects` 로 내려오며 **활성** 규칙만 포함됩니다
26
26
  (`api-reference.md`). 출발 경로는 정규화된 사이트 내부 절대 경로, 도착지는
27
27
  내부 경로 또는 절대 URL, 상태는 `301`(영구) 또는 `302`(임시)입니다.
28
28
 
29
- ## 미들웨어 설정
29
+ ## Proxy 설정
30
30
 
31
31
  `@roottale/cms-renderer-next` 의 `createRedirectMiddleware` 를 프로젝트 루트
32
- `middleware.ts` 에 마운트합니다. 규칙을 자동 캐시(기본 60초)하며, API 실패 시
32
+ `proxy.ts` 에 마운트합니다. 규칙을 자동 캐시(기본 60초)하며, API 실패 시
33
33
  기존 캐시가 있으면 오래된 규칙을 우선 사용하고(stale-first), 캐시가
34
34
  없으면 트래픽을 막지 않고 통과시킵니다(fail-soft).
35
35
 
36
36
  ```ts
37
- // middleware.ts
37
+ // proxy.ts
38
38
  import { NextResponse } from "next/server";
39
39
  import { createRedirectMiddleware } from "@roottale/cms-renderer-next/routes";
40
40
 
@@ -43,7 +43,7 @@ const redirects = createRedirectMiddleware({
43
43
  // apiBase, siteId, cacheTtlMs 는 선택.
44
44
  });
45
45
 
46
- export async function middleware(req: Request) {
46
+ export async function proxy(req: Request) {
47
47
  return (await redirects(req)) ?? NextResponse.next();
48
48
  }
49
49
 
@@ -66,14 +66,14 @@ export const config = {
66
66
  왕복을 막기 위해 해당 요청을 통과시킵니다.
67
67
  - 매칭이 없으면 `null` 을 반환하므로 `NextResponse.next()` 로 통과시키세요.
68
68
 
69
- Site Materializer로 새 사이트를 만들면 `middleware.ts`와
70
- `tests/redirect-middleware.test.ts`가 필수 산출물로 포함됩니다. 두 파일은
69
+ Site Materializer로 새 사이트를 만들면 `proxy.ts`와
70
+ `tests/redirect-proxy.test.ts`가 필수 산출물로 포함됩니다. 두 파일은
71
71
  내부 Materializer 영수증에도 기록되므로, 설치 여부를 추측하지 않고
72
72
  실제 납품 산출물로 확인할 수 있습니다.
73
73
 
74
74
  ## 캐시와 즉시성
75
75
 
76
- 규칙은 미들웨어가 TTL(기본 60초) 동안 캐시합니다. 운영자가 규칙을 바꾸면
76
+ 규칙은 Proxy가 TTL(기본 60초) 동안 캐시합니다. 운영자가 규칙을 바꾸면
77
77
  최대 TTL 만큼 뒤 반영됩니다. 더 빠른 반영이 필요하면 `cacheTtlMs` 를 줄이세요
78
78
  (요청당 API 호출이 늘어납니다).
79
79
 
@@ -84,7 +84,7 @@ const redirects = createRedirectMiddleware({
84
84
  });
85
85
  ```
86
86
 
87
- ## 직접 호출 (미들웨어 없이)
87
+ ## 직접 호출 (Proxy 없이)
88
88
 
89
89
  규칙 목록만 필요하면 `fetchRedirects` 로 직접 가져올 수 있습니다.
90
90
 
@@ -212,6 +212,9 @@ curl -sI -X POST https://<사이트 도메인>/api/revalidate
212
212
  - **주소가 바뀐 글**(slug 변경·카테고리 이동 등으로 상세 주소가 옮겨진 글)은 **옛 상세
213
213
  주소도 함께** 옵니다 — 옛 주소의 캐시가 비워져 바로 새 주소로 이동하고, 옛 주소를
214
214
  참조하던 페이지도 같이 갱신할 수 있게 하기 위해서입니다
215
+ - `category_tree` 표시 모델은 설정한 `basePath`, 선택한 분류까지의 모든 허브,
216
+ 분류를 포함한 상세 주소가 옵니다. 모델 preset이 `article`이어도 `/blog`로
217
+ 바꾸지 않습니다
215
218
 
216
219
  주소가 안 나오는 경우도 있습니다.
217
220
 
@@ -225,6 +228,13 @@ curl -sI -X POST https://<사이트 도메인>/api/revalidate
225
228
  수신 측은 모든 서명 이벤트에서 블로그 목록과 피드·사이트맵을 함께 갱신하고,
226
229
  `collections`를 넘긴 사이트는 각 유형의 목록 주소도 자동으로 갱신합니다.
227
230
 
231
+ 직접 만든 수신 라우트가 `modelKey`별 주소를 엄격히 나누는 경우에는 글 이벤트의
232
+ `paths`가 지원하는 주소를 하나도 포함하지 않거나 `modelKey`의 주소 계열과 다르면
233
+ `422` 같은 non-2xx로 답하세요. 아무 캐시도 지우지 않았는데 `200`을 반환하면
234
+ RootTale은 성공으로 기록하므로 경로 계산 오류가 조용히 숨습니다. `modelKey`를
235
+ 경로 대체값으로 써서 성공시키지 말고, 발신 경로와 설정의 불일치를 드러내는 검증값으로
236
+ 사용해야 합니다.
237
+
228
238
  ## 저수준 검증 — verifyRootTaleWebhook
229
239
 
230
240
  `createRevalidateRoute`를 못 쓰는 환경(다른 프레임워크 등)은
package/docs/seo.md CHANGED
@@ -53,6 +53,11 @@ const { generateSitemaps, sitemap } = createSitemapIndex(
53
53
  { url: `${SITE_URL}/blog` },
54
54
  { url: `${SITE_URL}/contact` },
55
55
  ],
56
+ {
57
+ // 사이트에 실제로 존재하는 라우트와 콘텐츠에 맞춰 코드에서 결정합니다.
58
+ blogImages: true,
59
+ authors: false,
60
+ },
56
61
  );
57
62
 
58
63
  // Next 16: 인덱스를 만들려면 generateSitemaps·default 를 둘 다 export.
@@ -60,8 +65,8 @@ export { generateSitemaps };
60
65
  export default sitemap;
61
66
  ```
62
67
 
63
- - **블로그 이미지 색인** — 글에 대표 이미지가 있으면 `<image:image>`로 함께 색인합니다.
64
- 어드민 **설정 > 블로그 > 사이트맵**에서 켜고 끌 수 있어요(기본 켜짐).
68
+ - **블로그 이미지 색인** — 글에 대표 이미지가 있으면 `<image:image>`로 함께 색인할지
69
+ 사이트 코드의 `blogImages`로 정합니다.
65
70
  - **분류·작가 모음 페이지는 기본으로 사이트맵에서 빠집니다** — 어드민에서 "검색에
66
71
  노출"을 켠 것만 들어갑니다. 아래 "색인 위생" 절을 보세요.
67
72
  - `changeFrequency`/`priority`는 Google이 무시하므로 더 이상 내보내지 않습니다(`loc` +
@@ -71,9 +76,9 @@ export default sitemap;
71
76
 
72
77
  ### 작가 아카이브 (`/blog/author/{slug}`)
73
78
 
74
- 어드민 **설정 > 팀**에서 작가에게 주소(slug)를 발급하면, 그 작가의 글 모음 페이지와
75
- 작가 사이트맵(`/sitemap/authors.xml`)을 만들 수 있습니다. 어드민 **설정 > 블로그 >
76
- 사이트맵**에서 "작가 사이트맵"을 켜면 인덱스에 `authors` 섹션이 추가됩니다.
79
+ 어드민 **설정 > 팀**에서 작가에게 주소(slug)를 발급하고 사이트 프로젝트에 실제 작가
80
+ 모음 라우트를 만든 뒤, `createSitemapIndex` 옵션의 `authors: true`를 지정하면 작가
81
+ 사이트맵(`/sitemap/authors.xml`)이 추가됩니다. 라우트가 없으면 반드시 `false`로 둡니다.
77
82
 
78
83
  ```tsx
79
84
  // app/blog/author/[slug]/page.tsx
@@ -153,8 +158,8 @@ const jsonLd = collectionPageSchema({
153
158
 
154
159
  - 켜는 곳: 어드민 **내 사이트 > 카테고리**에서 분류를 펼치고 "검색에 이 분류 페이지
155
160
  노출"을 체크합니다. 켠 분류만 사이트맵에 들어가고 `robots`도 색인 허용이 됩니다.
156
- - 작가 모음은 **설정 > 블로그 > 사이트맵**의 "작가별 글 모음도 검색엔진에 알리기"가
157
- 같은 역할을 합니다.
161
+ - 작가 모음은 프로젝트에 실제 라우트가 있을 때만 사이트 코드에서 `authors: true`로
162
+ 같은 결정을 내립니다.
158
163
  - 글 자체는 영향을 받지 않습니다 — 모음 페이지만 빠집니다.
159
164
 
160
165
  판정은 `decideArchiveIndex` 하나에서 나옵니다. 사이트맵 필터와 페이지의 `robots`가
@@ -797,9 +802,8 @@ export default async function RootLayout({ children }) {
797
802
  - `@type`: `[업종, "LocalBusiness"]` (중복 제거) — 어드민에서 고른 업종
798
803
  (세무·회계 = `AccountingService`, 병원·의원 = `MedicalClinic` 등)
799
804
  - `address`(PostalAddress) / `geo`(GeoCoordinates) /
800
- `openingHoursSpecification` / `priceRange` / `areaServed`
805
+ `openingHoursSpecification` / `areaServed`
801
806
  - `alternateName`: 띄어쓰기 변형처럼 같은 사업장을 다르게 부르는 이름
802
- - `faxNumber`: 팩스를 쓰는 업종(세무·법률 등)의 연락처
803
807
  - `hasOfferCatalog`: 어드민 "취급 업무" 목록 — "무엇을 하는 곳인가"를 명시하는
804
808
  신호입니다. 명함·간판에 적힌 업무를 그대로 옮기면 됩니다.
805
809
  - `hasMap`: 네이버플레이스 주소(없으면 카카오맵 주소)
@@ -807,6 +811,11 @@ export default async function RootLayout({ children }) {
807
811
  등의 URL — 검색엔진이 동일 사업장임을 연결합니다. 지도 딥링크(카카오맵)는
808
812
  프로필 페이지가 아니므로 `hasMap` 으로만 나가고 여기엔 포함되지 않습니다.
809
813
 
814
+ `faxNumber`는 고객이 사업장 정보에서 바꿀 수 있으며, 스타터와
815
+ `localBusinessSchema`가 자동으로 JSON-LD에 반영합니다. `priceRange`는 업종별 의미가
816
+ 달라 공통 관리자·스타터가 관리하지 않습니다. 필요하면 사이트 프로젝트가
817
+ `localBusinessSchema` 입력에 명시적으로 추가합니다.
818
+
810
819
  네이버플레이스([new.smartplace.naver.com](https://new.smartplace.naver.com))와
811
820
  구글 비즈니스 프로필([business.google.com](https://business.google.com)) 등록
812
821
  자체는 공개 API가 없어 사장님이 직접 해야 하며, 어드민 화면에 등록 안내와
@@ -126,7 +126,7 @@ const settings = await fetchBlogSettings({
126
126
  tags: [BLOG_SETTINGS_CACHE_TAG],
127
127
  });
128
128
  // showTableOfContents, showAuthor, showDate, showAuthorCard,
129
- // tocTitle, authorProfileImageRadius / Position 등
129
+ // tocTitle, tocPosition, authorCardEyebrow 등
130
130
  // siteProfile: siteDescription · homeTitle · homeDescription(메인 홈 전용
131
131
  // 검색 제목·설명, null 이면 사이트 이름·사이트 설명으로 폴백) · logoUrl ·
132
132
  // faviconUrl · defaultOgImageUrl — 사이트 <head>/OG 폴백 (seo.md 참고)
@@ -135,8 +135,11 @@ const settings = await fetchBlogSettings({
135
135
  const display = resolvePostDisplay(settings, post);
136
136
  ```
137
137
 
138
- `RootTaleBlogPost` 컴포넌트를 쓰면 이 설정이 자동 반영됩니다 — 커스텀 UI를
139
- 만들 때만 직접 조회하면 됩니다. 호환 필드인 `authorProfileName`·
138
+ `RootTaleBlogPost` 컴포넌트를 쓰면 위 글 표시 설정이 자동 반영됩니다. 작성자
139
+ 사진의 초점은 글에 연결된 작성자 콘텐츠 값을 적용하고, 사진 모양은 사이트의
140
+ 시멘틱 토큰/CSS를 따릅니다. 레거시 `postCta`는 아래 공통 블록이 없는 기존 글에서만
141
+ 자동 반영되며, 공통 블록이 배치되면 함께 표시되지 않습니다.
142
+ 호환 필드인 `authorProfileName`·
140
143
  `authorProfileBio`·`authorProfileImageUrl`·`authorCardDescription`은 항상
141
144
  `null`이며 새 코드에서 사용하지 마세요.
142
145
 
@@ -192,9 +195,13 @@ CSS 에 색·모양을 고정하지 말고 `sitePatternPresentationAttributes(pa
192
195
  ## 비즈니스 프로필 (로컬 SEO) — fetchBusinessProfile
193
196
 
194
197
  어드민 **운영 > 비즈니스 프로필**에서 저장한 사업장 정보(이름·별칭·업종·
195
- 연락처·팩스·주소·좌표·영업시간·취급 업무·네이버플레이스/구글/카카오 URL)를
198
+ 연락처·주소·좌표·영업시간·취급 업무·네이버플레이스/구글/카카오 URL)를
196
199
  조회합니다.
197
200
 
201
+ `fax_number`와 `price_range`는 구 소비자 호환을 위해 공개 응답에 남아 있지만 공통
202
+ 관리자와 스타터는 편집·자동 소비하지 않습니다. 필요한 사이트가 프로젝트 코드로
203
+ 소유합니다.
204
+
198
205
  ```ts
199
206
  import {
200
207
  BUSINESS_CACHE_TAG,
@@ -207,8 +214,7 @@ const business = await fetchBusinessProfile({
207
214
  tags: [BUSINESS_CACHE_TAG],
208
215
  });
209
216
  // 미설정이면 null. 설정돼 있으면 name, alternateName, businessType, telephone,
210
- // faxNumber, address, geo, openingHours, priceRange, areaServed, services,
211
- // profiles 포함.
217
+ // address, geo, openingHours, areaServed, services, profiles 포함.
212
218
 
213
219
  if (business) {
214
220
  const jsonLd = localBusinessSchema(business, {
@@ -223,7 +229,6 @@ if (business) {
223
229
  | 프로필 값 | JSON-LD |
224
230
  |---|---|
225
231
  | `alternateName` | `alternateName` — 띄어쓰기 변형 등 같은 사업장의 다른 표기 |
226
- | `faxNumber` | `faxNumber` |
227
232
  | `services` | `hasOfferCatalog` (각 항목이 `Offer` → `Service`) |
228
233
  | `profiles.naverPlace` \| `profiles.kakaoMap` | `hasMap` (네이버 플레이스 우선) |
229
234
  | 그 밖의 `profiles` 값 | `sameAs` — 지도 딥링크(`kakaoMap`)는 제외 |
@@ -232,6 +237,10 @@ if (business) {
232
237
  > 반환합니다. 전화·주소만 채우고 이름을 비워두면 **나머지 입력이 전부 무시**되니
233
238
  > 고객 안내 시 이름을 필수로 안내하세요.
234
239
 
240
+ `faxNumber`는 고객이 사업장 정보에서 바꾸며 스타터와 `localBusinessSchema`가
241
+ 자동 반영합니다. `priceRange`는 업종·사이트마다 의미와 표현이 달라 관리자에서
242
+ 관리하지 않습니다. 필요하면 사이트 프로젝트의 구조화 데이터 코드에 둡니다.
243
+
235
244
  ## ROOT-ANALYTICS 설정 — fetchAnalyticsConfig
236
245
 
237
246
  ROOT-ADMIN에서 등록한 외부 태그(GA4, Microsoft Clarity, Meta Pixel, 네이버)와
@@ -1,4 +1,5 @@
1
1
  // 블로그 상세 페이지 — 본문은 RootTaleBlogPost(블록 JSON 렌더러)에 위임,
2
+ // 글씨 크기·문단 간격·이미지 설명도 공통 렌더러가 처리한다. bodyJson의 서식을 제거하지 않는다.
2
3
  // 메타데이터는 어드민 SEO 패널(metaJson.seo) 값을 우선 적용.
3
4
  // slug 변경 시: API가 옛 slug로도 글을 찾아 현재 slug로 응답 → 301 redirect.
4
5
  import type { Metadata } from "next";
@@ -1,7 +1,7 @@
1
- // middleware.ts — 커스텀 리다이렉트(어드민 "설정 > 주소 이동") 적용.
1
+ // proxy.ts — 커스텀 리다이렉트(어드민 "설정 > 주소 이동") 적용.
2
2
  //
3
3
  // 글 슬러그 변경 자동 301 은 글 라우트에서 처리되지만(blog 예시 참고), 글이
4
- // 아닌 임의 경로(`/old-event → /promo`)는 라우팅 이전 단계인 미들웨어에서만
4
+ // 아닌 임의 경로(`/old-event → /promo`)는 라우팅 이전 단계인 Proxy에서만
5
5
  // 가로챌 수 있다. 규칙은 자동 캐시되고, API 실패 시 stale 캐시를
6
6
  // 우선 사용한다. 캐시가 없거나 순환 규칙이면 트래픽을 막지 않는다.
7
7
  import { NextResponse } from "next/server";
@@ -11,7 +11,7 @@ const redirects = createRedirectMiddleware({
11
11
  apiKey: process.env.ROOTTALE_API_KEY!,
12
12
  });
13
13
 
14
- export async function middleware(req: Request) {
14
+ export async function proxy(req: Request) {
15
15
  return (await redirects(req)) ?? NextResponse.next();
16
16
  }
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.59.0",
3
+ "version": "0.61.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": {