본문 바로가기

메뉴 (네비게이션) 연동

어드민 "디자인 > 메뉴"에서 관리하는 네비게이션을 헤더/푸터에 렌더

어드민(admin.roottale.com)⁠의 디자인 > 메뉴에서 저장한 네비게이션 트리를 사이트의 헤더·푸터에 렌더합니다. 고객이 직접 메뉴 항목(이름·주소·순서·하위 항목)⁠을 바꿀 수 있어 코드 수정 없이 네비가 갱신됩니다.

  • 메뉴는 위치 핸들(slug) 로 구분합니다 — 관례: primary(헤더), footer(푸터).
  • 항목은 깊이 2 트리 (상위 + 드롭다운 1단).
  • url 상대 경로(/about) 또는 절대 http(s) URL — 서버가 위험 스킴을 제거한 안전한 값만 내려줍니다.
  • 메뉴 저장 시 발행 웹훅(post.updated, paths: ["/"])⁠이 발송되므로 웹훅 수신 라우트(revalidation-webhooks.md)를 설정했다면 몇 초 내 반영됩니다.

서버 컴포넌트에서 메뉴 가져오기

// components/site-nav.tsx (Server Component)
import Link from "next/link";
import { MENUS_CACHE_TAG, fetchMenu } from "@roottale/cms-client/server";

export async function SiteNav() {
  const menu = await fetchMenu({
    apiKey: process.env.ROOTTALE_API_KEY!,
    slug: "primary",
    // 캐시 이름표 — 어드민에서 메뉴를 저장하면 웹훅 수신 라우트가 이 이름표로
    // 이 조회의 캐시를 지웁니다 (revalidation-webhooks.md §1 "설정 저장").
    tags: [MENUS_CACHE_TAG],
  });

  // 메뉴 미설정(null)이면 자체 fallback 네비를 렌더하세요.
  if (!menu) {
    return (
      <nav>
        <Link href="/blog">블로그</Link>
      </nav>
    );
  }

  return (
    <nav>
      {menu.items.map((item) => (
        <div key={item.id}>
          <Link
            href={item.url}
            {...(item.newTab
              ? { target: "_blank", rel: "noopener" }
              : {})}
          >
            {item.label}
          </Link>
          {item.children?.length ? (
            <div>
              {item.children.map((child) => (
                <Link key={child.id} href={child.url}>
                  {child.label}
                </Link>
              ))}
            </div>
          ) : null}
        </div>
      ))}
    </nav>
  );
}

fetchMenu 미존재/구 서버의 404 를 null 돌려주므로(fail-soft) 항상 fallback 분기를 두세요. 전체 메뉴 목록이 필요하면 fetchMenus({ apiKey, tags: [MENUS_CACHE_TAG] }) — 두 함수가 같은 이름표를 씁니다(메뉴를 저장하면 어느 쪽 캐시든 함께 낡기 때문).

타입

interface RootTaleMenu {
  id: string;
  name: string;        // "헤더 메뉴"
  slug: string;        // "primary" | "footer" | ...
  items: RootTaleMenuItem[];
  updatedAt: string | null;
}

interface RootTaleMenuItem {
  id: string;
  label: string;
  url: string;         // "/about" 또는 "https://..."
  newTab?: boolean;    // target="_blank" rel="noopener" 로 렌더
  children?: RootTaleMenuItem[];
}

캐싱

메뉴 응답은 5분 캐시(+SWR)⁠로 내려갑니다. 페이지가 ISR 이라면 발행 웹훅이 / revalidate 할 때 함께 갱신되므로 별도 처리 없이 near-real-time 입니다.

raw HTTP

JS 외 스택은 GET /v1/cms/public/menus/{slug} 직접 호출하세요 — api-reference.md 참고.