@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
130 lines (100 loc) • 4.88 kB
Markdown
---
title: 발행 웹훅 (캐시 자동 갱신)
description: 글 발행/수정 시 사이트 캐시를 near-real-time으로 갱신하는 웹훅 설정
---
# 발행 웹훅 — 캐시 자동 갱신
어드민에서 글을 발행/수정/삭제하면 RootTale이 고객 사이트의 revalidate
엔드포인트로 **ES256 서명된 웹훅**을 보냅니다. 사이트는 서명을 검증하고
`revalidatePath`를 호출해 즉시 갱신합니다.
- 별도 webhook secret을 보관할 필요가 없습니다 — 검증은 사이트 스코프 API
키로 JWKS 공개키를 가져와 수행합니다.
- ISR `revalidate = 1800` 같은 시간 기반 설정은 **fallback**입니다. 정상
경로는 웹훅입니다.
## 1. revalidate 라우트 추가 (Next.js)
```ts
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { createRevalidateRoute } from "@roottale/cms-renderer-next/routes";
export const POST = createRevalidateRoute({
apiKey: process.env.ROOTTALE_API_KEY!,
apiBase: process.env.ROOTTALE_API_BASE,
revalidate: revalidatePath,
// 글 변경 시 함께 갱신할 추가 경로 (기본: /feed.xml, /sitemap.xml, /blog)
alsoRevalidate: ["/feed.xml", "/sitemap.xml", "/blog", "/"],
});
export function GET(): Response {
return new Response("Method Not Allowed", { status: 405 });
}
```
블로그가 `/blog`가 아닌 경로면 `blogBasePath: "/insights"` 옵션을 추가하세요.
카테고리/태그 인덱스 같은 동적 경로가 있다면 `revalidate` 콜백을 확장합니다:
```ts
function revalidateBlogPath(path: string): void {
revalidatePath(path);
if (path === "/blog/categories") revalidatePath("/blog/categories/[category]", "page");
if (path === "/blog/tags") revalidatePath("/blog/tags/[tag]", "page");
}
```
## 2. 어드민에 웹훅 URL 등록
1. 어드민 **내 사이트 > (사이트 선택)** 페이지로 이동
2. "글 발행 후 사이트 자동 갱신" 카드에서:
- **자동 갱신 URL**: `https://<사이트 도메인>/api/revalidate`
- **활성화** 체크
3. 저장 — ES256 키페어가 자동 발급됩니다 (고객 측 보관 항목 없음)
URL을 비우고 저장하면 웹훅이 비활성화됩니다.
## 웹훅 발송 트리거
- 게시물 생성/발행/수정/삭제/발행 취소
- 게시물의 카테고리·태그 변경 (블로그 카드의 카테고리 라벨이 바뀌므로)
- 분류(taxonomy) 용어 삭제 (해당 용어를 참조하던 발행 글 전부)
- 블로그 표시 설정 변경 (TOC, 작성자/발행일, 작성자 카드)
- 디자인 토큰 변경
- 수동 revalidation API 호출
## 캐시 무효화 규칙
수신 측은 다음을 보장해야 합니다 (`createRevalidateRoute`가 기본 처리):
- 모든 서명된 이벤트에서 블로그 목록(`/blog`) revalidate — 글 본문이 안
바뀌어도 카드 메타(카테고리 라벨 등)가 바뀔 수 있음
- 상세 페이지는 현재 slug + payload의 `paths` 힌트 경로 모두 revalidate
- 홈에 최신 글 섹션이 있으면 `alsoRevalidate`에 `/` 포함
## 저수준 검증 — verifyRootTaleWebhook
`createRevalidateRoute`를 못 쓰는 환경(다른 프레임워크 등)은
`@roottale/cms-client/webhook`으로 직접 검증합니다:
```ts
import { verifyRootTaleWebhook } from "@roottale/cms-client/webhook";
export async function POST(request: Request) {
const rawBody = await request.text(); // 반드시 파싱 전 raw로 검증
const result = await verifyRootTaleWebhook({
rawBody,
headers: request.headers,
apiKey: process.env.ROOTTALE_API_KEY!,
});
if (!result.ok) {
return Response.json({ reason: result.reason }, { status: 401 });
}
// result.event: "post.published" | "post.updated" | "post.deleted"
// result.payload.paths: 갱신할 root-relative 경로 배열
return Response.json({ ok: true });
}
```
실패 `reason` 값: `missing_signature`, `invalid_signature`, `expired`,
`body_hash_mismatch`, `timestamp_out_of_window`, `replay_seen` 등.
옵션으로 `expectedSiteId`(멀티 사이트 하드닝), `consumeJti`(replay 방지 저장소),
`timestampWindowSec`(기본 300초)을 지정할 수 있습니다.
## 수동 revalidation API
배포 직후 등 강제 갱신이 필요할 때:
```http
POST https://api.roottale.com/v1/cms/revalidate
Authorization: Bearer rtlk_cust_...
Content-Type: application/json
{
"event": "post.updated",
"paths": ["/blog", "/blog/my-post"],
"slug": "my-post"
}
```
## 트러블슈팅
| 증상 | 확인 |
|---|---|
| 발행해도 사이트 미반영 | 어드민의 자동 갱신 URL·활성화 체크, 배포 도메인 일치 여부 |
| 401 `invalid_signature` | `ROOTTALE_API_KEY`가 해당 사이트 스코프 키인지 |
| 401 `timestamp_out_of_window` | 서버 시계 동기화 (NTP) |
| 일부 페이지만 갱신 | `alsoRevalidate`·동적 경로 콜백 누락 |