vtex-io-rootpath

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

VTEX IO rootPath for multi-binding stores

面向多绑定商店的VTEX IO rootPath

When this skill applies

本技能的适用场景

Use this skill when building VTEX IO apps that must work in stores with multi-binding configurations—typically cross-border stores where multiple bindings share a single domain with path prefixes (e.g.
store.com/us/
,
store.com/br/
,
store.com/mx/
).
  • Your app generates URLs (links, redirects, API endpoints, canonical URLs) that must include the binding's path prefix
  • Your app loads assets (images, scripts, stylesheets) that break when the store uses a sub-path binding
  • Your backend routes need to construct URLs for sitemaps, canonical links, or cross-binding references
  • You're debugging 404s or wrong links that only appear in multi-binding stores but work fine in single-binding
Do not use this skill for:
  • Single-binding stores with a dedicated domain per locale (no path prefix needed)
  • General IO backend patterns (use
    vtex-io-service-apps
    )
  • CDN/edge caching configuration (use
    vtex-io-service-paths-and-cdn
    )
当你构建的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

Decision rules

决策规则

  • Single-domain multi-binding (e.g.
    store.com/us/
    ,
    store.com/br/
    ) →
    rootPath
    is required. Every generated URL must be prefixed with the binding's root path.
  • Multi-domain single-binding (e.g.
    store.us
    ,
    store.com.br
    ) →
    rootPath
    is typically empty or
    /
    . URLs work without prefixing, but code should still handle
    rootPath
    gracefully (use it if non-empty, skip if empty).
  • Backend (Node) → The platform sends the binding's path prefix in the
    x-vtex-root-path
    request header. Your app needs an early middleware (typically called
    prepare
    ) that reads this header, sanitizes it, and stores it on
    ctx.state.rootPath
    . Once set up, all downstream handlers read
    ctx.state.rootPath
    directly. The middleware must also set
    Vary: x-vtex-root-path
    so CDN caching works correctly per binding.
  • Frontend (React) → Use
    useRuntime().rootPath
    from
    vtex.render-runtime
    to get the current binding's path prefix in components.
  • Always prefix, never hardcode — Never hardcode a path prefix like
    /us/
    . Always use the runtime-provided
    rootPath
    so the same code works across all bindings.
  • 单域名多绑定(例如
    store.com/us/
    store.com/br/
    )→ 必须使用
    rootPath
    。所有生成的URL都必须添加绑定的根路径前缀。
  • 多域名单绑定(例如
    store.us
    store.com.br
    )→
    rootPath
    通常为
    /
    。无需前缀即可正常生成URL,但代码仍需优雅处理
    rootPath
    (非空时使用,为空时跳过)。
  • 后端(Node) → 平台会在
    x-vtex-root-path
    请求头中发送绑定的路径前缀。你的应用需要一个早期中间件(通常名为
    prepare
    )来读取该请求头,对其进行清理,并将其存储在
    ctx.state.rootPath
    中。设置完成后,所有下游处理器直接读取
    ctx.state.rootPath
    。该中间件还必须设置
    Vary: x-vtex-root-path
    ,以确保CDN针对每个绑定正确缓存。
  • 前端(React) → 使用
    vtex.render-runtime
    中的
    useRuntime().rootPath
    在组件中获取当前绑定的路径前缀。
  • 始终使用前缀,绝不硬编码 — 永远不要硬编码
    /us/
    这类路径前缀。始终使用运行时提供的
    rootPath
    ,以便同一代码可在所有绑定中正常工作。

Hard constraints

硬性约束

Constraint: Always use rootPath when constructing URLs in multi-binding stores

约束:在多绑定商店中构建URL时必须使用rootPath

Every URL your app generates—links, redirects, API endpoints, canonical URLs, sitemap entries—must include the
rootPath
prefix when the store uses path-based bindings.
Why this matters — Without
rootPath
, a link to
/my-account/orders
in the
/br/
binding points to the wrong binding (or 404s). Sitemaps with unprefixed URLs break SEO by pointing search engines to the wrong locale. Redirects without the prefix send users to the default binding instead of their current one.
Detection — URLs constructed as string literals (e.g.
/${slug}
) without prepending
rootPath
. Or
navigate()
calls that omit the
rootPath
prefix.
Correct — Prepend rootPath (parsed by prepare middleware) to all generated paths.
typescript
// 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}`;
tsx
// 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>;
};
Wrong — Hardcoded paths without rootPath.
typescript
// 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>
}
当商店使用基于路径的绑定时,你的应用生成的每个URL(链接、重定向、API端点、规范URL、站点地图条目)都必须包含
rootPath
前缀。
重要性 — 如果没有
rootPath
/br/
绑定中指向
/my-account/orders
的链接会指向错误的绑定(或返回404)。未添加前缀的站点地图会导致搜索引擎索引错误区域设置的URL,破坏SEO。未添加前缀的重定向会将用户发送到默认绑定而非当前绑定。
检测方式 — 以字符串字面量构建的URL(例如
/${slug}
)未添加
rootPath
前缀。或者
navigate()
调用省略了
rootPath
前缀。
正确示例 — 将所有生成的路径添加rootPath前缀(由prepare中间件解析)。
typescript
// 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}`;
tsx
// 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的硬编码路径。
typescript
// 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>
}

Constraint: Sanitize rootPath to avoid double slashes

约束:清理rootPath以避免双斜杠

When
rootPath
is
"/"
(single-binding or default binding), using it directly produces double slashes in URLs (e.g.
//product/shoes
). Normalize: if
rootPath === "/"
, treat it as
""
.
Why this matters — Double slashes in URLs cause redirect loops, broken canonical URLs, and SEO penalties. Some CDN layers treat
//path
differently from
/path
.
Detection — URL construction that concatenates
rootPath + "/" + path
without checking for
rootPath === "/"
.
Correct
typescript
// 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}`;
Wrong
typescript
const rootPath = ctx.get("x-vtex-root-path") || "/";
const url = `${rootPath}/${path}`; // Produces "//path" for default binding
rootPath
"/"
(单绑定或默认绑定)时,直接使用会导致URL中出现双斜杠(例如
//product/shoes
)。需进行规范化处理:如果
rootPath === "/"
,则将其视为
""
重要性 — URL中的双斜杠会导致重定向循环、规范URL失效以及SEO惩罚。部分CDN层对
//path
/path
的处理方式不同。
检测方式 — URL构建时直接拼接
rootPath + "/" + path
,未检查
rootPath === "/"
正确示例
typescript
// 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}`;
错误示例
typescript
const rootPath = ctx.get("x-vtex-root-path") || "/";
const url = `${rootPath}/${path}`; // Produces "//path" for default binding

Preferred pattern

推荐模式

State interface with rootPath

包含rootPath的状态接口

Declare
rootPath
and related binding state on your
State
interface so all handlers have typed access:
typescript
// 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>;
}
State
接口中声明
rootPath
及相关绑定状态,以便所有处理器都能通过类型化访问:
typescript
// 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>;
}

Prepare middleware (parses rootPath from header)

Prepare中间件(从请求头解析rootPath)

Wire a
prepare
middleware early in every route's middleware chain. It reads the
x-vtex-root-path
header, sanitizes it, resolves the current binding, and sets
Vary
headers so the CDN caches responses per binding:
typescript
// 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);
}
Downstream handlers then use
ctx.state.rootPath
directly — no header parsing needed:
typescript
// 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}`;
  // ...
}
在每个路由的中间件链早期添加
prepare
中间件。它会读取
x-vtex-root-path
请求头,对其进行清理,解析当前绑定,并设置
Vary
请求头,以便CDN针对每个绑定缓存响应:
typescript
// 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);

  // 防御格式错误的根路径 — 必须以/开头
  if (rootPath && !rootPath.startsWith("/")) {
    rootPath = `/${rootPath}`;
  }

  // 将"/"规范化为空字符串,避免URL构建时出现双斜杠
  if (rootPath === "/") {
    rootPath = "";
  }

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

  ctx.state = {
    ...ctx.state,
    forwardedHost,
    forwardedPath,
    rootPath,
    // ... 解析binding、matchingBindings等
  };

  await next();

  // 设置这些请求头的Vary字段,以便CDN针对每个绑定缓存不同的响应
  ctx.vary(FORWARDED_HOST_HEADER);
  ctx.vary(VTEX_ROOT_PATH_HEADER);
}
下游处理器随后可直接使用
ctx.state.rootPath
— 无需再解析请求头:
typescript
// 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}`;
  // ...
}

Frontend utility

前端工具函数

tsx
import { useRuntime } from "vtex.render-runtime";

function usePrefixedPath(path: string): string {
  const { rootPath = "" } = useRuntime();
  const prefix = rootPath === "/" ? "" : rootPath;
  return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
}
tsx
import { useRuntime } from "vtex.render-runtime";

function usePrefixedPath(path: string): string {
  const { rootPath = "" } = useRuntime();
  const prefix = rootPath === "/" ? "" : rootPath;
  return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
}

Binding-aware API calls from frontend

前端的绑定感知API调用

tsx
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.
tsx
const { rootPath, binding } = useRuntime();
// binding.id — 当前绑定ID
// binding.canonicalBaseAddress — 例如 "store.com/br"
// rootPath — 例如 "/br"

// 调用后端API时,平台会自动处理IO内部调用的rootPath。对于外部URL或自定义重定向,请手动添加前缀。

Common failure modes

常见故障模式

  • Links break in multi-binding — Navigation links constructed without
    rootPath
    send users to the wrong binding or 404.
  • Sitemap has wrong URLs — Sitemap generator omits
    rootPath
    , causing search engines to index unprefixed URLs that resolve to the default binding.
  • Double slashes
    rootPath === "/"
    concatenated with
    /path
    produces
    //path
    . Normalize to empty string.
  • Hardcoded locale paths — Using
    /us/
    or
    /br/
    instead of dynamic
    rootPath
    . Breaks when bindings are reconfigured.
  • Backend ignores header — Node service constructs URLs without reading
    x-vtex-root-path
    , producing wrong canonicals in multi-binding.
  • Missing Vary header — Response doesn't set
    Vary: x-vtex-root-path
    and
    Vary: x-forwarded-host
    , causing the CDN to serve the same cached response for different bindings.
  • 多绑定中链接失效 — 未使用
    rootPath
    构建的导航链接会将用户发送到错误的绑定或返回404。
  • 站点地图包含错误URL — 站点地图生成器未添加
    rootPath
    ,导致搜索引擎索引未添加前缀的URL,这些URL会解析到默认绑定。
  • 双斜杠问题
    rootPath === "/"
    /path
    拼接后产生
    //path
    。需规范化为空字符串。
  • 硬编码区域路径 — 使用
    /us/
    /br/
    而非动态
    rootPath
    。当绑定配置变更时会失效。
  • 后端忽略请求头 — Node服务未读取
    x-vtex-root-path
    就构建URL,导致多绑定中生成错误的规范URL。
  • 缺少Vary请求头 — 响应未设置
    Vary: x-vtex-root-path
    Vary: x-forwarded-host
    ,导致CDN为不同绑定提供相同的缓存响应。

Review checklist

审核清单

  • Does the app have a
    prepare
    middleware that reads
    x-vtex-root-path
    into
    ctx.state.rootPath
    (backend) or use
    useRuntime()
    (frontend)?
  • Does the response set
    Vary: x-vtex-root-path
    and
    Vary: x-forwarded-host
    so CDN caches per binding?
  • Are all generated URLs (links, redirects, canonicals, sitemaps) prefixed with
    rootPath
    ?
  • Is
    rootPath === "/"
    normalized to
    ""
    to avoid double slashes?
  • Are there no hardcoded locale path prefixes (e.g.
    /us/
    ,
    /br/
    )?
  • Does the app work correctly in both single-binding and multi-binding stores?
  • 应用是否有
    prepare
    中间件,将
    x-vtex-root-path
    读取到
    ctx.state.rootPath
    (后端)或使用
    useRuntime()
    (前端)?
  • 响应是否设置
    Vary: x-vtex-root-path
    Vary: x-forwarded-host
    ,以便CDN针对每个绑定缓存?
  • 所有生成的URL(链接、重定向、规范URL、站点地图)是否都添加了
    rootPath
    前缀?
  • 是否将
    rootPath === "/"
    规范化为
    ""
    以避免双斜杠?
  • 是否没有硬编码的区域路径前缀(例如
    /us/
    /br/
    )?
  • 应用在单绑定和多绑定商店中是否都能正常运行?

Related skills

相关技能

  • vtex-io-service-paths-and-cdn — Route prefixes and CDN behavior
  • vtex-io-service-apps — Backend middleware patterns
  • vtex-io-react-apps — Frontend component patterns
  • vtex-io-service-paths-and-cdn — 路由前缀与CDN行为
  • vtex-io-service-apps — 后端中间件模式
  • vtex-io-react-apps — 前端组件模式

Reference

参考资料