@roottale/cms-mcp 0.72.1 → 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 +38 -0
- package/dist/index.js +1 -1
- package/docs/api-reference.md +47 -0
- package/docs/blog.md +7 -0
- package/docs/content-models-and-exposures.md +13 -0
- package/docs/inquiries.md +12 -2
- package/docs/revalidation-webhooks.md +1 -0
- package/docs/seo.md +86 -2
- package/docs/theme-and-settings.md +92 -10
- package/docs/visual-content-editing.md +45 -0
- package/examples/nextjs/app/.well-known/roottale.json/route.ts +16 -1
- package/examples/nextjs/app/api/revalidate/route.ts +2 -2
- package/examples/nextjs/app/error.tsx +21 -0
- package/examples/nextjs/app/layout.tsx +18 -0
- package/examples/nextjs/app/page.tsx +11 -1
- package/examples/nextjs/components/shared-field-output.tsx +9 -0
- package/examples/nextjs/components/site-inquiry-form.tsx +21 -1
- package/examples/nextjs/external-source-sync.md +53 -0
- package/examples/nextjs/hospital-fees/README.md +72 -0
- package/examples/nextjs/hospital-fees/content-area.json +43 -0
- package/examples/nextjs/hospital-fees/files/app/guide/non-covered/page.tsx +16 -0
- package/examples/nextjs/hospital-fees/files/app/preview/post/[id]/page.tsx +34 -0
- package/examples/nextjs/hospital-fees/files/cms/non-covered-fees-page.json +12 -0
- package/examples/nextjs/hospital-fees/files/components/site/SiteAnalytics.tsx +16 -0
- package/examples/nextjs/hospital-fees/files/features/site/non-covered/NonCoveredFees.tsx +32 -0
- package/examples/nextjs/hospital-fees/files/features/site/non-covered/nonCoveredFees.module.css +11 -0
- package/examples/nextjs/hospital-fees/files/lib/root-admin/content-table.ts +51 -0
- package/examples/nextjs/hospital-fees/files/lib/root-admin/miridoc-client.ts +10 -0
- package/examples/nextjs/hospital-fees/files/lib/root-admin/miridoc-fees.ts +89 -0
- package/examples/nextjs/hospital-fees/files/lib/root-admin/non-covered-fees.ts +37 -0
- package/examples/nextjs/hospital-fees/files/tests/non-covered.test.tsx +69 -0
- package/examples/nextjs/hospital-fees/files.json +13 -0
- package/examples/nextjs/lib/page-metadata.ts +4 -0
- 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,43 @@
|
|
|
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
|
+
|
|
15
|
+
## 0.73.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 5806994: 공개 페이지의 색인을 사이트가 관리하는 `robots.forceIndex` 정책을 추가합니다. 개별 noindex와 분류 승격보다 우선하되 미리보기·미발행·콘텐츠 조건의 `forceNoindex`를 해제하지 않습니다. 관리자와 공개 head·사이트맵이 같은 정책을 사용할 수 있습니다.
|
|
20
|
+
- 2c91d43: 병원 공통 비급여를 미리닥 API로 자동 연동하고 ROOT-ADMIN에서 병원 수정·추가·숨김을 관리한다. 기존 셀별 문서와 이력을 보존하며 의료기관 ID를 연결하는 병원 스타터·공개 페이지·인증된 미리보기를 제공한다.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 49a3bfe: 고객 사이트의 well-known 응답에 실제 Next.js·Node.js·React 버전을 연결하는 예제와 ROOT-ADMIN 버전·보안패치 점검 계약을 추가합니다.
|
|
25
|
+
- dd539dc: 외부 문의 동기화에 담당자가 답변하지 않기로 결정한 `excluded`(응대 제외) 상태를 추가하고 연동 문서와 Next.js 예제를 갱신합니다.
|
|
26
|
+
- dd539dc: 공통 비콘에서 유입 출처와 페이지 이동 경로를 CTA 클릭에 동봉합니다. CTA 이름·위치와 기존 CTA 이벤트를 CRM 방문 여정에 포함하고 첫 페이지 기록 순서를 수정합니다. Next.js 설치·CTA 표식·문의 동봉 예제를 함께 제공합니다.
|
|
27
|
+
- dd539dc: CTA 노출·첫 클릭과 문의 오류를 공통 수집합니다. Next 어댑터가 기존 버튼 표식의 노출을
|
|
28
|
+
감지하며 `trackFormError()`로 사용자 정의 검증·접수 실패를 기록할 수 있습니다.
|
|
29
|
+
기본 문의 폼의 HTML 검증 오류와 설치·접수 예제도 같은 계약을 사용합니다.
|
|
30
|
+
|
|
31
|
+
비콘 설치 직후 페이지 실행·처리되지 않은 Promise·리소스 오류를 수집합니다.
|
|
32
|
+
처리된 요청 실패는 `trackSiteError()`로 연결하며 원문·스택·요청 URL은 보내지 않습니다.
|
|
33
|
+
Next.js 오류 화면 예제를 추가했습니다.
|
|
34
|
+
|
|
35
|
+
관리자의 오류·실패 내역은 사이트 접근 권한이 있는 `@roottale.com` 계정에만 제공합니다.
|
|
36
|
+
고객 계정은 기존 버튼 반응·유입 경로를 이용하며 오류 집계는 서버 응답에서 제외됩니다.
|
|
37
|
+
|
|
38
|
+
- dd539dc: 외부 원고의 영구 생성 키와 수정·발행 충돌 검사를 관리 API 문서 및 Next.js 연동 예제에 추가합니다. 동기화 체크포인트는 공개 응답에서 제외합니다.
|
|
39
|
+
- a9fe1c3: 공개 주소가 없는 공통 콘텐츠의 실제 문구 반영 확인 계약과 Next.js 출력 예제를 추가합니다.
|
|
40
|
+
|
|
3
41
|
## 0.72.1
|
|
4
42
|
|
|
5
43
|
### Patch 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의 초안·예약·비공개 글과 미디어 목록도 조회할 수
|
|
@@ -80,18 +88,33 @@ curl https://api.roottale.com/v1/cms/posts \
|
|
|
80
88
|
| `category_ids`, `tag_ids` | 생성과 동시에 연결할 term ID 배열 |
|
|
81
89
|
| `model_key` | 페이지·글·정보 모델의 안정 key. 모델이 여러 개면 필수 |
|
|
82
90
|
| `field_values` | 모델 필드 정의로 검증할 편집 원문 객체 |
|
|
91
|
+
| `source_key` | 외부 원고의 안정 식별키(1–255자). 같은 tenant/site/key는 기존 글을 반환 |
|
|
92
|
+
|
|
93
|
+
외부 원장 연동은 제목·슬러그 대신 `source_key`를 사용합니다(예: `notion:<Page ID>`).
|
|
94
|
+
키는 기존 글과 같은 DB 트랜잭션에 보관하며 동시 생성도 한 글로 합칩니다.
|
|
95
|
+
최초 생성은 201, 같은 키의 재시도는 200으로 기존 글을 반환하고 내용을 덮어쓰지
|
|
96
|
+
않습니다. 휴지통에 있으면 409 `source_post_removed`이며 자동으로 복원하지 않습니다.
|
|
97
|
+
이는 Worker 메모리의 `Idempotency-Key` 재시도 캐시와 별개입니다. 생성은 초안으로
|
|
98
|
+
진행하고 응답의 ID/상태를 확인한 뒤 수정·SEO·발행을 각각 처리하세요.
|
|
99
|
+
`meta_json._sync`는 외부 동기화 체크포인트용으로 사용할 수 있으며 관리 응답에서만
|
|
100
|
+
반환하고 공개 글 목록·상세·미리보기 응답에서는 제외합니다. 인증 정보는 넣지 마세요.
|
|
83
101
|
|
|
84
102
|
### PATCH /v1/cms/posts/{post_id}
|
|
85
103
|
|
|
86
104
|
보낸 필드만 수정합니다. `featured_media_id: null`이면 썸네일을 해제하고,
|
|
87
105
|
`scheduled_at: null`이면 예약을 해제합니다. `author_profile_id`를 생략하면 기존
|
|
88
106
|
공개 작성자를 유지하고, `null`이면 초안의 공개 작성자를 비웁니다.
|
|
107
|
+
`expected_updated_at`에 마지막 조회의 `updated_at`을 보내면 다른 저장이 먼저
|
|
108
|
+
일어났을 때 409 `post_update_conflict`로 중단합니다. 충돌 후 새 버전을 읽어
|
|
109
|
+
수정 내용을 확인하세요. 새 버전만 바꿔 맹목적으로 재전송하면 사람이 한 수정을 덮을 수 있습니다.
|
|
89
110
|
|
|
90
111
|
### POST /v1/cms/posts/{post_id}/publish
|
|
91
112
|
|
|
92
113
|
초안 또는 예약 글을 즉시 발행하고 등록된 revalidate 웹훅을 전송합니다.
|
|
93
114
|
본문은 `{}` 또는 `{"site_id":"..."}`입니다. 글(`type: "post"`)은 현재
|
|
94
115
|
사이트의 활성 공통 작성자가 지정돼 있어야 발행·예약할 수 있습니다.
|
|
116
|
+
선택적 `expected_updated_at`은 본문 검사 이후 다른 저장이 발생했을 때 발행을
|
|
117
|
+
409 `post_update_conflict`로 중단합니다. 글 수정과 같은 조회 버전 형식입니다.
|
|
95
118
|
|
|
96
119
|
### POST /v1/cms/posts/{post_id}/unpublish
|
|
97
120
|
|
|
@@ -556,6 +579,30 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
|
|
|
556
579
|
연결된 CMS 홈이 초안이거나 소유 경로가 제거되면 `null`을 반환합니다.
|
|
557
580
|
새 연동은 `fetchRouteMetadata`와 공통 빌더를 사용하세요(`seo.md` 참고).
|
|
558
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
|
+
|
|
559
606
|
## GET /v1/cms/public/patterns
|
|
560
607
|
|
|
561
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
|
공개 주소로 링크). 고른 것이 없을 때만 아래 자동 추천이 동작합니다.
|
|
@@ -19,6 +19,19 @@ RootTale의 콘텐츠 관리 단위는 다음과 같습니다.
|
|
|
19
19
|
삭제할 수 없습니다. 모델 상태만 활성화·비활성화할 수 있습니다.
|
|
20
20
|
`source: "site"`인 리소스는 관리 API로 만들고 수정할 수 있습니다.
|
|
21
21
|
|
|
22
|
+
## 병원 비급여 자동 연동
|
|
23
|
+
|
|
24
|
+
병원 사이트는 미리닥 API의 전체 항목을 자동으로 읽고 ROOT-ADMIN에는 병원 수정·추가·숨김만 저장합니다.
|
|
25
|
+
사이트 콘텐츠 계약의 `contentAreas[].editor.kind`는 `fields`, 입력칸은 `table`이며,
|
|
26
|
+
`table.source`에 `{ "provider": "miridoc", "institutionId": "<6자리 ID>" }`를 둡니다.
|
|
27
|
+
원본 조회는 문서를 쓰지 않습니다. 수정한 행은 다음 갱신 때 보존하고, 원천에서 빠진 수정본은 공개하지 않습니다.
|
|
28
|
+
기존 셀별 문서와 수정 이력은 `legacy` 키 매핑으로 보존합니다.
|
|
29
|
+
|
|
30
|
+
새 병원은 내부 `starter init --industry medical --miridoc-id <ID>`로 페이지·계약·푸터·미리보기를
|
|
31
|
+
함께 생성합니다. 기존 병원은 기존 URL·디자인에 같은 모듈을 연결합니다.
|
|
32
|
+
자료가 없는 기관은 병원 추가 내용을 사용합니다. 자동 자료의 범위·공개 경계·적용 순서는
|
|
33
|
+
[병원 공통 모듈](../examples/nextjs/hospital-fees/README.md)을 따릅니다.
|
|
34
|
+
|
|
22
35
|
## 권한
|
|
23
36
|
|
|
24
37
|
전체 자동화에는 API 키 프로필 `full_management`를 권장합니다. 세부 권한은
|
package/docs/inquiries.md
CHANGED
|
@@ -329,6 +329,8 @@ const result = await submitInquiry({
|
|
|
329
329
|
|
|
330
330
|
문의 제출 직전까지의 방문 경로(페이지 이동·주요 전환 이벤트)를 CRM 상세의
|
|
331
331
|
"방문 여정" 패널에서 시간순으로 확인할 수 있습니다.
|
|
332
|
+
일반 CTA와 기존 전화·메인·배너 버튼도 기록하며, 버튼 이름·위치가 있으면 함께 표시합니다.
|
|
333
|
+
공통 수집기는 [ROOT-ANALYTICS 설치](theme-and-settings.md#root-analytics-조회수first-party-비콘)를 따릅니다.
|
|
332
334
|
`@roottale/analytics-runtime`의 `readJourney()`로 읽은 sessionStorage 링버퍼를
|
|
333
335
|
`JSON.stringify`한 문자열을 그대로 `journey` 필드(직접 HTTP는 `attr_journey`
|
|
334
336
|
폼 필드)로 보내면 됩니다 — 파싱·검증은 서버가 담당합니다.
|
|
@@ -383,8 +385,16 @@ CMS 읽기 키와 공개 폼 접수 키에는 이 권한을 추가하지 마세
|
|
|
383
385
|
`source`는 변하지 않는 시스템 식별자, `observed_at`은 일관된 DB 읽기를 시작한
|
|
384
386
|
시각입니다. 각 항목은 `external_id`, `received_at`, `name`, `phone`, `email`,
|
|
385
387
|
`message`, `status`가 필요하며, `extras`와 `attribution`은 선택입니다.
|
|
386
|
-
상태는 `new`, `contacting`, `contacted`, `follow_up`, `converted`, `closed`입니다.
|
|
387
|
-
|
|
388
|
+
상태는 `new`, `contacting`, `contacted`, `follow_up`, `converted`, `closed`, `excluded`입니다.
|
|
389
|
+
`booked`(상담 예약), `visited`(실제 방문), `no_show`(노쇼), `canceled`(예약 취소)는
|
|
390
|
+
병원(`medical`) 사이트 영업 문의에서만 쓰며, 운영 DB에 새 상태 마이그레이션이
|
|
391
|
+
적용된 뒤 사용합니다. 병원 외 사이트가 이 상태를 보내면 전체 스냅샷을 `422`로
|
|
392
|
+
거부하며 기존 문의는 바뀌지 않습니다.
|
|
393
|
+
실제 성공 계약이 확인된 경우만 `converted`를 보냅니다. 병원에서는 시술·진료비 결제를 뜻합니다.
|
|
394
|
+
`excluded`는 담당자가 답변하지 않기로 결정한 **응대 제외**입니다. 아직 처리할
|
|
395
|
+
문의는 `new`, 고객에게 연락했지만 연결되지 않은 문의는 `contacting`으로 구분합니다.
|
|
396
|
+
응대 제외는 접수 건수에 포함하고 응대·전환 완료와 별도로 집계합니다. 운영 DB에
|
|
397
|
+
새 상태 마이그레이션이 적용된 뒤 사용하세요.
|
|
388
398
|
|
|
389
399
|
- 같은 사이트·원장·외부 ID는 같은 문의를 갱신합니다. 원본 접수일로 성과에 집계합니다.
|
|
390
400
|
- 전체 스냅샷에서 빠진 문의는 해당 원장의 사본에서 삭제됩니다. 페이지 한 장이나
|
|
@@ -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` 예시처럼 기존 글 원본을 사용합니다.
|
|
@@ -95,6 +135,26 @@ YouTube 입력은 인증정보·공백·제어문자·역슬래시 없는 HTTPS
|
|
|
95
135
|
|
|
96
136
|
사이트맵도 같은 최종 `noindex`·대표 경로 정책을 사용하고 활성 리다이렉트 출발 주소를 제외해야 합니다. 목록의 페이지 번호·필터별 정책은 사이트 라우트에서 계산합니다. 메타데이터 저장 성공·웹훅 수신 성공·실제 HTML 반영 확인은 서로 다른 상태입니다.
|
|
97
137
|
|
|
138
|
+
### 사이트가 공개 페이지의 색인을 관리할 때
|
|
139
|
+
|
|
140
|
+
공개 페이지 전체를 검색에 허용하는 사이트는 `robots.forceIndex: true`를 선언합니다.
|
|
141
|
+
고정 경로는 `metadata.routes[].robots`, 글·분류는 해당 모델의
|
|
142
|
+
`metadata.posts.defaultRules[modelKey].robots`와
|
|
143
|
+
`metadata.terms.defaultRules[modelKey].robots`에 같은 정책을 등록합니다.
|
|
144
|
+
FRONT의 `resolveMetadata`·`buildPageMetadata`와 사이트맵 필터에도 이 `robots`를
|
|
145
|
+
전달해야 합니다. 개별 `seo.noindex`나 분류의 `hub_promoted`를 일괄 수정하지 않습니다.
|
|
146
|
+
|
|
147
|
+
`forceIndex`는 개별 검색 제외·기본값보다 우선하지만 `forceNoindex`보다 우선하지
|
|
148
|
+
않습니다. 미리보기, 미발행 글, 콘텐츠 검수 조건과 빈 목록의 필수 제외는 계속 적용됩니다.
|
|
149
|
+
`ContentPolicy`도 같은 규칙으로 수동 제외를 해석합니다. 관리자에는 사이트 정책으로
|
|
150
|
+
검색 허용이 표시되며 개별 검색 제외 입력은 잠깁니다. 연결 미확인 상태는 검색 허용으로
|
|
151
|
+
표시하지 않습니다. 이 표시는 검색엔진이 이미 페이지를 색인했다는 뜻이 아닙니다.
|
|
152
|
+
|
|
153
|
+
새 정책을 사용하는 소비자는 이를 지원하는 SDK로 먼저 배포한 뒤 콘텐츠 계약을
|
|
154
|
+
동기화하고 설치 확인을 갱신합니다. `robots`를 직접 복사할 때 일부 키만 골라 정책을
|
|
155
|
+
누락하지 마세요. [CMS 페이지 예제](../examples/nextjs/lib/page-metadata.ts)는 정책을
|
|
156
|
+
그대로 받는 선택 인자를 제공합니다.
|
|
157
|
+
|
|
98
158
|
### 동적 콘텐츠의 공개·색인 정책
|
|
99
159
|
|
|
100
160
|
SDK 0.69.0의 `ContentPolicy`·`ContentQuery`는 `@roottale/cms-core`에서 제공합니다.
|
|
@@ -1059,18 +1119,42 @@ export default async function RootLayout({ children }) {
|
|
|
1059
1119
|
|
|
1060
1120
|
## Fleet 프로브 (운영 가시성)
|
|
1061
1121
|
|
|
1062
|
-
|
|
1122
|
+
ROOT-ADMIN 직원용 ‘전 사이트’ 화면이 배포 버전과 보안패치 상태를 확인하는
|
|
1123
|
+
well-known 라우트입니다. CMS 정보는 기존 SDK를 사용하고 실행 버전을 덧붙입니다.
|
|
1063
1124
|
|
|
1064
1125
|
```ts
|
|
1065
1126
|
// app/.well-known/roottale.json/route.ts
|
|
1066
1127
|
import { createFleetInfoRoute } from "@roottale/cms-renderer-next/routes";
|
|
1128
|
+
import nextPackage from "next/package.json";
|
|
1129
|
+
import { version as reactVersion } from "react";
|
|
1067
1130
|
|
|
1068
|
-
|
|
1131
|
+
const getCmsFleetInfo = createFleetInfoRoute({ site: "example" });
|
|
1132
|
+
export const dynamic = "force-dynamic";
|
|
1133
|
+
export const runtime = "nodejs";
|
|
1134
|
+
|
|
1135
|
+
export async function GET() {
|
|
1136
|
+
const response = await getCmsFleetInfo();
|
|
1137
|
+
return Response.json({
|
|
1138
|
+
...await response.json(),
|
|
1139
|
+
runtime: { next: nextPackage.version, react: reactVersion, node: process.versions.node },
|
|
1140
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
1141
|
+
}
|
|
1069
1142
|
```
|
|
1070
1143
|
|
|
1071
1144
|
`site`에는 사이트 식별용 슬러그를 넣습니다. 필수는 아니지만 운영 지원을
|
|
1072
1145
|
받으려면 추가를 권장합니다.
|
|
1073
1146
|
|
|
1147
|
+
기존 `exposures` 설정이 있으면 그대로 유지합니다. `runtime.next`는 설치된 정확한
|
|
1148
|
+
버전이며 `package.json`의 캐럿 범위를 보내면 안 됩니다. `runtime.node`는 요청을
|
|
1149
|
+
처리하는 Node.js 버전입니다. 정적 응답으로 만들면 빌드 머신의 버전이 잘못 표시될 수
|
|
1150
|
+
있으므로 동적 실행과 `no-store`를 유지합니다. `runtime.react`는 서버 React의 버전으로,
|
|
1151
|
+
안정판 Next.js에 포함된 React도 canary 식별자를 가질 수 있습니다. 환경 변수나 API 키는 응답에 넣지 않습니다.
|
|
1152
|
+
|
|
1153
|
+
실행 예제는 `examples/nextjs/app/.well-known/roottale.json/route.ts`에 있습니다.
|
|
1154
|
+
관리자는 누락·오류·알 수 없는 버전을 ‘미확인’으로 표시하고 공식 공지의 보안 최소
|
|
1155
|
+
버전과 권장 Next.js 버전을 구분합니다. 저장소 업데이트 뒤 운영 재배포까지 끝나야
|
|
1156
|
+
새 버전이 표시됩니다. 이 점검이 사이트를 자동 업데이트하거나 배포하지는 않습니다.
|
|
1157
|
+
|
|
1074
1158
|
## AEO/GEO — llms.txt
|
|
1075
1159
|
|
|
1076
1160
|
AI 검색·생성엔진(ChatGPT·Claude·Perplexity 등)의 크롤러는
|
|
@@ -349,21 +349,102 @@ ROOT-ANALYTICS 비콘은 쿠키리스 first-party 분석(페이지·클릭)과 *
|
|
|
349
349
|
|
|
350
350
|
```tsx
|
|
351
351
|
// app/layout.tsx (Next.js) — 서버 컴포넌트
|
|
352
|
-
import {
|
|
352
|
+
import { buildManifest } from "@roottale/analytics-runtime";
|
|
353
|
+
import { RootTaleAnalytics } from "@roottale/analytics-runtime/react";
|
|
354
|
+
import { RootTaleAnalyticsNextAdapter } from "@roottale/analytics-runtime/next";
|
|
353
355
|
import { fetchAnalyticsConfig } from "@roottale/cms-client/server";
|
|
354
356
|
|
|
355
357
|
const cfg = await fetchAnalyticsConfig({ apiKey: process.env.ROOTTALE_API_KEY! });
|
|
356
358
|
// ...<body> 안에:
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
siteId: cfg.siteId, // ← API 키에서 유도. 별도 env 불필요.
|
|
362
|
-
}),
|
|
363
|
-
}}
|
|
359
|
+
<RootTaleAnalytics
|
|
360
|
+
manifest={buildManifest(cfg)}
|
|
361
|
+
collectUrl="https://api.roottale.com/v1/collect"
|
|
362
|
+
siteId={cfg.siteId}
|
|
364
363
|
/>
|
|
364
|
+
<RootTaleAnalyticsNextAdapter />
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
루트 레이아웃에 한 번만 설치하세요. Next 어댑터가 클라이언트 페이지 이동도 기록합니다.
|
|
368
|
+
프레임워크 없는 사이트는 `renderJourneyBridgeScript()`를 먼저 실행한 뒤
|
|
369
|
+
`renderBeaconScript()`를 설치합니다. 자체 SPA 라우터는 이동 완료 시
|
|
370
|
+
`trackCustomEvent("pageview")`를 한 번 호출하세요.
|
|
371
|
+
|
|
372
|
+
### 유입부터 CTA 클릭까지
|
|
373
|
+
|
|
374
|
+
ROOT-ADMIN의 **성과 분석 → 페이지·문의 → 유입부터 버튼 클릭까지의 경로**를 펼치면
|
|
375
|
+
문의하지 않은 방문자의 클릭도 출처·캠페인·페이지 이동·CTA·위치별로 집계합니다.
|
|
376
|
+
CTA에는 아래 표식을 사용하세요. 입력값·전화번호·이메일·고객 이름을 표식에 넣지 마세요.
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
<a href="/contact"
|
|
380
|
+
data-track="cta_click"
|
|
381
|
+
data-track-cta="상담 신청"
|
|
382
|
+
data-track-placement="home-hero">
|
|
383
|
+
상담 신청하기
|
|
384
|
+
</a>
|
|
365
385
|
```
|
|
366
386
|
|
|
387
|
+
클릭 위임이 자동 수집하므로 같은 클릭을 `onClick`에서도 보내지 마세요.
|
|
388
|
+
DOM 클릭이 없는 사용자 행동만 `trackCustomEvent("cta_click", {
|
|
389
|
+
trackCta: "상담 신청", trackPlacement: "home-hero" })`로 보냅니다.
|
|
390
|
+
버튼 이름 표식이 없으면 해당 추적 버튼의 접근성 이름 또는 보이는 문구를 사용합니다.
|
|
391
|
+
추적 표식이 없는 버튼은 자동으로 수집하지 않습니다.
|
|
392
|
+
|
|
393
|
+
클릭 경로는 사이트별 sessionStorage에서 관리합니다. 같은 탭에서 30분간 기록이 없으면
|
|
394
|
+
새 경로가 시작되고 외부 출처로 다시 들어와도 새로 시작합니다. 최초 페이지와 최근 페이지를
|
|
395
|
+
최대 10개·1,200바이트 안에서 동봉하며 일부 생략·수집 시작 누락은 화면에 표시합니다.
|
|
396
|
+
저장소가 차단돼 있으면 현재 문서에서 수집한 이동만 유지됩니다.
|
|
397
|
+
URL 쿼리·해시, 외부 referrer 전체 주소, 광고 클릭 ID 원문은 경로에 포함하지 않습니다.
|
|
398
|
+
문의 고객의 CRM 기록에는 기존 `readJourney()` 동봉도 필요합니다([문의 연동](inquiries.md#방문-여정-journey)).
|
|
399
|
+
|
|
400
|
+
이 기능은 새 런타임으로 사이트를 다시 빌드한 이후의 클릭부터 적용됩니다.
|
|
401
|
+
기존 런타임의 클릭은 경로 미기록으로 구분하며 과거 이동을 추정하지 않습니다.
|
|
402
|
+
|
|
403
|
+
### 버튼 반응과 오류
|
|
404
|
+
|
|
405
|
+
`페이지·문의` 상단에는 **버튼 반응**이 표시됩니다. **확인할 오류**는 사이트 접근 권한이
|
|
406
|
+
있는 `@roottale.com` 계정 전용입니다. 고객 계정에는 오류 내역·횟수를 전달하지 않습니다.
|
|
407
|
+
|
|
408
|
+
| 항목 | 수집 방법 |
|
|
409
|
+
|---|---|
|
|
410
|
+
| 버튼 노출 | Next 어댑터가 `data-track` 버튼의 50% 이상 노출을 감지. 숨겨진 탭 제외 |
|
|
411
|
+
| 노출 뒤 첫 클릭 | 페이지·버튼·위치별 1회. 일반 클릭 횟수는 별도로 유지 |
|
|
412
|
+
| 필수값·형식 오류 | `form[data-track-form="contact"]`의 HTML 검증 오류 자동 기록 |
|
|
413
|
+
| 서버·연결·사용자 정의 검증 오류 | `trackFormError()` 호출 |
|
|
414
|
+
| 페이지 실행·처리되지 않은 Promise·리소스 실패 | 비콘 설치 직후 자동 수집 |
|
|
415
|
+
| 처리된 API 요청 실패·프레임워크 오류 | `trackSiteError()` 호출 |
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
import { trackFormError } from "@roottale/analytics-runtime";
|
|
419
|
+
|
|
420
|
+
// 오류 메시지·입력값 대신 폼과 항목의 고정 key, 분류 코드만 보냅니다.
|
|
421
|
+
trackFormError("contact", "format", "email");
|
|
422
|
+
trackFormError("contact", "network");
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
허용 코드는 `required`, `format`, `length`, `range`, `invalid`, `verification`,
|
|
426
|
+
`rate_limited`, `network`, `server`, `unknown`입니다. key에는 영문·숫자·`_:-`만 사용하세요.
|
|
427
|
+
기본 `RootTaleLeadForm`은 오류 표식이 포함됩니다. 새 폼의 접수 성공·실패 연결은
|
|
428
|
+
[문의 폼 예제](../examples/nextjs/components/site-inquiry-form.tsx)를 따르세요.
|
|
429
|
+
입력값이나 서버 오류 문장을 보내지 마세요.
|
|
430
|
+
|
|
431
|
+
노출 누락으로 클릭이 노출보다 많으면 비율은 `—`로 표시합니다. 재방문·새 페이지는
|
|
432
|
+
새 노출로 세므로 방문자 수가 아닙니다. 표본 보정 수치는 화면에서 구분합니다.
|
|
433
|
+
DOM 표식 없이 직접 호출한 클릭은 노출 대비 클릭률에서 제외합니다.
|
|
434
|
+
Next 외 환경은 비콘 뒤 `initObservers()`를 실행하고 SPA 이동마다 감지기를 재연결하세요.
|
|
435
|
+
|
|
436
|
+
사이트 오류는 종류·페이지·고정 작업명·실패 상태만 전송합니다. 오류 원문·스택·거절 값·
|
|
437
|
+
요청 URL은 보내지 않습니다. 오류 반복은 페이지당 최대 10건으로 제한하므로 수집 횟수입니다.
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { trackSiteError } from "@roottale/analytics-runtime";
|
|
441
|
+
if (!response.ok) trackSiteError("request", { operation: "load_posts", status: response.status });
|
|
442
|
+
// 연결 자체가 실패하면 status 생략
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Next.js가 처리한 렌더 오류는 [오류 화면 예제](../examples/nextjs/app/error.tsx)를 따릅니다.
|
|
446
|
+
상세 원인 분석용 스택과 서버 로그는 기존 오류 모니터링에서 확인하세요.
|
|
447
|
+
|
|
367
448
|
**글별 조회수**가 정확히 집계되려면, 글 상세 페이지가 자신의 글 ID를
|
|
368
449
|
`<meta name="rt:content-id">`로 노출해야 합니다. 비콘이 이 값을 읽어 조회를 해당
|
|
369
450
|
글에 귀속시킵니다(URL·경로가 바뀌어도 안정적).
|
|
@@ -432,8 +513,9 @@ ROOT-ADMIN의 통계 > 페이지·문의 성과에는 출처·캠페인 → 첫
|
|
|
432
513
|
의미하지 않습니다. 기존 기록에 출처를 추정해 붙이지 않습니다. 원본 referrer 경로,
|
|
433
514
|
검색 입력값, 광고 클릭 ID는 보내지 말고 호스트·캠페인 라벨·광고 클릭 여부만 사용합니다.
|
|
434
515
|
|
|
435
|
-
Analytics Engine의 blob10~15는 해당 클릭의 유입
|
|
436
|
-
|
|
516
|
+
Analytics Engine의 blob10~15는 해당 클릭의 유입 출처를 저장합니다. blob20은 공통 수집기의
|
|
517
|
+
경로 JSON 또는 이전 `attr:1` 계약의 첫 도착 pathname을 받습니다. 두 형식을 모두 읽으며
|
|
518
|
+
페이지 유입 보고서는 계속 `pageview`만 읽습니다. 현재 20개 blob 슬롯을
|
|
437
519
|
모두 사용하므로 추가 차원은 새 저장 설계를 검토해야 합니다.
|
|
438
520
|
|
|
439
521
|
비콘은 사이트별 브라우저 저장소에 마지막 방문 시각만 두고, 30분 동안 움직임이 없으면
|
|
@@ -310,3 +310,48 @@ expect(result.invalidExceptions).toEqual([]);
|
|
|
310
310
|
계약이 그룹·반복 컨테이너를 선언하면 편집기가 생성하는 자식 주소도 허용합니다.
|
|
311
311
|
`flexible_content`는 `layouts: [{ name, subFields }]`와 실제 행의 `__layout`을
|
|
312
312
|
대조하며 다른 레이아웃 필드를 연결하면 실패합니다.
|
|
313
|
+
|
|
314
|
+
## 공통 문구의 공개 반영 확인
|
|
315
|
+
|
|
316
|
+
공개 주소가 없는 `shared: true` 모델은 제목 포함 여부로 확인할 수 없습니다.
|
|
317
|
+
모델에 다음 선택 설정을 등록하면 발행 웹훅 전달 후 지정된 공개 HTML의 실제 문구를 비교합니다.
|
|
318
|
+
`detailPath`가 있는 모델에는 이 설정을 사용할 수 없습니다. 기존 계약은 설정 없이 그대로 동작합니다.
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"sharedFieldVerification": {
|
|
323
|
+
"paths": ["/news"],
|
|
324
|
+
"fields": [{
|
|
325
|
+
"key": "field_phone",
|
|
326
|
+
"path": ["clinic", "phone"],
|
|
327
|
+
"sectionKey": "clinic",
|
|
328
|
+
"visibilitySectionKey": "footer",
|
|
329
|
+
"normalization": "phone"
|
|
330
|
+
}]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`paths`는 동일 사이트의 고정 공개 경로 1~3개입니다. 필드는 최대 50개이며,
|
|
336
|
+
`key`는 해당 모델의 실제화면 편집 템플릿에 등록된 필드, `path`는 ACF 루트부터의 저장 경로입니다.
|
|
337
|
+
`sectionKey`는 그 필드가 속한 편집 섹션입니다. 다른 섹션의 표시 상태도 영향을 준다면
|
|
338
|
+
`visibilitySectionKey`를 지정합니다. 두 섹션 중 하나가 숨김·삭제 상태이거나 문서가 미발행이면
|
|
339
|
+
그 필드가 공개 HTML에 없어야 합니다. 선언한 값이 저장 데이터에서 누락되면 확인 실패로 처리합니다.
|
|
340
|
+
|
|
341
|
+
공개 텍스트 자체에 `data-rt-field-check="문서ID:필드key"`를 붙이고 가장 가까운 상위 영역에
|
|
342
|
+
`data-rt-shared-section="모델key:섹션key"`를 붙입니다.
|
|
343
|
+
[`SharedFieldOutput`](../examples/nextjs/components/shared-field-output.tsx) 예제처럼 실제 값만 렌더합니다.
|
|
344
|
+
CMS 원문·기대값·해시를 숨은 HTML이나 속성에 복제해서는 안 됩니다.
|
|
345
|
+
코드 대체값을 표시할 때 CMS 문서 표식을 붙이면 안 됩니다.
|
|
346
|
+
|
|
347
|
+
`text`는 Unicode NFC와 공백만 정규화하고, `phone`은 공백·괄호·점·하이픈만 제거합니다.
|
|
348
|
+
그 외 글자와 대소문자는 그대로 비교합니다. 같은 필드가 여러 번 있으면 모두 일치해야 하며,
|
|
349
|
+
스크립트·템플릿·숨김·`aria-hidden` 요소의 값은 인정하지 않습니다.
|
|
350
|
+
검증 작업에는 문구의 SHA-256만 저장합니다. HTTP 실패·누락·오래된 문구는 기존 반영 확인의
|
|
351
|
+
재시도와 결과 표시에 연결됩니다. 문서의 새 수정이나 계약 체크섬 변경이 생기면 이전 결과는 폐기합니다.
|
|
352
|
+
|
|
353
|
+
대표 경로에는 해당 공통 필드가 항상 등장하는 화면을 선택합니다. 페이지 자체의 별도 숨김 설정이나
|
|
354
|
+
조건에 따라 필드가 빠지는 화면은 선택하지 않습니다. 이 검사는 지정한 문구의 서버 HTML 반영을
|
|
355
|
+
확인하며, CSS 가시성·이미지·링크 목적지·전체 사이트를 검사하는 기능은 아닙니다.
|
|
356
|
+
사이트에 표식을 먼저 배포한 뒤 새 계약을 등록합니다. 계약 버전·runtime receipt도 함께 갱신하고,
|
|
357
|
+
기존 고객 콘텐츠를 다시 가져와 덮어쓰지 않습니다.
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
import nextPackage from "next/package.json";
|
|
2
|
+
import { version as reactVersion } from "react";
|
|
1
3
|
import { createFleetInfoRoute } from "@roottale/cms-renderer-next/routes";
|
|
2
4
|
import { EXPOSURE_SLOTS } from "../../../lib/exposure-slots";
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
const getCmsFleetInfo = createFleetInfoRoute({
|
|
5
7
|
exposures: { runtimeVersion: 2, endpoint: "/api/exposures", slots: EXPOSURE_SLOTS },
|
|
6
8
|
});
|
|
9
|
+
|
|
10
|
+
// Node 버전은 요청을 처리하는 런타임에서 읽는다. 빌드 시 정적 응답으로 고정하지 않는다.
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
export const runtime = "nodejs";
|
|
13
|
+
|
|
14
|
+
export async function GET() {
|
|
15
|
+
const response = await getCmsFleetInfo();
|
|
16
|
+
const info = await response.json();
|
|
17
|
+
return Response.json({
|
|
18
|
+
...info,
|
|
19
|
+
runtime: { next: nextPackage.version, react: reactVersion, node: process.versions.node },
|
|
20
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
21
|
+
}
|
|
@@ -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도 갱신합니다.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { trackSiteError } from "@roottale/analytics-runtime";
|
|
5
|
+
|
|
6
|
+
/** Next.js가 처리한 렌더 오류도 기록한다. error 원문·digest는 전송하지 않는다. */
|
|
7
|
+
export default function SiteError({ error, reset }: { error: Error; reset: () => void }) {
|
|
8
|
+
const recorded = useRef<Error | null>(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (recorded.current === error) return;
|
|
11
|
+
recorded.current = error;
|
|
12
|
+
trackSiteError("script", { operation: "page" });
|
|
13
|
+
}, [error]);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<section role="alert">
|
|
17
|
+
<h1>화면을 불러오지 못했습니다</h1>
|
|
18
|
+
<button onClick={reset}>다시 시도</button>
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -8,6 +8,9 @@ import "@roottale/cms-renderer-next/styles";
|
|
|
8
8
|
import type { Metadata } from "next";
|
|
9
9
|
import { headers } from "next/headers";
|
|
10
10
|
import { SiteExposures } from "../components/site-exposures";
|
|
11
|
+
import { buildManifest } from "@roottale/analytics-runtime";
|
|
12
|
+
import { RootTaleAnalytics } from "@roottale/analytics-runtime/react";
|
|
13
|
+
import { RootTaleAnalyticsNextAdapter } from "@roottale/analytics-runtime/next";
|
|
11
14
|
|
|
12
15
|
import {
|
|
13
16
|
BUSINESS_CACHE_TAG,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
fetchBusinessProfile,
|
|
16
19
|
fetchTheme,
|
|
17
20
|
googleTagManagerSnippet,
|
|
21
|
+
fetchAnalyticsConfig,
|
|
18
22
|
localBusinessSchema,
|
|
19
23
|
siteVerificationMetadata,
|
|
20
24
|
} from "@roottale/cms-client/server";
|
|
@@ -51,6 +55,10 @@ export default async function RootLayout({
|
|
|
51
55
|
apiKey: process.env.ROOTTALE_API_KEY!,
|
|
52
56
|
tags: [BUSINESS_CACHE_TAG],
|
|
53
57
|
}).catch(() => null);
|
|
58
|
+
const analytics = await fetchAnalyticsConfig({
|
|
59
|
+
apiKey: process.env.ROOTTALE_API_KEY!,
|
|
60
|
+
baseUrl: process.env.ROOTTALE_API_BASE,
|
|
61
|
+
}).catch(() => null);
|
|
54
62
|
|
|
55
63
|
const theme = await fetchTheme({ apiKey: process.env.ROOTTALE_API_KEY!, tags: [THEME_CACHE_TAG] }).catch(() => null);
|
|
56
64
|
const host = (await headers()).get("host")?.split(":")[0];
|
|
@@ -74,6 +82,16 @@ export default async function RootLayout({
|
|
|
74
82
|
/>
|
|
75
83
|
) : null}
|
|
76
84
|
<SiteExposures>{children}</SiteExposures>
|
|
85
|
+
{analytics ? (
|
|
86
|
+
<>
|
|
87
|
+
<RootTaleAnalytics
|
|
88
|
+
manifest={buildManifest(analytics)}
|
|
89
|
+
siteId={analytics.siteId}
|
|
90
|
+
collectUrl={`${(process.env.ROOTTALE_API_BASE ?? "https://api.roottale.com").replace(/\/$/, "")}/v1/collect`}
|
|
91
|
+
/>
|
|
92
|
+
<RootTaleAnalyticsNextAdapter />
|
|
93
|
+
</>
|
|
94
|
+
) : null}
|
|
77
95
|
</body>
|
|
78
96
|
</html>
|
|
79
97
|
);
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { HomeBanner } from "../components/site-exposures";
|
|
2
|
+
import Link from "next/link";
|
|
2
3
|
|
|
3
4
|
export default function HomePage() {
|
|
4
|
-
return
|
|
5
|
+
return (
|
|
6
|
+
<main>
|
|
7
|
+
<h1>홈페이지</h1>
|
|
8
|
+
<HomeBanner />
|
|
9
|
+
<p>사이트 소개와 소식을 확인하세요.</p>
|
|
10
|
+
<Link href="/contact" data-track="cta_click" data-track-cta="상담 신청" data-track-placement="home-hero">
|
|
11
|
+
상담 신청하기
|
|
12
|
+
</Link>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
5
15
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Match the installed sharedFieldVerification contract; render the real public text only. */
|
|
2
|
+
export function SharedFieldOutput({ entityId, modelKey, sectionKey, fieldKey, value, visible = true }: {
|
|
3
|
+
entityId: string; modelKey: string; sectionKey: string; fieldKey: string; value: string; visible?: boolean;
|
|
4
|
+
}) {
|
|
5
|
+
if (!visible) return null;
|
|
6
|
+
return <span data-rt-shared-section={`${modelKey}:${sectionKey}`}>
|
|
7
|
+
<span data-rt-field-check={`${entityId}:${fieldKey}`}>{value}</span>
|
|
8
|
+
</span>;
|
|
9
|
+
}
|