@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.
- package/CHANGELOG.md +74 -0
- package/README.md +17 -9
- package/dist/index.js +660 -45
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +70 -9
- package/docs/blog.md +108 -1
- package/docs/collections.md +35 -1
- package/docs/content-models-and-exposures.md +329 -32
- package/docs/custom-redirects.md +5 -0
- package/docs/getting-started.md +20 -10
- package/docs/revalidation-webhooks.md +7 -1
- package/docs/seo.md +66 -0
- package/docs/theme-and-settings.md +3 -0
- package/examples/nextjs/app/blog/[slug]/page.tsx +4 -2
- package/examples/nextjs/app/blog/categories/[slug]/page.tsx +1 -1
- package/examples/nextjs/app/blog/page.tsx +1 -1
- package/examples/nextjs/app/preview/post/[id]/page.tsx +70 -0
- package/examples/nextjs/lib/blog.ts +29 -0
- package/examples/nextjs/lib/content-models.ts +12 -0
- package/package.json +2 -2
|
@@ -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.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "RootTale CMS MCP server and CLI for
|
|
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
|
},
|