mcpField
V
vtex-io-rootpath

vtex-io-rootpath

github.com/vtex開発47743 回インストール2 週間前に更新
リポジトリ

vtex-io-rootpath について

マルチバインディングの VTEX IO アプリで、URL やアセットにバインディングのパス接頭辞を適用するための開発スキルです。バックエンドとフロントエンドでの rootPath の扱い、正規化、URL やキャッシュの確認方法を説明します。

主な機能

  1. 01rootPath が必要なマルチバインディングの判定
  2. 02バックエンドヘッダーからの rootPath 取得と正規化
  3. 03useRuntime().rootPath によるフロントエンド接頭辞の取得
  4. 04生成 URL へのバインディングパスの付与
  5. 05キャッシュを区別する Vary ヘッダーの設定

活用シーン

  1. 01マルチバインディング店舗のリンク切れや 404 の修正
  2. 02接頭辞付き sitemap と canonical URL の生成
  3. 03サブパス型バインディングでのアセットパス対応
  4. 04固定ロケールパスや二重スラッシュの点検

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vtex/skills --skill vtex-io-rootpath

インストール後、Claude Code で/vtex-io-rootpathを入力して読み込まれたか確認します。

詳細

マルチバインディングストア向けの VTEX IO rootPath

このスキルを使用する場面

マルチバインディング構成のストアで動作する必要がある VTEX IO アプリを構築するときに、このスキルを使用します。通常は、複数のバインディングがパスプレフィックス(例:store.com/us/、store.com/br/、store.com/mx/)付きの単一ドメインを共有する越境ストアが該当します。

  • バインディングのパスプレフィックスを含める必要がある URL(リンク、リダイレクト、API エンドポイント、正規 URL)をアプリが生成する
  • ストアがサブパスのバインディングを使用すると壊れる アセット(画像、スクリプト、スタイルシート)をアプリが読み込む
  • サイトマップ、正規リンク、バインディング間参照の URL を構築するために、バックエンドのルートが必要になる
  • マルチバインディングストアでのみ発生し、シングルバインディングでは問題なく動作する 404 や 誤ったリンクをデバッグする

以下の場合、このスキルは使用しないでください。

  • ロケールごとに専用ドメインがあるシングルバインディングストア(パスプレフィックスが不要)
  • 一般的な IO バックエンドのパターン(vtex-io-service-apps を使用してください)
  • CDN/エッジキャッシュの設定(vtex-io-service-paths-and-cdn を使用してください)

判断ルール

  • 単一ドメインのマルチバインディング(例:store.com/us/、store.com/br/)→ rootPath が必須です。生成するすべての URL に、バインディングのルートパスをプレフィックスとして付ける必要があります。
  • 複数ドメインのシングルバインディング(例:store.us、store.com.br)→ rootPath は通常、空または / です。プレフィックスを付けなくても URL は動作しますが、コードでは引き続き rootPath を適切に処理する必要があります(空でなければ使用し、空なら省略します)。
  • バックエンド(Node) → プラットフォームは、バインディングのパスプレフィックスを x-vtex-root-path リクエストヘッダーで送信します。アプリには、このヘッダーを読み取り、サニタイズして ctx.state.rootPath に保存する早期ミドルウェア(通常は prepare という名前)が必要です。設定後、後続のハンドラーは ctx.state.rootPath を直接読み取ります。また、このミドルウェアは、バインディングごとに CDN キャッシュが正しく機能するように Vary: x-vtex-root-path も設定する必要があります。
  • フロントエンド(React) → コンポーネント内で vtex.render-runtime の useRuntime().rootPath を使用して、現在のバインディングのパスプレフィックスを取得します。
  • 必ずプレフィックスを付け、ハードコードしない — /us/ などのパスプレフィックスをハードコードしてはいけません。同じコードがすべてのバインディングで動作するよう、ランタイムから提供される rootPath を必ず使用してください。

厳守する制約

制約:マルチバインディングストアで URL を構築するときは、必ず rootPath を使用する

アプリが生成するすべての URL(リンク、リダイレクト、API エンドポイント、正規 URL、サイトマップのエントリー)には、ストアがパスベースのバインディングを使用する場合、rootPath プレフィックスを含める必要があります。

重要な理由 — rootPath がないと、/br/ バインディング内の /my-account/orders へのリンクは、誤ったバインディングを指すか、404 になります。プレフィックスのない URL を含むサイトマップは、検索エンジンを誤ったロケールに誘導し、SEO に悪影響を及ぼします。プレフィックスのないリダイレクトは、ユーザーを現在のバインディングではなくデフォルトのバインディングに送ります。

検出方法 — rootPath を先頭に付けずに文字列リテラル(例:/${slug})で URL を構築している。または、rootPath プレフィックスを含まない navigate() 呼び出しがある。

正しい例 — prepare ミドルウェアで解析した rootPath を、生成するすべてのパスの先頭に付けます。

// Backend: use ctx.state.rootPath (parsed by prepare middleware)
const { rootPath, forwardedHost } = ctx.state;

const canonicalUrl = `https://${forwardedHost}${rootPath}/product/${slug}`;
const sitemapEntry = `${rootPath}/${categoryPath}`;
// Frontend: use runtime hook
import { useRuntime } from "vtex.render-runtime";

const MyLink = ({ slug }: { slug: string }) => {
  const { rootPath } = useRuntime();
  return <a href={`${rootPath}/product/${slug}`}>View product</a>;
};

誤った例 — rootPath のないハードコードされたパス。

// Backend: missing rootPath — breaks in multi-binding
const canonicalUrl = `https://${host}/product/${slug}`

// Frontend: hardcoded path
const MyLink = ({ slug }: { slug: string }) => {
  return <a href={`/product/${slug}`}>View product</a>
}
制約:二重スラッシュを避けるため rootPath をサニタイズする

rootPath が "/"(シングルバインディングまたはデフォルトのバインディング)の場合、そのまま使用すると URL に二重スラッシュが発生します(例://product/shoes)。正規化して、rootPath === "/" の場合は "" として扱います。

重要な理由 — URL の二重スラッシュは、リダイレクトループ、正規 URL の破損、SEO の評価低下を引き起こします。一部の CDN レイヤーは、//path と /path を異なるものとして扱います。

検出方法 — rootPath === "/" を確認せずに rootPath + "/" + path を連結して URL を構築している。

正しい例

// In the prepare middleware, sanitize before storing on state:
let rootPath = ctx.get("x-vtex-root-path");
if (rootPath && !rootPath.startsWith("/")) {
  rootPath = `/${rootPath}`;
}
if (rootPath === "/") {
  rootPath = "";
}
ctx.state.rootPath = rootPath;

// Downstream: ctx.state.rootPath is already sanitized
const { rootPath } = ctx.state;
const url = `${rootPath}/${path}`;

誤った例

const rootPath = ctx.get("x-vtex-root-path") || "/";
const url = `${rootPath}/${path}`; // Produces "//path" for default binding

推奨パターン

rootPath を含む State インターフェース

すべてのハンドラーが型付きでアクセスできるよう、State インターフェースに rootPath と関連するバインディングの状態を宣言します。

// node/typings.d.ts
declare global {
  interface State extends RecorderState {
    binding: Binding;
    rootPath: string;
    forwardedHost: string;
    forwardedPath: string;
    isCrossBorder: boolean;
    matchingBindings: Binding[];
  }

  type Context = ServiceContext<Clients, State>;
}
ミドルウェアの準備(ヘッダーからrootPathを解析)

すべてのルートのミドルウェアチェーンの早い段階でprepareミドルウェアを接続してください。このミドルウェアはx-vtex-root-pathヘッダーを読み取り、サニタイズして現在のバインディングを解決し、CDNがバインディングごとにレスポンスをキャッシュするようにVaryヘッダーを設定します。

// node/middlewares/prepare.ts
const FORWARDED_HOST_HEADER = "x-forwarded-host";
const VTEX_ROOT_PATH_HEADER = "x-vtex-root-path";

export async function prepare(ctx: Context, next: () => Promise<void>) {
  const forwardedHost = ctx.get(FORWARDED_HOST_HEADER);

  let rootPath = ctx.get(VTEX_ROOT_PATH_HEADER);

  // Defend against malformed root path — must start with /
  if (rootPath && !rootPath.startsWith("/")) {
    rootPath = `/${rootPath}`;
  }

  // Normalize "/" to "" to avoid double slashes in URL construction
  if (rootPath === "/") {
    rootPath = "";
  }

  const [forwardedPath] = ctx.get("x-forwarded-path").split("?");

  ctx.state = {
    ...ctx.state,
    forwardedHost,
    forwardedPath,
    rootPath,
    // ... resolve binding, matchingBindings, etc.
  };

  await next();

  // Vary on these headers so CDN caches separate responses per binding
  ctx.vary(FORWARDED_HOST_HEADER);
  ctx.vary(VTEX_ROOT_PATH_HEADER);
}

その後、後続のハンドラーではctx.state.rootPathを直接使用できるため、ヘッダーを解析する必要はありません。

// node/middlewares/generateSitemap.ts
export async function generateSitemap(ctx: Context, next: () => Promise<void>) {
  const { rootPath, binding } = ctx.state;
  const canonicalUrl = `https://${ctx.state.forwardedHost}${rootPath}/${slug}`;
  // ...
}
フロントエンドユーティリティ
import { useRuntime } from "vtex.render-runtime";

function usePrefixedPath(path: string): string {
  const { rootPath = "" } = useRuntime();
  const prefix = rootPath === "/" ? "" : rootPath;
  return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
}
フロントエンドからのバインディング対応API呼び出し
const { rootPath, binding } = useRuntime();
// binding.id — current binding ID
// binding.canonicalBaseAddress — e.g. "store.com/br"
// rootPath — e.g. "/br"

// When calling backend APIs, the platform handles rootPath automatically
// for IO-internal calls. For external URLs or custom redirects, prefix manually.

よくある失敗例

  • 複数バインディングでリンクが壊れる — rootPathを指定せずに作成されたナビゲーションリンクは、ユーザーを誤ったバインディングに誘導するか、404エラーになります。
  • サイトマップのURLが誤っている — サイトマップ生成でrootPathが省略されると、検索エンジンはプレフィックスのないURLをインデックスし、デフォルトのバインディングに解決されます。
  • スラッシュの重複 — rootPath === "/"を/pathに連結すると、//pathになります。空文字列に正規化してください。
  • ロケールパスのハードコード — 動的なrootPathではなく、/us/や/br/を使用します。バインディングの再設定時に問題が発生します。
  • バックエンドがヘッダーを無視する — Nodeサービスがx-vtex-root-pathを読み取らずにURLを作成すると、複数バインディングで誤ったcanonical URLが生成されます。
  • Varyヘッダーの欠落 — レスポンスにVary: x-vtex-root-pathとVary: x-forwarded-hostが設定されていないと、CDNが異なるバインディングに同じキャッシュ済みレスポンスを配信します。

レビューチェックリスト

  • アプリには、x-vtex-root-pathをctx.state.rootPathに読み込むprepareミドルウェア(バックエンド)、またはuseRuntime()の使用(フロントエンド)がありますか?
  • CDNがバインディングごとにキャッシュするよう、レスポンスにVary: x-vtex-root-pathとVary: x-forwarded-hostを設定していますか?
  • 生成されるすべてのURL(リンク、リダイレクト、canonical URL、サイトマップ)にrootPathを付けていますか?
  • 二重スラッシュを避けるため、rootPath === "/"を""に正規化していますか?
  • ロケールパスのプレフィックス(例:/us/、/br/)をハードコードしていませんか?
  • アプリは単一バインディングと複数バインディングの両方のストアで正しく動作しますか?

関連スキル

参考資料

コメント