@roottale/cms-mcp
Version:
RootTale CMS integration MCP server — bundled integration docs, Next.js example code, and public API lookup tools. Run with: npx @roottale/cms-mcp
200 lines (161 loc) • 6.19 kB
Markdown
---
title: 블로그 연동
description: 블로그 목록/상세 페이지 구현 — 컴포넌트 빠른 경로와 직접 fetch 커스텀 경로
---
두 가지 경로가 있습니다:
- **빠른 경로** — `@roottale/cms-renderer-next`의 RSC 컴포넌트 사용. 데이터
fetch + 렌더링까지 한 번에.
- **커스텀 경로** — `@roottale/cms-client`로 raw 데이터를 가져와 자체 UI로
렌더링. 디자인을 완전히 통제할 때.
본문 렌더링은 두 경로 모두 `RootTaleBlogPost`(블록 JSON → React)를 쓰는 것을
권장합니다. 본문 JSON 스키마를 직접 파싱하지 마세요.
```tsx
// app/blog/page.tsx
import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
export const revalidate = 1800; // 30분 fallback — 실시간 갱신은 웹훅이 담당
export default function BlogPage() {
return (
<RootTaleBlogList
apiKey={process.env.ROOTTALE_API_KEY!}
baseUrl={process.env.ROOTTALE_API_BASE}
limit={20}
showCategoryFilter
postHref={(post) => `/blog/${post.slug}`}
/>
);
}
```
```tsx
// app/blog/[slug]/page.tsx
import { RootTaleBlogPost } from "@roottale/cms-renderer-next/server";
export const revalidate = 1800;
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // Next.js 15+ async params
return (
<RootTaleBlogPost
apiKey={process.env.ROOTTALE_API_KEY!}
baseUrl={process.env.ROOTTALE_API_BASE}
slugOrId={slug}
showTableOfContents
tableOfContentsTitle="목차"
/>
);
}
```
목차(ToC)·작성자 카드·발행일 표시는 어드민의 블로그 표시 설정으로도 제어됩니다
(`theme-and-settings.md` 참고).
어드민의 고정 페이지(`type: "page"`)는 `RootTalePage`로 렌더링합니다 — 블로그
크롬(날짜·작성자·작성자 카드) 없이 제목+본문만 출력합니다 (renderer-next
0.22.0+):
```tsx
// app/about/page.tsx
import { RootTalePage } from "@roottale/cms-renderer-next/server";
export const revalidate = 1800;
export default function AboutPage() {
return (
<RootTalePage
apiKey={process.env.ROOTTALE_API_KEY!}
baseUrl={process.env.ROOTTALE_API_BASE}
slugOrId="about"
// showTitle={false} — 페이지 제목을 직접 마크업할 때
/>
);
}
```
```ts
// lib/blog.ts
import { fetchPosts, fetchPost, type CmsPostContent } from "@roottale/cms-client/server";
export async function getAllPosts() {
const page = await fetchPosts({
apiKey: process.env.ROOTTALE_API_KEY!,
baseUrl: process.env.ROOTTALE_API_BASE,
type: "post",
limit: 100,
});
return page.items; // CmsPostContent[]
// page.hasMore / page.nextCursor 로 커서 페이지네이션
}
export async function getPost(slug: string) {
return fetchPost({
apiKey: process.env.ROOTTALE_API_KEY!,
baseUrl: process.env.ROOTTALE_API_BASE,
slugOrId: slug, // slug 또는 UUID — 404면 null 반환
});
}
```
`CmsPostContent` 주요 필드:
| 필드 | 설명 |
|---|---|
| `id`, `slug`, `title` | 식별자·제목 |
| `excerpt` | 요약 (목록 카드용) |
| `publishedAt` | 발행 시각 (ISO) |
| `bodyJson` | 본문 블록 JSON — `RootTaleBlogPost` 또는 `renderBlocks`로 렌더 |
| `terms` | 분류 용어 배열 (`taxonomy: "category" \| "tag"`, `name`, `slug`) |
| `featuredImageUrl` | 대표 이미지 |
| `authorName` | 작성자 표시명 |
| `metaJson` | 부가 메타 — `metaJson.seo`에 SEO 오버라이드 |
```tsx
// app/blog/[slug]/page.tsx (커스텀 UI 버전)
import type { Metadata } from "next";
import { getAllPosts, getPost } from "@/lib/blog";
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return {};
// 어드민 글 에디터의 SEO 패널 값(metaJson.seo)을 우선 적용
const seo = (post.metaJson as { seo?: Record<string, string | boolean> })?.seo;
return {
title: (seo?.title as string) || post.title,
description: (seo?.description as string) || post.excerpt,
...(seo?.noindex || seo?.nofollow
? { robots: { index: !seo?.noindex, follow: !seo?.nofollow } }
: {}),
};
}
```
SEO 오버라이드 필드: `title`, `description`, `canonical`, `ogImage`,
`noindex`, `nofollow`.
어드민에서 글 slug를 바꿔도 API는 **옛 slug로 글을 찾아 현재 slug로
응답**합니다(slug history fallback). 페이지에서 요청 slug와 응답 slug가
다르면 301로 보내야 검색엔진 순위가 새 URL로 승계됩니다:
```tsx
// app/blog/[slug]/page.tsx
import { notFound, permanentRedirect } from "next/navigation";
import { postRedirectPath } from "@roottale/cms-renderer-next/routes";
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
const redirect = postRedirectPath(post, slug); // 기본 basePath "/blog"
if (redirect) permanentRedirect(redirect);
// ... 렌더
}
```
`generateMetadata`는 redirect 페이지에서 실행돼도 무방하지만, canonical을
직접 계산한다면 `post.slug`(현재 slug) 기준으로 계산하세요.
- 페이지에 `export const revalidate = 1800` (30분) — **fallback일 뿐**입니다.
- 정상 동작은 발행 웹훅이 즉시 revalidate 하는 것 → `revalidation-webhooks.md`
를 반드시 함께 설정하세요.
- 홈 화면에 최신 글 섹션을 둔다면 홈도 웹훅의 `alsoRevalidate`에 포함하세요.
완전한 동작 예시는 MCP tool `readRootTaleNextjsExampleCode`로 확인할 수 있습니다.