@roottale/cms-mcp 0.22.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.
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: 테마·블로그 표시·분석 태그 설정
3
+ description: 어드민에서 관리하는 디자인 토큰, 블로그 표시 옵션, 분석 태그를 사이트에서 조회
4
+ ---
5
+
6
+ # 테마·블로그 표시·분석 태그 설정
7
+
8
+ 어드민에서 설정한 값을 공개 API로 조회해 사이트에 반영합니다. 모두
9
+ `@roottale/cms-client/server`에서 제공하며 같은 API 키를 사용합니다.
10
+
11
+ ## 디자인 토큰 — fetchTheme
12
+
13
+ ```ts
14
+ import { fetchTheme } from "@roottale/cms-client/server";
15
+
16
+ const theme = await fetchTheme({ apiKey: process.env.ROOTTALE_API_KEY! });
17
+ // theme.colors / theme.fonts / theme.radius — 어드민에서 설정한 토큰만 포함
18
+ ```
19
+
20
+ 토큰을 CSS 변수로 매핑해 렌더러 스타일과 사이트 스타일을 일치시킬 수
21
+ 있습니다. 토큰 변경 시에도 발행 웹훅이 발송되어 캐시가 갱신됩니다.
22
+
23
+ ## 블로그 표시 설정 — fetchBlogSettings
24
+
25
+ 어드민의 블로그 표시 옵션(TOC 노출, 작성자/발행일 표시, 작성자 카드, 저자
26
+ 프로필)을 조회합니다.
27
+
28
+ ```ts
29
+ import {
30
+ fetchBlogSettings,
31
+ resolvePostDisplay,
32
+ DEFAULT_BLOG_SETTINGS,
33
+ } from "@roottale/cms-client/server";
34
+
35
+ const settings = await fetchBlogSettings({
36
+ apiKey: process.env.ROOTTALE_API_KEY!,
37
+ });
38
+ // showTableOfContents, showAuthor, showDate, showAuthorCard,
39
+ // tocTitle, authorProfileName / Bio / ImageUrl 등
40
+
41
+ // 글 단위 오버라이드(metaJson)와 합성해 최종 표시값 계산
42
+ const display = resolvePostDisplay(settings, post);
43
+ ```
44
+
45
+ `RootTaleBlogPost` 컴포넌트를 쓰면 이 설정이 자동 반영됩니다 — 커스텀 UI를
46
+ 만들 때만 직접 조회하면 됩니다.
47
+
48
+ ## 분석 태그 — fetchAnalyticsConfig
49
+
50
+ 어드민에서 등록한 외부 분석 태그(GA4, Microsoft Clarity, Meta Pixel, 네이버)
51
+ 설정을 조회해 사이트에 주입합니다.
52
+
53
+ ```ts
54
+ import { fetchAnalyticsConfig } from "@roottale/cms-client/server";
55
+
56
+ const config = await fetchAnalyticsConfig({
57
+ apiKey: process.env.ROOTTALE_API_KEY!,
58
+ });
59
+ // config.tags: { provider: "ga4" | "clarity" | "meta_pixel" | "naver",
60
+ // id: string, enabled: boolean }[]
61
+ ```
62
+
63
+ `enabled: true`인 태그만 렌더링하세요. 태그 ID는 어드민에서 변경될 수
64
+ 있으므로 하드코딩하지 말고 본 API로 조회하는 것을 권장합니다.
@@ -0,0 +1,4 @@
1
+ // RootTale fleet 프로브 — 운영 측이 배포 버전·헬스를 확인하는 well-known 라우트.
2
+ import { createFleetInfoRoute } from "@roottale/cms-renderer-next/routes";
3
+
4
+ export const GET = createFleetInfoRoute({ site: "example" });
@@ -0,0 +1,22 @@
1
+ // RootTale 발행 웹훅 수신 — ES256 서명 검증 후 Next.js 캐시 revalidate.
2
+ // 어드민 "내 사이트 > 배포"에 https://<도메인>/api/revalidate 로 등록한다.
3
+ import { revalidatePath } from "next/cache";
4
+ import { createRevalidateRoute } from "@roottale/cms-renderer-next/routes";
5
+
6
+ function revalidateBlogPath(path: string): void {
7
+ revalidatePath(path);
8
+ // 카테고리/태그 인덱스 같은 동적 경로가 있으면 함께 무효화
9
+ // if (path === "/blog/categories") revalidatePath("/blog/categories/[category]", "page");
10
+ }
11
+
12
+ export const POST = createRevalidateRoute({
13
+ apiKey: process.env.ROOTTALE_API_KEY!,
14
+ apiBase: process.env.ROOTTALE_API_BASE,
15
+ revalidate: revalidateBlogPath,
16
+ // 홈에 최신 글 섹션이 있으면 "/" 포함 — 글 변경 시 홈도 함께 갱신
17
+ alsoRevalidate: ["/feed.xml", "/sitemap.xml", "/blog", "/"],
18
+ });
19
+
20
+ export function GET(): Response {
21
+ return new Response("Method Not Allowed", { status: 405 });
22
+ }
@@ -0,0 +1,54 @@
1
+ // 블로그 상세 페이지 — 본문은 RootTaleBlogPost(블록 JSON 렌더러)에 위임,
2
+ // 메타데이터는 어드민 SEO 패널(metaJson.seo) 값을 우선 적용.
3
+ import type { Metadata } from "next";
4
+ import { notFound } from "next/navigation";
5
+ import { RootTaleBlogPost } from "@roottale/cms-renderer-next/server";
6
+
7
+ import { getAllPosts, getPost } from "@/lib/blog";
8
+
9
+ export const revalidate = 1800;
10
+
11
+ type Props = { params: Promise<{ slug: string }> };
12
+
13
+ export async function generateStaticParams() {
14
+ const posts = await getAllPosts();
15
+ return posts.map((p) => ({ slug: p.slug }));
16
+ }
17
+
18
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
19
+ const { slug } = await params;
20
+ const post = await getPost(slug);
21
+ if (!post) return {};
22
+ const seo = post.seo;
23
+ const title = seo?.title || post.title;
24
+ const description = seo?.description || post.description;
25
+ return {
26
+ title,
27
+ description,
28
+ ...(seo?.canonical ? { alternates: { canonical: seo.canonical } } : {}),
29
+ ...(seo?.noindex ? { robots: { index: false, follow: true } } : {}),
30
+ openGraph: {
31
+ type: "article",
32
+ title,
33
+ description,
34
+ publishedTime: post.date,
35
+ ...(post.image ? { images: [{ url: seo?.ogImage || post.image, alt: title }] } : {}),
36
+ },
37
+ };
38
+ }
39
+
40
+ export default async function PostPage({ params }: Props) {
41
+ const { slug } = await params;
42
+ const post = await getPost(slug);
43
+ if (!post) notFound();
44
+
45
+ return (
46
+ <main>
47
+ <RootTaleBlogPost
48
+ apiKey={process.env.ROOTTALE_API_KEY!}
49
+ baseUrl={process.env.ROOTTALE_API_BASE}
50
+ slugOrId={post.id}
51
+ />
52
+ </main>
53
+ );
54
+ }
@@ -0,0 +1,34 @@
1
+ // 블로그 목록 페이지 — 30분 ISR은 fallback일 뿐, 정상 갱신은 발행 웹훅.
2
+ import Link from "next/link";
3
+
4
+ import { getAllPosts } from "@/lib/blog";
5
+
6
+ export const revalidate = 1800;
7
+
8
+ export const metadata = {
9
+ title: "블로그",
10
+ description: "최신 소식과 인사이트",
11
+ };
12
+
13
+ export default async function BlogPage() {
14
+ const posts = await getAllPosts();
15
+ return (
16
+ <main>
17
+ <h1>블로그</h1>
18
+ <ul>
19
+ {posts.map((post) => (
20
+ <li key={post.id}>
21
+ <Link href={`/blog/${post.slug}`}>
22
+ {post.category && <span>{post.category}</span>}
23
+ <h2>{post.title}</h2>
24
+ <p>{post.description}</p>
25
+ <time dateTime={post.date}>
26
+ {new Date(post.date).toLocaleDateString("ko-KR")}
27
+ </time>
28
+ </Link>
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ </main>
33
+ );
34
+ }
@@ -0,0 +1,15 @@
1
+ // RSS 2.0 피드 — 발행된 글 자동 포함.
2
+ import { createFeedRoute } from "@roottale/cms-renderer-next/routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ const SITE_URL =
7
+ process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") || "https://example.com";
8
+
9
+ export const GET = createFeedRoute({
10
+ apiKey: process.env.ROOTTALE_API_KEY!,
11
+ apiBase: process.env.ROOTTALE_API_BASE,
12
+ siteUrl: SITE_URL,
13
+ title: "예시 사이트 블로그",
14
+ description: "최신 소식과 인사이트",
15
+ });
@@ -0,0 +1,19 @@
1
+ // 사이트맵 — 정적 경로 + 발행 글 URL 자동 포함.
2
+ import { createSitemap } from "@roottale/cms-renderer-next/routes";
3
+
4
+ const SITE_URL =
5
+ process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") || "https://example.com";
6
+
7
+ export default createSitemap(
8
+ {
9
+ apiKey: process.env.ROOTTALE_API_KEY!,
10
+ apiBase: process.env.ROOTTALE_API_BASE,
11
+ siteUrl: SITE_URL,
12
+ title: "예시 사이트",
13
+ },
14
+ [
15
+ { url: SITE_URL, changeFrequency: "weekly", priority: 1.0 },
16
+ { url: `${SITE_URL}/blog`, changeFrequency: "weekly", priority: 0.7 },
17
+ { url: `${SITE_URL}/contact`, changeFrequency: "monthly", priority: 0.9 },
18
+ ],
19
+ );
@@ -0,0 +1,50 @@
1
+ // 상담문의 폼 → RootTale CRM. 블로그와 같은 API 키로 테넌트가 식별된다.
2
+ // 클라이언트 폼에서 useActionState(submitContact, { status: "idle" })로 연결.
3
+ "use server";
4
+
5
+ import { submitInquiry } from "@roottale/cms-client/server";
6
+
7
+ export interface ContactState {
8
+ status: "idle" | "success" | "error";
9
+ message?: string;
10
+ errors?: Partial<Record<"name" | "phone" | "privacyConsent", string>>;
11
+ }
12
+
13
+ export async function submitContact(
14
+ _prevState: ContactState,
15
+ formData: FormData,
16
+ ): Promise<ContactState> {
17
+ const name = (formData.get("name") as string | null)?.trim() ?? "";
18
+ const phone = (formData.get("phone") as string | null)?.trim() ?? "";
19
+ const field = (formData.get("field") as string | null)?.trim() ?? "";
20
+ const message = (formData.get("message") as string | null)?.trim() ?? "";
21
+ const privacyConsent = formData.get("privacyConsent") !== null;
22
+
23
+ const errors: ContactState["errors"] = {};
24
+ if (!name) errors.name = "이름을 입력해주세요.";
25
+ if (!phone || phone.length < 7) errors.phone = "연락처를 입력해주세요.";
26
+ if (!privacyConsent) errors.privacyConsent = "개인정보 수집·이용에 동의해주세요.";
27
+ if (Object.keys(errors).length > 0) {
28
+ return { status: "error", message: "필수 항목을 입력해주세요.", errors };
29
+ }
30
+
31
+ const result = await submitInquiry({
32
+ apiKey: process.env.ROOTTALE_API_KEY!,
33
+ baseUrl: process.env.ROOTTALE_API_BASE,
34
+ fields: {
35
+ vertical: "consulting", // 사이트 업종에 맞게: consulting | medical | tax | legal
36
+ contactName: name,
37
+ businessName: name, // 사업체명 미수집 폼이면 이름으로 대체
38
+ email: `noemail-${Date.now()}@example.invalid`, // 이메일 미수집 폼 placeholder
39
+ phone,
40
+ consultationField: field || undefined,
41
+ message: message || undefined,
42
+ privacyConsent: true, // 사용자가 명시 동의한 경우에만
43
+ },
44
+ });
45
+
46
+ if (result.ok) {
47
+ return { status: "success", message: "상담 문의가 접수되었습니다." };
48
+ }
49
+ return { status: "error", message: result.message };
50
+ }
@@ -0,0 +1,72 @@
1
+ // RootTale CMS 블로그 데이터 레이어 — 서버 전용.
2
+ // 사이트 UI에 맞는 메타 형태로 변환하는 wrapper. API 키는 env에서만 읽는다.
3
+ import {
4
+ fetchPosts,
5
+ fetchPost,
6
+ type CmsPostContent,
7
+ } from "@roottale/cms-client/server";
8
+
9
+ function getApiKey(): string {
10
+ const key = process.env.ROOTTALE_API_KEY;
11
+ if (!key) throw new Error("ROOTTALE_API_KEY is not set");
12
+ return key;
13
+ }
14
+
15
+ const baseUrl = process.env.ROOTTALE_API_BASE;
16
+
17
+ export interface BlogPostMeta {
18
+ id: string;
19
+ slug: string;
20
+ title: string;
21
+ description: string;
22
+ date: string; // publishedAt ISO
23
+ category: string;
24
+ tags: { name: string; slug: string }[];
25
+ image: string | null;
26
+ authorName?: string;
27
+ seo?: {
28
+ title?: string;
29
+ description?: string;
30
+ canonical?: string;
31
+ ogImage?: string;
32
+ noindex?: boolean;
33
+ };
34
+ }
35
+
36
+ function toMeta(post: CmsPostContent): BlogPostMeta {
37
+ const category = post.terms?.find((t) => t.taxonomy === "category");
38
+ const meta = (post.metaJson ?? {}) as Record<string, unknown>;
39
+ return {
40
+ id: post.id,
41
+ slug: post.slug,
42
+ title: post.title,
43
+ description: post.excerpt ?? "",
44
+ date: post.publishedAt ?? "",
45
+ category: category?.name ?? "",
46
+ tags:
47
+ post.terms
48
+ ?.filter((t) => t.taxonomy === "tag")
49
+ .map((t) => ({ name: t.name, slug: t.slug })) ?? [],
50
+ image: post.featuredImageUrl ?? null,
51
+ authorName: post.authorName ?? undefined,
52
+ seo: meta.seo as BlogPostMeta["seo"],
53
+ };
54
+ }
55
+
56
+ export async function getAllPosts(): Promise<BlogPostMeta[]> {
57
+ const page = await fetchPosts({
58
+ apiKey: getApiKey(),
59
+ baseUrl,
60
+ type: "post",
61
+ limit: 100,
62
+ });
63
+ return page.items.map(toMeta);
64
+ }
65
+
66
+ export async function getPost(
67
+ slug: string,
68
+ ): Promise<(BlogPostMeta & { bodyJson: unknown }) | null> {
69
+ const post = await fetchPost({ apiKey: getApiKey(), baseUrl, slugOrId: slug });
70
+ if (!post) return null;
71
+ return { ...toMeta(post), bodyJson: post.bodyJson };
72
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@roottale/cms-mcp",
3
+ "version": "0.22.0",
4
+ "type": "module",
5
+ "description": "RootTale CMS integration MCP server — bundled integration docs, Next.js example code, and public API lookup tools. Run with: npx @roottale/cms-mcp",
6
+ "bin": {
7
+ "roottale-cms-mcp": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist/",
12
+ "docs/",
13
+ "examples/",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "type-check": "tsc --noEmit",
20
+ "test": "vitest run --passWithNoTests",
21
+ "start": "node dist/index.js"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.0.0",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.7.0",
31
+ "vitest": "^2.1.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "keywords": [
37
+ "roottale",
38
+ "cms",
39
+ "mcp",
40
+ "integration",
41
+ "docs"
42
+ ],
43
+ "license": "UNLICENSED",
44
+ "homepage": "https://github.com/RootTale/roottale-platform/tree/main/packages/cms-mcp",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/RootTale/roottale-platform.git",
48
+ "directory": "packages/cms-mcp"
49
+ }
50
+ }