UNPKG

@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
--- 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} — 페이지 제목을 직접 마크업할 때 /> ); } ``` ## 커스텀 경로 — 직접 fetch ```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 변경 시 301 리다이렉트 (필수 권장) 어드민에서 글 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`로 확인할 수 있습니다.