vtex-io-rootpath について
マルチバインディングの VTEX IO アプリで、URL やアセットにバインディングのパス接頭辞を適用するための開発スキルです。バックエンドとフロントエンドでの rootPath の扱い、正規化、URL やキャッシュの確認方法を説明します。
主な機能
- 01rootPath が必要なマルチバインディングの判定
- 02バックエンドヘッダーからの rootPath 取得と正規化
- 03useRuntime().rootPath によるフロントエンド接頭辞の取得
- 04生成 URL へのバインディングパスの付与
- 05キャッシュを区別する Vary ヘッダーの設定
活用シーン
- 01マルチバインディング店舗のリンク切れや 404 の修正
- 02接頭辞付き sitemap と canonical URL の生成
- 03サブパス型バインディングでのアセットパス対応
- 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/)をハードコードしていませんか? - アプリは単一バインディングと複数バインディングの両方のストアで正しく動作しますか?
関連スキル
- vtex-io-service-paths-and-cdn — ルートプレフィックスとCDNの動作
- vtex-io-service-apps — バックエンドのミドルウェアパターン
- vtex-io-react-apps — フロントエンドのコンポーネントパターン
参考資料
- 越境ストアのコンテンツ国際化 — 越境ストア向けの複数バインディング設定
- サービスパスのパターン — パブリック、セグメント、プライベートのパスプレフィックス
- アプリ開発 — VTEX IOアプリ開発ハブ