enable-shopify-menus

작성자: vercel

하드코딩된 내비게이션 및 푸터 메뉴를 Shopify 기반 메뉴로 교체합니다.

npx skills add https://github.com/vercel/shop --skill enable-shopify-menus

Enable Shopify Menus

By default, the storefront's nav renders an inline const items: MenuItem[] = [...] in Nav (components/nav/index.tsx), and the footer renders an empty const items: MenuItem[] = [] in Footer (components/footer/index.tsx). QuickLinks, MobileMenu, and Footer's FooterMenu already render MenuItem[] from lib/menu/types.ts, up to three levels deep in the nav and two in the footer.

This skill adds a cached Shopify menu read and points the selected components at it with the inline items as fallback. Shopify sends no webhook when a menu changes, so menus stay cached until the storefront's protected POST /api/revalidate endpoint is called with the menus tag.

Before you start

Ask the user two questions in order:

1. Which menus do you want to fetch from Shopify?

  • Nav menu — replaces the hardcoded items used by the desktop quick links and mobile sheet.
  • Footer menu — adds footer columns.
  • Both

2. What are the Shopify menu handles?

Ask for each selected menu. Defaults: main-menu for nav, footer for footer. Every Shopify store starts with both.

Wait for the user to answer before proceeding.


1. Add the menu operation

Create lib/shopify/operations/menu/server.ts. Shopify returns absolute item URLs on the store's myshopify or primary domain, sometimes with a Markets locale prefix; the operation turns those into storefront paths and leaves external links alone.

import { gql } from "@shopify/hydrogen";

import { shopConfig } from "@/lib/config";
import type { CommerceLocale } from "@/lib/config/types";
import type { MenuItem } from "@/lib/menu/types";
import { assertStorefrontOk } from "@/lib/shopify/errors/server";
import { storefront } from "@/lib/shopify/storefront/server";

const GET_MENU_QUERY = gql(`#graphql
  query getMenu($handle: String!, $country: CountryCode, $language: LanguageCode) @inContext(country: $country, language: $language) {
    shop {
      primaryDomain {
        url
      }
    }
    menu(handle: $handle) {
      items {
        id
        title
        url
        items {
          id
          title
          url
          items {
            id
            title
            url
          }
        }
      }
    }
  }
`);

function toStorefrontUrl(url: string | null | undefined, internalHosts: string[]): string {
  if (!url) return "/";
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    return url;
  }
  if (!internalHosts.includes(parsed.hostname)) return url;
  const path = parsed.pathname.replace(/^\/[a-z]{2}(?:-[a-z]{2,4})?(?=\/|$)/i, "") || "/";
  return `${path}${parsed.search}${parsed.hash}`;
}

export async function fetchMenu({
  handle,
  locale = shopConfig.localization,
}: {
  handle: string;
  locale?: CommerceLocale;
}): Promise<MenuItem[] | null> {
  const response = await storefront.request(GET_MENU_QUERY, { locale, variables: { handle } });
  assertStorefrontOk(response, "getMenu");

  const menu = response.data.menu;
  if (!menu || menu.items.length === 0) return null;

  const internalHosts = [
    process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN ?? "",
    new URL(response.data.shop.primaryDomain.url).hostname,
  ];
  const link = ({ id, title, url }: { id: string; title: string; url?: string | null }) => ({
    id,
    title,
    url: toStorefrontUrl(url, internalHosts),
  });

  return menu.items.map((item) => ({
    ...link(item),
    items: item.items.map((child) => ({
      ...link(child),
      items: child.items.map((leaf) => ({ ...link(leaf), items: [] })),
    })),
  }));
}

If you add fields, validate the document with the Shopify AI Toolkit first.

2. Cache the menu

Create lib/menu/server.ts:

import { cacheLife, cacheTag } from "next/cache";

import type { CommerceLocale } from "@/lib/config/types";
import type { MenuItem } from "@/lib/menu/types";
import { fetchMenu } from "@/lib/shopify/operations/menu/server";

export async function getMenu(params: {
  handle: string;
  locale?: CommerceLocale;
}): Promise<MenuItem[] | null> {
  "use cache: remote";
  cacheLife("max");
  cacheTag("menus");

  return fetchMenu(params);
}

3. Refresh menus after edits

The storefront's app/api/revalidate/route.ts refreshes the cache tags it is given, including menus, and returns 404 until REVALIDATE_SECRET is set. If the storefront has no app/api/revalidate/route.ts, take it from the template with the update-shop skill first; do not add a menu-only endpoint.

Tell the user to set REVALIDATE_SECRET in the deployment's environment variables, then refresh menus after editing them in Shopify admin:

curl -X POST "https://your-store.example.com/api/revalidate?tag=menus" \
  -H "Authorization: Bearer $REVALIDATE_SECRET"

Any scheduler or automation that can send that request can refresh menus on a schedule.

4. Wire the nav menu

Skip this section if the user did not select the nav menu.

Edit components/nav/index.tsx. Make Nav async (preserving any existing props in a customized storefront), import getMenu, and let the Shopify menu take precedence over the inline default:

import { getMenu } from "@/lib/menu/server";
const items: MenuItem[] = (await getMenu({ handle: "NAV_HANDLE" }).catch(() => null)) ?? [
  { id: "default-nav-shop", items: [], title: "Shop", url: "/collections/all" },
];

Replace "NAV_HANDLE" with the handle the user provided. QuickLinks and MobileMenu need no changes.

5. Wire the footer menu

Skip this section if the user did not select the footer menu.

Edit components/footer/index.tsx. Import getMenu and replace const items: MenuItem[] = []; with:

const items: MenuItem[] = (await getMenu({ handle: "FOOTER_HANDLE" }).catch(() => null)) ?? [];

Replace "FOOTER_HANDLE" with the handle the user provided. FooterMenu needs no changes; an empty or missing menu hides the columns.

Verification

  • The nav and footer render the Shopify menu items, with internal links as storefront paths such as /collections/all, and external links unchanged.
  • A missing or empty menu falls back to the inline items.
  • POST /api/revalidate?tag=menus returns 404 without REVALIDATE_SECRET, 401 with a wrong token, and 200 with {"revalidated":["menus"]} with the right one.
  • Lint, typecheck, and build pass.

Guardrails

  • Keep the inline fallbacks so a missing menu or Storefront error never leaves a blank nav.
  • Keep cacheLife("max") with the menus tag and refresh through POST /api/revalidate; do not shorten the cache life to pick up edits.
  • In a localized or Markets-enabled store, pass the validated commerce locale to getMenu from each caller; do not read locale from request headers inside the cached read.
  • New fallback labels belong beside the consuming component; Shopify menu translations and storefront fallback copy are separate.

vercel의 다른 스킬

vercel
vercel
로컬 개발 및 테스트를 위한 Vercel REST API 에뮬레이션입니다. 사용자가 로컬에서 Vercel API 엔드포인트와 상호작용하거나 Vercel 통합을 테스트해야 할 때 사용합니다.
cron-jobs
vercel
Vercel Cron Jobs 구성 및 모범 사례. vercel.json에서 예약된 작업을 추가, 편집 또는 디버깅할 때 사용합니다.
codegen
vercel
json-render을 위한 코드 생성 유틸리티입니다. UI 명세서에서 코드를 생성하거나, 사용자 정의 코드 내보내기를 구축하거나, 명세서를 탐색하거나, props를 직렬화할 때 사용합니다.
next-best-practice
vercel
Next.js 모범 사례 - 파일 규칙, RSC 경계, 데이터 패턴, 비동기 API, 메타데이터, 오류 처리, 라우트 핸들러, 이미지/폰트 최적화,…
benchmark-sandbox
vercel
Vercel Sandbox에서 vercel-plugin eval 시나리오를 로컬 WezTerm 패널 대신 실행합니다. Claude Code와 플러그인이 사전 설치된 임시 마이크로VM을 프로비저닝합니다.
write-guide
vercel
점진적인 예제를 통해 실제 사용 사례를 가르치는 기술 가이드를 제작합니다. 개념은 독자가 필요로 할 때만 소개됩니다.
benchmark-testing
vercel
벤치마크 테스트 프로젝트를 생성하고 실행하여 실제 시나리오에서 vercel-plugin 스킬 인젝션을 테스트합니다. 격리된 디렉토리를 설정하고, 설치하며…
ai-gateway
vercel
Vercel AI Gateway 전문가 안내. 모델 라우팅, 제공업체 장애 조치, 비용 추적 또는 통합된 방식을 통해 여러 AI 제공업체를 관리할 때 사용합니다.