@roottale/cms-mcp 0.53.1 → 0.56.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,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
+ }
@@ -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";
@@ -10,6 +11,7 @@ import {
10
11
  type ArchivePromotion,
11
12
  fetchBlogArchivePosts,
12
13
  resolveCategoryPromotion,
14
+ storedPostPath,
13
15
  } from "@roottale/cms-renderer-next/routes";
14
16
 
15
17
  function getApiKey(): string {
@@ -23,6 +25,11 @@ const baseUrl = process.env.ROOTTALE_API_BASE;
23
25
  export interface BlogPostMeta {
24
26
  id: string;
25
27
  slug: string;
28
+ /**
29
+ * 플랫폼이 저장한 정규 공개 경로(ADR-0105, 예 `/blog/my-post`). 링크·canonical·
30
+ * JSON-LD 는 이 값을 읽고, 없을 때(구 서버·주소 규칙 없는 유형)만 `/blog/{slug}` 로 폴백.
31
+ */
32
+ path: string | null;
26
33
  title: string;
27
34
  description: string;
28
35
  date: string; // publishedAt ISO
@@ -31,6 +38,7 @@ export interface BlogPostMeta {
31
38
  categorySlug: string; // "" = 카테고리 미지정 — /blog/categories/{slug} 허브 필터링용
32
39
  tags: { name: string; slug: string }[];
33
40
  image: string | null;
41
+ authorProfileId?: string | null;
34
42
  authorName?: string;
35
43
  authorSlug?: string | null; // /blog/author/{slug} — 미발급 작가는 null
36
44
  seo?: {
@@ -49,6 +57,7 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
49
57
  return {
50
58
  id: post.id,
51
59
  slug: post.slug,
60
+ path: storedPostPath(post),
52
61
  title: post.title,
53
62
  description: post.excerpt ?? "",
54
63
  date: post.publishedAt ?? "",
@@ -60,6 +69,7 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
60
69
  ?.filter((t) => t.taxonomy === "tag")
61
70
  .map((t) => ({ name: t.name, slug: t.slug })) ?? [],
62
71
  image: post.featuredImageUrl ?? null,
72
+ authorProfileId: post.authorProfileId,
63
73
  authorName: post.authorName ?? undefined,
64
74
  authorSlug: post.authorSlug,
65
75
  seo: meta.seo as BlogPostMeta["seo"],
@@ -96,6 +106,25 @@ export async function getPostsByCategory(
96
106
  return posts.filter((p) => p.categorySlug === categorySlug);
97
107
  }
98
108
 
109
+ // 카테고리 허브의 소개문·검색 문구·공유 이미지는 카테고리 집계 응답에 함께 옵니다.
110
+ // 구버전 API와 이미지 미설정을 고려해 nullable 값으로 다룹니다.
111
+ export async function getCategoryArchiveMeta(categorySlug: string) {
112
+ const { categories } = await fetchCategoryCounts({
113
+ apiKey: getApiKey(),
114
+ baseUrl,
115
+ excludeCollections: true,
116
+ });
117
+ const category = categories.find((item) => item.slug === categorySlug);
118
+ if (!category) return null;
119
+ return {
120
+ title: category.seoTitle ?? `${category.name} 글 모음`,
121
+ description: category.seoDescription ?? category.description,
122
+ image: category.imageUrl
123
+ ? { url: category.imageUrl, alt: category.imageAlt ?? category.name }
124
+ : null,
125
+ };
126
+ }
127
+
99
128
  // 색인 위생 — 어드민 "내 사이트 > 카테고리 > 검색에 이 분류 페이지 노출" 을 켠
100
129
  // 분류인지. 켜지 않은 분류의 모음 페이지는 noindex + 사이트맵 제외가 됩니다
101
130
  // (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
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@roottale/cms-mcp",
3
- "version": "0.53.1",
3
+ "version": "0.56.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
  },