---
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`·동적 경로 콜백 누락 |
