@roottale/cms-mcp 0.53.0 → 0.55.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.
@@ -1,9 +1,9 @@
1
1
  ---
2
- title: 테마·블로그 표시·분석 태그 설정
3
- description: 어드민에서 관리하는 디자인 토큰, 블로그 표시 옵션, 분석 태그를 사이트에서 조회
2
+ title: 테마·블로그 표시·ROOT-ANALYTICS 설정
3
+ description: 어드민에서 관리하는 디자인 토큰, 블로그 표시 옵션, ROOT-ANALYTICS 설정을 사이트에서 조회
4
4
  ---
5
5
 
6
- # 테마·블로그 표시·분석 태그 설정
6
+ # 테마·블로그 표시·ROOT-ANALYTICS 설정
7
7
 
8
8
  어드민에서 설정한 값을 공개 API로 조회해 사이트에 반영합니다. 모두
9
9
  `@roottale/cms-client/server`에서 제공하며 같은 API 키를 사용합니다.
@@ -179,10 +179,10 @@ if (business) {
179
179
  > 반환합니다. 전화·주소만 채우고 이름을 비워두면 **나머지 입력이 전부 무시**되니
180
180
  > 고객 안내 시 이름을 필수로 안내하세요.
181
181
 
182
- ## 분석 태그 — fetchAnalyticsConfig
182
+ ## ROOT-ANALYTICS 설정 — fetchAnalyticsConfig
183
183
 
184
- 어드민에서 등록한 외부 분석 태그(GA4, Microsoft Clarity, Meta Pixel, 네이버)
185
- 설정을 조회해 사이트에 주입합니다.
184
+ ROOT-ADMIN에서 등록한 외부 태그(GA4, Microsoft Clarity, Meta Pixel, 네이버)와
185
+ ROOT-ANALYTICS 사이트 ID를 조회해 고객 사이트에 연결합니다.
186
186
 
187
187
  ```ts
188
188
  import { fetchAnalyticsConfig } from "@roottale/cms-client/server";
@@ -197,9 +197,9 @@ const config = await fetchAnalyticsConfig({
197
197
  `enabled: true`인 태그만 렌더링하세요. 태그 ID는 어드민에서 변경될 수
198
198
  있으므로 하드코딩하지 말고 본 API로 조회하는 것을 권장합니다.
199
199
 
200
- ## 조회수 / first-party 비콘
200
+ ## ROOT-ANALYTICS 조회수·first-party 비콘
201
201
 
202
- RootTale 비콘은 쿠키리스 first-party 분석(방문수·클릭)과 **글별 조회수**를
202
+ ROOT-ANALYTICS 비콘은 쿠키리스 first-party 분석(페이지·클릭)과 **글별 조회수**를
203
203
  수집합니다. **API 키 하나로** 동작합니다 — 별도 사이트 ID 환경변수가 필요 없습니다.
204
204
  `fetchAnalyticsConfig`가 돌려주는 `siteId`를 비콘에 그대로 넘기세요.
205
205
 
@@ -240,10 +240,21 @@ export async function generateMetadata({ params }): Promise<Metadata> {
240
240
  > `@roottale/cms-client/server`의 `contentIdMeta(post.id)`가 같은 `<meta>` 태그
241
241
  > 문자열을 만들어 줍니다.
242
242
 
243
- 수집은 익명·쿠키리스이며 비콘은 클릭(`data-track`)과 pageview만 보냅니다. 봇
244
- 트래픽은 서버에서 제외됩니다. 공개 사이트에 "조회 N"을 표시하는 옵션은 어드민의
243
+ 수집은 익명·쿠키리스이며 비콘은 pageview와 명시한 행동 이벤트를 보냅니다. Next.js
244
+ SPA 전환과 섹션·스크롤·읽기·폼 감지는 `@roottale/analytics-runtime/next` 어댑터로
245
+ 연결합니다. 봇 트래픽은 서버에서 제외됩니다. 공개 사이트에 "조회 N"을 표시하는 옵션은 어드민의
245
246
  사이트 설정에서 켤 수 있습니다(켜면 글 응답에 `view_count`가 포함됩니다).
246
247
 
248
+ 저장 위치는 데이터 성격에 따라 나뉩니다.
249
+
250
+ | 데이터 | 저장 위치 |
251
+ |---|---|
252
+ | 페이지·클릭·섹션 이벤트 | Cloudflare Analytics Engine `cms_site_events` |
253
+ | 글 누적 조회수 | 사이트별 Durable Object SQLite, PostgreSQL `posts.view_count` 미러 |
254
+ | 첫·마지막 유입 | 브라우저 `localStorage._rt_attr`(30일) |
255
+ | 현재 방문 여정 | 브라우저 `sessionStorage._rt_journey`(최대 30건) |
256
+ | 문의에 귀속된 유입·여정 | PostgreSQL `inquiries.attribution`, `inquiries.journey` |
257
+
247
258
  ## 사이트 지식 — 브랜드 보이스 (AI 에이전트용)
248
259
 
249
260
  이 사이트의 **브랜드 보이스**(어조·톤·화자)와 **용어 규칙**(금지어·교정어)을
@@ -0,0 +1,70 @@
1
+ // 글 미리보기 — ROOT-ADMIN 편집기의 '미리보기'·'공유 링크'가 여는 주소.
2
+ // 관리자 발급 토큰(1시간, 글 1건 전용)으로 편집 중인 내용을 받아 발행 글과
3
+ // **같은 템플릿**(RootTaleBlogPost)으로 그린다 → 미리보기 = 발행 결과.
4
+ // 라우트를 배포한 뒤 관리자 설정 › 외부 연결 › "글 미리보기 여는 곳"에서
5
+ // "홈페이지에서 열기"를 켜면 편집기가 이 주소를 연다.
6
+ import type { Metadata } from "next";
7
+ import { notFound } from "next/navigation";
8
+ import {
9
+ fetchPostPreview,
10
+ isPreviewExpiredError,
11
+ } from "@roottale/cms-client/server";
12
+ import {
13
+ RootTaleBlogPost,
14
+ RootTalePreviewNotice,
15
+ } from "@roottale/cms-renderer-next/server";
16
+ import { buildPreviewMetadata } from "@roottale/cms-renderer-next/routes";
17
+
18
+ // 초안이 ISR·CDN 에 남으면 안 된다 — 항상 동적 렌더.
19
+ export const dynamic = "force-dynamic";
20
+ export const revalidate = 0;
21
+
22
+ type Props = {
23
+ params: Promise<{ id: string }>;
24
+ searchParams: Promise<{ token?: string }>;
25
+ };
26
+
27
+ function loadPreview(token: string) {
28
+ return fetchPostPreview({
29
+ apiKey: process.env.ROOTTALE_API_KEY!,
30
+ baseUrl: process.env.ROOTTALE_API_BASE,
31
+ token,
32
+ });
33
+ }
34
+
35
+ export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
36
+ const { token } = await searchParams;
37
+ const post = token ? await loadPreview(token).catch(() => null) : null;
38
+ // 항상 noindex/nofollow — 초안이 색인되거나 발행 주소와 중복되면 안 된다.
39
+ return buildPreviewMetadata({ title: post?.title });
40
+ }
41
+
42
+ export default async function PostPreviewPage({ params, searchParams }: Props) {
43
+ const { id } = await params;
44
+ const { token } = await searchParams;
45
+ if (!token) notFound(); // 주소만으로는 아무것도 보이지 않는다.
46
+
47
+ let expiresAt: string | undefined;
48
+ try {
49
+ const post = await loadPreview(token);
50
+ if (!post || post.id !== id) notFound(); // 토큰은 글 1건 전용
51
+ expiresAt = post.preview.expiresAt;
52
+ } catch (error) {
53
+ if (!isPreviewExpiredError(error)) throw error; // 만료는 컴포넌트가 안내
54
+ }
55
+
56
+ return (
57
+ <main className="container">
58
+ <RootTalePreviewNotice expiresAt={expiresAt} />
59
+ {/* 발행 글 상세(app/blog/[slug]/page.tsx)와 같은 props 를 쓰되
60
+ slugOrId 대신 previewToken 만 넘긴다. */}
61
+ <RootTaleBlogPost
62
+ apiKey={process.env.ROOTTALE_API_KEY!}
63
+ baseUrl={process.env.ROOTTALE_API_BASE}
64
+ previewToken={token}
65
+ relatedPostsCount={3}
66
+ breadcrumb={{ siteUrl: process.env.NEXT_PUBLIC_SITE_URL }}
67
+ />
68
+ </main>
69
+ );
70
+ }
@@ -0,0 +1,64 @@
1
+ // 사이트 통합 검색 — 고객 브라우저가 아니라 이 Server Component가 CMS API를 호출한다.
2
+ import type { Metadata } from "next";
3
+ import Link from "next/link";
4
+
5
+ import {
6
+ fetchCollections,
7
+ resolveSearchHitPath,
8
+ searchPosts,
9
+ } from "@roottale/cms-client/server";
10
+
11
+ export const metadata: Metadata = {
12
+ title: "사이트 검색",
13
+ robots: { index: false, follow: true },
14
+ };
15
+
16
+ interface Props {
17
+ searchParams: Promise<{ q?: string | string[] }>;
18
+ }
19
+
20
+ export default async function SearchPage({ searchParams }: Props) {
21
+ const { q = "" } = await searchParams;
22
+ const query = (typeof q === "string" ? q : q[0] ?? "").trim();
23
+ const apiKey = process.env.ROOTTALE_API_KEY!;
24
+ const baseUrl = process.env.ROOTTALE_API_BASE;
25
+ const [hits, collections] = query
26
+ ? await Promise.all([
27
+ searchPosts({ apiKey, baseUrl, query, type: "all", limit: 20 }),
28
+ fetchCollections({ apiKey, baseUrl }).catch(() => []),
29
+ ])
30
+ : [[], []];
31
+
32
+ return (
33
+ <main>
34
+ <h1>사이트 검색</h1>
35
+ <form action="/search" method="get" role="search">
36
+ <label htmlFor="site-search">검색어</label>
37
+ <input
38
+ defaultValue={query}
39
+ id="site-search"
40
+ maxLength={100}
41
+ name="q"
42
+ required
43
+ type="search"
44
+ />
45
+ <button type="submit">검색</button>
46
+ </form>
47
+ {query ? <p>검색 결과 {hits.length}건</p> : <p>검색어를 입력해 주세요.</p>}
48
+ <ul>
49
+ {hits.map((hit) => {
50
+ const href = resolveSearchHitPath(hit, collections);
51
+ if (!href) return null;
52
+ return (
53
+ <li key={hit.id}>
54
+ <Link href={href}>
55
+ <h2>{hit.title}</h2>
56
+ {hit.excerpt ? <p>{hit.excerpt}</p> : null}
57
+ </Link>
58
+ </li>
59
+ );
60
+ })}
61
+ </ul>
62
+ </main>
63
+ );
64
+ }
@@ -0,0 +1,13 @@
1
+ import { RootTaleExposureSlot } from "@roottale/cms-renderer-next/server";
2
+
3
+ export async function GlobalPopup({ path }: { path: string }) {
4
+ return (
5
+ <RootTaleExposureSlot
6
+ apiKey={process.env.ROOTTALE_API_KEY!}
7
+ slotKey="global-popup"
8
+ path={path}
9
+ allowedVariants={["card", "image-card"]}
10
+ revalidate={60}
11
+ />
12
+ );
13
+ }
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  BLOG_SETTINGS_CACHE_TAG,
5
5
  fetchBlogSettings,
6
+ fetchCategoryCounts,
6
7
  fetchPost,
7
8
  type CmsPostContent,
8
9
  } from "@roottale/cms-client/server";
@@ -31,6 +32,7 @@ export interface BlogPostMeta {
31
32
  categorySlug: string; // "" = 카테고리 미지정 — /blog/categories/{slug} 허브 필터링용
32
33
  tags: { name: string; slug: string }[];
33
34
  image: string | null;
35
+ authorProfileId?: string | null;
34
36
  authorName?: string;
35
37
  authorSlug?: string | null; // /blog/author/{slug} — 미발급 작가는 null
36
38
  seo?: {
@@ -60,6 +62,7 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
60
62
  ?.filter((t) => t.taxonomy === "tag")
61
63
  .map((t) => ({ name: t.name, slug: t.slug })) ?? [],
62
64
  image: post.featuredImageUrl ?? null,
65
+ authorProfileId: post.authorProfileId,
63
66
  authorName: post.authorName ?? undefined,
64
67
  authorSlug: post.authorSlug,
65
68
  seo: meta.seo as BlogPostMeta["seo"],
@@ -96,6 +99,25 @@ export async function getPostsByCategory(
96
99
  return posts.filter((p) => p.categorySlug === categorySlug);
97
100
  }
98
101
 
102
+ // 카테고리 허브의 소개문·검색 문구·공유 이미지는 카테고리 집계 응답에 함께 옵니다.
103
+ // 구버전 API와 이미지 미설정을 고려해 nullable 값으로 다룹니다.
104
+ export async function getCategoryArchiveMeta(categorySlug: string) {
105
+ const { categories } = await fetchCategoryCounts({
106
+ apiKey: getApiKey(),
107
+ baseUrl,
108
+ excludeCollections: true,
109
+ });
110
+ const category = categories.find((item) => item.slug === categorySlug);
111
+ if (!category) return null;
112
+ return {
113
+ title: category.seoTitle ?? `${category.name} 글 모음`,
114
+ description: category.seoDescription ?? category.description,
115
+ image: category.imageUrl
116
+ ? { url: category.imageUrl, alt: category.imageAlt ?? category.name }
117
+ : null,
118
+ };
119
+ }
120
+
99
121
  // 색인 위생 — 어드민 "내 사이트 > 카테고리 > 검색에 이 분류 페이지 노출" 을 켠
100
122
  // 분류인지. 켜지 않은 분류의 모음 페이지는 noindex + 사이트맵 제외가 됩니다
101
123
  // (seo.md "색인 위생" 절).
@@ -0,0 +1,12 @@
1
+ import { fetchContentModels, fetchPosts } from "@roottale/cms-client/server";
2
+
3
+ const apiKey = process.env.ROOTTALE_API_KEY!;
4
+
5
+ export async function getTeamMembers() {
6
+ const models = await fetchContentModels({ apiKey });
7
+ const model = models.models.find((candidate) => candidate.key === "team-member");
8
+ if (!model || model.preset !== "entity") return [];
9
+
10
+ const response = await fetchPosts({ apiKey, modelKey: model.key, limit: 100 });
11
+ return response.items;
12
+ }
@@ -2,7 +2,8 @@
2
2
  //
3
3
  // 글 슬러그 변경 자동 301 은 글 라우트에서 처리되지만(blog 예시 참고), 글이
4
4
  // 아닌 임의 경로(`/old-event → /promo`)는 라우팅 이전 단계인 미들웨어에서만
5
- // 가로챌 수 있다. 규칙은 자동 캐시되고, API 실패 시 트래픽을 막지 않는다.
5
+ // 가로챌 수 있다. 규칙은 자동 캐시되고, API 실패 시 stale 캐시를
6
+ // 우선 사용한다. 캐시가 없거나 순환 규칙이면 트래픽을 막지 않는다.
6
7
  import { NextResponse } from "next/server";
7
8
  import { createRedirectMiddleware } from "@roottale/cms-renderer-next/routes";
8
9
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
- "description": "RootTale CMS MCP server and CLI for post publishing, media uploads, integration docs, and public API access.",
5
+ "description": "RootTale CMS MCP server and CLI for models, entries, exposures, media, integration docs, and public API access.",
6
6
  "bin": {
7
7
  "roottale-cms-mcp": "dist/index.js"
8
8
  },