storefront-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Saleor Storefront Playbook

Saleor 店面开发实践手册

This skill owns Saleor data contracts and UX/data-layer behaviour. It does not own framework scaffolding, CSS setup, or env-loading specifics — the agent discovers those from the local project.
Parse
$ARGUMENTS
to determine which step to run.
本Skill负责Saleor的数据契约以及UX/数据层行为。它负责框架脚手架、CSS配置或环境加载的具体细节——Agent会从本地项目中自动识别这些内容。
解析
$ARGUMENTS
来确定要执行的步骤。

Step routing

步骤路由

Read the first word of
$ARGUMENTS
as the step number and jump to that section. Execute only that step, then stop and wait for the user to ask for the next one. Never chain steps automatically.
If no step is provided or the step is unrecognized, print:
Saleor Storefront Builder

Usage: /storefront-builder <step>

Steps:
  1   Bootstrap — wire GraphQL client, codegen, Saleor API connection
  2   Design & aesthetic — color palette, typography, accent color
  3   Catalog — product list page + product detail page with variant selection

Example: /storefront-builder 1

读取
$ARGUMENTS
的第一个单词作为步骤编号,并跳转到对应章节。仅执行指定步骤,完成后停止并等待用户请求下一步。禁止自动执行后续步骤。
如果未提供步骤或步骤编号不被识别,输出以下内容:
Saleor Storefront Builder

使用方法: /storefront-builder <step>

步骤:
  1   初始化 — 连接GraphQL客户端、代码生成工具、Saleor API
  2   设计与视觉风格 — 调色板、排版、强调色
  3   商品目录 — 产品列表页 + 带变体选择的产品详情页

示例: /storefront-builder 1

Step 1: Project Bootstrap

步骤1:项目初始化

Connect an existing project to Saleor's GraphQL API with correct client separation and codegen.
将现有项目连接到Saleor的GraphQL API,配置正确的客户端分离和代码生成工具。

0. Saleor instance check

0. Saleor实例检查

Ask the user:
"Do you have a Saleor instance ready?
  • No — create one at https://cloud.saleor.io/ (free tier available), then come back with the API URL.
  • Yes — paste your storefront/API URL and we'll get started."
Wait for the user's response before continuing. If they don't have an instance yet, stop here and let them set one up. If they provide a URL, note it for use in step 6.
询问用户:
"你是否已准备好Saleor实例?
等待用户回复后再继续。如果用户还没有实例,在此处停止,让用户先完成实例创建。如果用户提供了地址,记录下来以便在步骤6中使用。

1. Inspect the project

1. 检查项目

Read
package.json
and any framework config files present (
nuxt.config.ts
,
next.config.*
,
svelte.config.js
,
remix.config.js
,
vite.config.*
, etc.) to understand:
  • Framework and version
  • Package manager in use (check for lockfiles:
    pnpm-lock.yaml
    ,
    yarn.lock
    ,
    package-lock.json
    )
  • Existing GraphQL setup (if any)
  • Import alias conventions (e.g.
    @/
    ,
    ~/
    ,
    #
    )
  • Source directory layout (
    src/
    ,
    app/
    , flat root)
Do not ask about any of the above — derive it from the project. Only ask if something cannot be determined and is needed to proceed.
读取
package.json
和所有存在的框架配置文件(
nuxt.config.ts
next.config.*
svelte.config.js
remix.config.js
vite.config.*
等),以了解:
  • 使用的框架及版本
  • 使用的包管理器(通过锁文件判断:
    pnpm-lock.yaml
    yarn.lock
    package-lock.json
  • 已有的GraphQL配置(如果存在)
  • 导入别名约定(如
    @/
    ~/
    #
  • 源码目录结构(
    src/
    app/
    、根目录平铺)
无需询问上述信息——直接从项目中推导。仅当无法确定且该信息是继续操作的必要条件时,才向用户询问。

2. Create AGENTS.md

2. 创建AGENTS.md

If
AGENTS.md
does not already exist at the repo root, create it now. This wires Saleor-specific rules into the AI harness for all future interactions in this repo.
Check for installed skills:
bash
ls .agent-skills/saleor-storefront/AGENTS.md 2>/dev/null && echo "STOREFRONT" || echo ""
ls .agent-skills/saleor-configurator/AGENTS.md 2>/dev/null && echo "CONFIGURATOR" || echo ""
Write
AGENTS.md
, including only the
@
references for skills that are present:
markdown
undefined
如果仓库根目录下不存在
AGENTS.md
,立即创建。该文件会将Saleor专属规则集成到AI工具中,用于本仓库未来的所有交互。
检查已安装的Skill:
bash
ls .agent-skills/saleor-storefront/AGENTS.md 2>/dev/null && echo "STOREFRONT" || echo ""
ls .agent-skills/saleor-configurator/AGENTS.md 2>/dev/null && echo "CONFIGURATOR" || echo ""
编写
AGENTS.md
,仅包含已存在的Skill的
@
引用:
markdown
undefined

Saleor Storefront

Saleor Storefront

This is a Saleor-powered storefront.
这是一个基于Saleor的店面项目。

Workflow

工作流程

When running
/storefront-builder
, execute only the requested step, then stop and wait for the user to ask for the next one. Never chain steps automatically.
运行
/storefront-builder
时,仅执行请求的步骤,完成后停止并等待用户请求下一步。禁止自动执行后续步骤。

Saleor rules

Saleor规则

<!-- include if .agent-skills/saleor-storefront/ exists -->
@.agent-skills/saleor-storefront/AGENTS.md
<!-- include if .agent-skills/saleor-configurator/ exists -->
@.agent-skills/saleor-configurator/AGENTS.md

If `AGENTS.md` already exists, skip this step entirely — do not overwrite it.
<!-- 如果.agent-skills/saleor-storefront/存在则包含 -->
@.agent-skills/saleor-storefront/AGENTS.md
<!-- 如果.agent-skills/saleor-configurator/存在则包含 -->
@.agent-skills/saleor-configurator/AGENTS.md

如果`AGENTS.md`已存在,完全跳过此步骤——不要覆盖现有文件。

3. Install GraphQL dependencies

3. 安装GraphQL依赖

Using the package manager detected in step 1:
graphql-request graphql
@graphql-codegen/cli @graphql-codegen/client-preset  (dev)
使用步骤1中检测到的包管理器安装以下依赖:
graphql-request graphql
@graphql-codegen/cli @graphql-codegen/client-preset  (开发依赖)

4. Create codegen config

4. 创建代码生成配置

Write a codegen config file at the project root (filename:
codegen.ts
or
codegen.js
based on project conventions). Key values to set:
  • schema: Saleor GraphQL API URL, read from the env variable the project uses (or
    SALEOR_API_URL
    if none is established)
  • documents: glob pointing to the project's GraphQL files directory, following local conventions
  • generates: use the
    client
    preset with
    gqlTagName: "graphql"
Add a
codegen
script to
package.json
.
在项目根目录下编写代码生成配置文件(根据项目约定选择文件名:
codegen.ts
codegen.js
)。需要设置的关键值:
  • schema: Saleor GraphQL API地址,从项目使用的环境变量中读取(如果未建立相关变量,则使用
    SALEOR_API_URL
  • documents: 符合本地约定的、指向项目GraphQL文件目录的glob路径
  • generates: 使用
    client
    预设,并设置
    gqlTagName: "graphql"
package.json
中添加
codegen
脚本。

5. Create Saleor API clients

5. 创建Saleor API客户端

Two-client pattern — this is a Saleor correctness rule, not optional:
Write a client module in the location that matches the project's library/util conventions. Export two clients:
saleorClient      — anonymous, no auth headers — safe for RSC, SSG, public product queries
saleorAuthClient  — server-only, reads app token from env — NEVER use in browser bundles
Why two clients matter: passing an app token on public/cached queries leaks privileged access and can expose customer data. Anonymous queries must stay anonymous.
The auth client should only include the
Authorization
header when the token env var is set (guard with a conditional so the module doesn't throw on front-end environments where the var is absent).
双客户端模式——这是Saleor的正确性规则,不可省略:
在符合项目工具类/库约定的位置编写客户端模块。导出两个客户端:
saleorClient      — 匿名客户端,无认证头 — 适用于RSC、SSG、公开产品查询
saleorAuthClient  — 仅服务器端使用,从环境变量读取应用令牌 — 绝不能在浏览器包中使用
双客户端的重要性: 在公开/缓存查询中传递应用令牌会泄露特权访问权限,可能暴露客户数据。匿名查询必须保持匿名。
仅当令牌环境变量存在时,认证客户端才应包含
Authorization
头(使用条件判断,避免在未设置该变量的前端环境中抛出错误)。

6. Configure environment

6. 配置环境变量

Determine the env variable naming convention from the project (e.g. Next.js uses
NEXT_PUBLIC_*
for browser-accessible vars, Nuxt uses
NUXT_PUBLIC_*
, etc.).
Required variables:
  • [PUBLIC_PREFIX]_SALEOR_API_URL
    — Saleor GraphQL endpoint
  • [PUBLIC_PREFIX]_SALEOR_CHANNEL
    — default channel slug
  • SALEOR_APP_TOKEN
    (no public prefix — server-side only)
Write or update the project's env file (
.env.local
,
.env
, etc.) with placeholder values and comments. Ask the user if they have a Saleor API URL and channel slug to fill in.
Tip — inspecting an existing store with Configurator If you have access to an existing Saleor instance and are unsure what channels, categories, or products are configured, use the Configurator CLI:
bash
export SALEOR_URL=https://your-store.saleor.cloud/graphql/
export SALEOR_TOKEN=YOUR_TOKEN
pnpm dlx @saleor/configurator introspect
Read the resulting
config.yml
to find exact channel slugs, published products, and category structure — use these values directly in env and queries.
从项目中推导环境变量命名约定(例如Next.js使用
NEXT_PUBLIC_*
表示可浏览器访问的变量,Nuxt使用
NUXT_PUBLIC_*
等)。
必填变量:
  • [PUBLIC_PREFIX]_SALEOR_API_URL
    — Saleor GraphQL端点
  • [PUBLIC_PREFIX]_SALEOR_CHANNEL
    — 默认渠道别名
  • SALEOR_APP_TOKEN
    (无公共前缀——仅服务器端使用)
在项目的环境文件(
.env.local
.env
等)中写入或更新占位符值及注释。询问用户是否有Saleor API地址和渠道别名来填充这些值。
提示——使用Configurator检查现有店面 如果你有权访问现有Saleor实例,但不确定已配置的渠道、分类或商品,可以使用Configurator CLI:
bash
export SALEOR_URL=https://your-store.saleor.cloud/graphql/
export SALEOR_TOKEN=YOUR_TOKEN
pnpm dlx @saleor/configurator introspect
查看生成的
config.yml
文件,获取准确的渠道别名、已发布产品和分类结构——直接将这些值用于环境变量和查询。

7. Verify

7. 验证配置

If the API URL is configured, run codegen to confirm the schema is reachable:
bash
[package-manager] codegen 2>&1 | head -20
If it fails with a network error, help troubleshoot (wrong URL, missing auth, etc.).
如果已配置API地址,运行代码生成工具以确认架构可访问:
bash
[包管理器] codegen 2>&1 | head -20
如果因网络错误失败,帮助排查问题(地址错误、缺少认证等)。

8. Summary

8. 总结

[✓/–] AGENTS.md: [created / already existed]
✓ Framework: [detected framework]
✓ Package manager: [pm]
✓ Deps: graphql-request, @graphql-codegen/cli, @graphql-codegen/client-preset
✓ Clients: [path] (public + authenticated)
✓ Codegen: codegen.ts
[✓/⚠] API URL: [set / not set]
[✓/⚠] Channel: [slug / placeholder]

Next: /storefront-builder 2
After printing the summary, stop. Do not proceed to Step 2 unless the user explicitly asks.

[✓/–] AGENTS.md: [已创建 / 已存在]
✓ 框架: [检测到的框架]
✓ 包管理器: [包管理器名称]
✓ 依赖: graphql-request, @graphql-codegen/cli, @graphql-codegen/client-preset
✓ 客户端: [路径](公开 + 认证)
✓ 代码生成配置: codegen.ts
[✓/⚠] API地址: [已设置 / 未设置]
[✓/⚠] 渠道: [别名 / 占位符]

下一步: /storefront-builder 2
输出总结后停止。除非用户明确请求,否则不要执行步骤2。

Step 2: Design & Aesthetic

步骤2:设计与视觉风格

Define the visual identity of the storefront before writing any UI code. The output of this step is a theme module and design tokens that all future steps will import. The exact file paths and token format follow the project's existing conventions.
在编写任何UI代码之前,定义店面的视觉标识。本步骤的输出是一个主题模块和设计令牌,供后续所有步骤导入。文件路径和令牌格式遵循项目的现有约定。

1. Inspect the project's styling setup

1. 检查项目的样式配置

Read the project to determine:
  • CSS framework in use (Tailwind, CSS Modules, styled-components, UnoCSS, vanilla CSS, etc.)
  • Existing design token conventions (CSS custom properties, a
    theme.*
    file, Tailwind config, etc.)
  • Where shared styles live
Do not assume Tailwind or any specific CSS approach — derive it from the project.
读取项目以确定:
  • 使用的CSS框架(Tailwind、CSS Modules、styled-components、UnoCSS、原生CSS等)
  • 已有的设计令牌约定(CSS自定义属性、
    theme.*
    文件、Tailwind配置等)
  • 共享样式的存放位置
不要默认使用Tailwind或任何特定CSS方案——直接从项目中推导。

2. Ask about the aesthetic

2. 询问视觉风格偏好

Ask the user three questions in one message — conversational, not a form:
"Let's define the look of your storefront. A few quick questions:
  1. Do you have any references? (a brand, a URL, a screenshot — or skip)
  2. What's the general vibe? Some starting points if helpful: minimalist light, dark luxury, bold & colorful, soft & warm, classic editorial — or describe it in your own words.
  3. Any accent color in mind? This goes on buttons and links. A hex, a color name, or leave it to me."
If the user gives very little, ask one follow-up before proceeding.
在一条消息中向用户提出三个问题——采用对话式语气,而非表单式:
"让我们定义你的店面外观。几个简单的问题:
  1. 你是否有参考示例?(品牌、网址、截图——或跳过)
  2. 整体风格倾向?提供一些参考方向:极简浅色、深色奢华、大胆多彩、柔和温暖、经典排版风——或用你自己的语言描述。
  3. 是否有指定的强调色?将用于按钮和链接。可以是十六进制值、颜色名称,或由我推荐。"
如果用户提供的信息很少,最多追问一次后再继续。

3. Decide on tokens

3. 确定设计令牌值

Determine values for: background, surface, border, text primary, text secondary, accent, accent-hover, border radius, heading font, body font.
确定以下值:背景色、表面色、边框色、主文本色、次要文本色、强调色、强调色悬停态、边框圆角、标题字体、正文字体。

4. Write theme tokens

4. 编写主题令牌

Write a theme module in a location consistent with the project's conventions. Include a comment block capturing:
  • Style preset name
  • Reference (if any)
  • Accent rationale
  • Typography choice
Wire the tokens into the project's styling system following local conventions:
  • Tailwind: extend
    tailwind.config.*
    with the token values
  • CSS custom properties: write to the project's global CSS file
  • Other: follow what's already in use
Update the global/base CSS to apply background and text defaults.
在符合项目约定的位置编写主题模块。包含注释块,记录:
  • 样式预设名称
  • 参考示例(如果有)
  • 强调色选择理由
  • 排版选择
按照本地约定将令牌集成到项目的样式系统中:
  • Tailwind: 在
    tailwind.config.*
    中扩展令牌值
  • CSS自定义属性: 写入项目的全局CSS文件
  • 其他方案: 遵循项目已有的配置方式
更新全局/基础CSS以应用背景色和文本默认值。

5. Summary

5. 总结

✓ Style: [preset name]
✓ Accent: [color]
✓ Typography: [font choice]
✓ Theme tokens: [path]
✓ Styling system updated: [tailwind.config / globals.css / etc.]

Next: /storefront-builder 3
After printing the summary, stop. Do not proceed to Step 3 unless the user explicitly asks.

✓ 风格: [预设名称]
✓ 强调色: [颜色]
✓ 排版: [字体选择]
✓ 主题令牌: [路径]
✓ 样式系统已更新: [tailwind.config / globals.css / 等]

下一步: /storefront-builder 3
输出总结后停止。除非用户明确请求,否则不要执行步骤3。

Step 3: Catalog — Product List + PDP

步骤3:商品目录——产品列表页 + 产品详情页

Build a product listing page and product detail page with variant selection.
构建产品列表页和带变体选择的产品详情页。

Prerequisites check

前置检查

Verify the Saleor client module exists (search for it based on what was set up in Step 1). If missing, tell the user to run
/storefront-builder 1
first.
Check for a channel slug in the project's env file. If missing and not passed as argument, ask:
"What's your Saleor channel slug? (Saleor Dashboard → Channels, or press Enter for 'default-channel')"
Inspect the framework and routing conventions from the project to determine where to write pages and how data-fetching works (server components,
getStaticProps
, loaders,
load
functions,
asyncData
, etc.).
验证Saleor客户端模块是否存在(根据步骤1中创建的内容进行搜索)。如果不存在,告知用户先运行
/storefront-builder 1
检查项目环境文件中是否存在渠道别名。如果不存在且未作为参数传递,询问:
"你的Saleor渠道别名是什么?(可在Saleor后台 → 渠道中查看,或按Enter使用'default-channel')"
从项目中检测框架和路由约定,以确定页面的编写位置和数据获取方式(服务器组件、
getStaticProps
、loaders、
load
函数、
asyncData
等)。

1. GraphQL queries — Saleor data contracts

1. GraphQL查询——Saleor数据契约

Write a
products.graphql
file in the project's GraphQL documents directory.
在项目的GraphQL文档目录中编写
products.graphql
文件。

ProductCard fragment

ProductCard 片段

Required fields for a product listing surface:
graphql
fragment ProductCard on Product {
  id
  name
  slug
  thumbnail {
    url
    alt
  }
  pricing {
    priceRange {
      start {
        gross {
          amount
          currency
        }
      }
    }
  }
  category {
    name
    slug
  }
}
Why these fields:
  • thumbnail
    is nullable — always guard with a fallback image or placeholder
  • pricing.priceRange.start
    is nullable — guard before rendering price
  • category
    is nullable — guard before rendering category label
产品列表页所需的字段:
graphql
fragment ProductCard on Product {
  id
  name
  slug
  thumbnail {
    url
    alt
  }
  pricing {
    priceRange {
      start {
        gross {
          amount
          currency
        }
      }
    }
  }
  category {
    name
    slug
  }
}
选择这些字段的原因:
  • thumbnail
    可为空——必须始终设置备用图片或占位符
  • pricing.priceRange.start
    可为空——渲染价格前需进行判断
  • category
    可为空——渲染分类标签前需进行判断

ProductDetails fragment

ProductDetails 片段

Required fields for a PDP surface:
graphql
fragment ProductDetails on Product {
  id
  name
  slug
  description
  thumbnail {
    url
    alt
  }
  media {
    url
    alt
    type
  }
  pricing {
    priceRange {
      start {
        gross {
          amount
          currency
        }
      }
    }
  }
  category {
    name
    slug
  }
  variants {
    id
    name
    sku
    pricing {
      price {
        gross {
          amount
          currency
        }
      }
      priceUndiscounted {
        gross {
          amount
          currency
        }
      }
    }
    selectionAttributes: attributes(variantSelection: VARIANT_SELECTION) {
      attribute {
        name
        slug
      }
      values {
        name
        slug
      }
    }
    quantityAvailable
  }
}
Why these fields:
  • media
    array preferred over
    thumbnail
    on PDP — use
    thumbnail
    as fallback when
    media
    is empty
  • variants.pricing
    is nullable — guard before accessing
    amount
  • quantityAvailable
    is nullable for anonymous users — treat
    null
    as in-stock (behave as if 1 available)
  • selectionAttributes
    uses
    variantSelection: VARIANT_SELECTION
    filter — returns only variant-differentiating attributes (size, color, etc.), not product-level attributes
产品详情页所需的字段:
graphql
fragment ProductDetails on Product {
  id
  name
  slug
  description
  thumbnail {
    url
    alt
  }
  media {
    url
    alt
    type
  }
  pricing {
    priceRange {
      start {
        gross {
          amount
          currency
        }
      }
    }
  }
  category {
    name
    slug
  }
  variants {
    id
    name
    sku
    pricing {
      price {
        gross {
          amount
          currency
        }
      }
      priceUndiscounted {
        gross {
          amount
          currency
        }
      }
    }
    selectionAttributes: attributes(variantSelection: VARIANT_SELECTION) {
      attribute {
        name
        slug
      }
      values {
        name
        slug
      }
    }
    quantityAvailable
  }
}
选择这些字段的原因:
  • 产品详情页优先使用
    media
    数组而非
    thumbnail
    ——当
    media
    为空时,使用
    thumbnail
    作为备用
  • variants.pricing
    可为空——访问
    amount
    前需进行判断
  • 匿名用户的
    quantityAvailable
    可为空——将
    null
    视为有库存(按1件可用处理)
  • selectionAttributes
    使用
    variantSelection: VARIANT_SELECTION
    过滤器——仅返回区分变体的属性(尺寸、颜色等),不返回产品级属性

Queries

查询语句

graphql
query ProductList($channel: String!, $first: Int = 20, $after: String) {
  products(channel: $channel, first: $first, after: $after) {
    edges {
      node {
        ...ProductCard
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

query ProductBySlug($slug: String!, $channel: String!) {
  product(slug: $slug, channel: $channel) {
    ...ProductDetails
  }
}
Channel is always required — queries without
channel
return no pricing or availability data.
Run codegen after writing the queries.
graphql
query ProductList($channel: String!, $first: Int = 20, $after: String) {
  products(channel: $channel, first: $first, after: $after) {
    edges {
      node {
        ...ProductCard
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

query ProductBySlug($slug: String!, $channel: String!) {
  product(slug: $slug, channel: $channel) {
    ...ProductDetails
  }
}
渠道参数是必填项——不带
channel
的查询不会返回定价或库存数据。
编写查询后运行代码生成工具。

2. Saleor data handling rules

2. Saleor数据处理规则

Apply these rules when implementing the pages and components:
实现页面和组件时,需遵循以下规则:

Description parsing

描述解析

Saleor stores
description
as EditorJS JSON. Never render it raw. Parse safely:
typescript
function extractDescriptionText(description: unknown): string {
  try {
    const parsed = typeof description === "string" ? JSON.parse(description) : description;
    return parsed?.blocks
      ?.map((b: { data?: { text?: string } }) => b.data?.text ?? "")
      .filter(Boolean)
      .join(" ") ?? "";
  } catch {
    return "";
  }
}
Saleor以EditorJS JSON格式存储
description
。绝不能直接渲染原始内容。需安全解析:
typescript
function extractDescriptionText(description: unknown): string {
  try {
    const parsed = typeof description === "string" ? JSON.parse(description) : description;
    return parsed?.blocks
      ?.map((b: { data?: { text?: string } }) => b.data?.text ?? "")
      .filter(Boolean)
      .join(" ") ?? "";
  } catch {
    return "";
  }
}

Price formatting

价格格式化

Always use
Intl.NumberFormat
with the currency from the response — never hardcode currency symbols:
typescript
function formatPrice(amount: number, currency: string) {
  return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(amount);
}
Use
undefined
locale to respect the user's browser locale (or pass a locale if the project has a locale system).
始终使用
Intl.NumberFormat
并传入响应中的货币代码——绝不能硬编码货币符号:
typescript
function formatPrice(amount: number, currency: string) {
  return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(amount);
}
使用
undefined
locale以尊重用户的浏览器区域设置(如果项目有区域设置系统,也可传入指定locale)。

Image handling

图片处理

  • On PDP: prefer
    product.media[0]
    over
    thumbnail
    ; fall back to
    thumbnail
    if
    media
    is empty
  • Always guard for missing images — render a neutral placeholder, not a broken
    <img>
    tag
  • Use
    alt ?? product.name
    as the alt text fallback
  • 产品详情页:优先使用
    product.media[0]
    而非
    thumbnail
    ;如果
    media
    为空,回退到
    thumbnail
  • 始终处理图片缺失的情况——渲染中性占位符,而非损坏的
    <img>
    标签
  • 使用
    alt ?? product.name
    作为alt文本的备用值

Inventory / availability semantics

库存/可用性规则

  • quantityAvailable === null
    → treat as available (anonymous users don't see inventory)
  • quantityAvailable === 0
    → out of stock — disable selection and show visual indicator (strikethrough or muted)
  • quantityAvailable > 0
    → in stock
  • quantityAvailable === null
    → 视为有库存(匿名用户无法获取库存数量)
  • quantityAvailable === 0
    → 缺货——禁用选择并显示视觉标识(删除线或灰化)
  • quantityAvailable > 0
    → 有库存

Variant selection UX

变体选择UX

  • Show all variants; disable (not hide) out-of-stock ones — visibility helps users understand what exists
  • Use
    selectionAttributes
    to label variants (e.g. "Size: M", "Color: Red") when attributes are present
  • If a product has only one variant and no selection attributes, skip the selector and go straight to Add to Cart
  • The selected variant's
    pricing.price
    overrides the product-level
    pricing.priceRange
    — update the displayed price on selection
  • 显示所有变体;禁用(而非隐藏)缺货变体——可见性有助于用户了解产品的所有选项
  • 如果存在属性,使用
    selectionAttributes
    为变体添加标签(如"尺寸: M"、"颜色: 红")
  • 如果产品只有一个变体且无选择属性,跳过选择器,直接显示加入购物车按钮
  • 选中变体的
    pricing.price
    会覆盖产品级的
    pricing.priceRange
    ——选择变体时更新显示的价格

Empty / error states

空状态/错误状态

  • Product list with no results: show a clear message with troubleshooting hint (wrong channel slug or products not published)
  • Product not found (null from
    ProductBySlug
    ): use the framework's not-found/404 mechanism
  • Pricing missing: omit price entirely rather than showing $0 or NaN
  • 产品列表无结果:显示清晰的提示信息及排查建议(渠道别名错误或产品未在该渠道发布)
  • 产品未找到(
    ProductBySlug
    返回null):使用框架的404/未找到机制
  • 定价缺失:完全不显示价格,而非显示$0或NaN

3. Write shared navigation

3. 编写共享导航组件

Write a nav/header component in the project's component directory following local naming conventions. The nav should use the theme tokens established in Step 2 (or sensible neutral defaults if Step 2 was skipped).
Wire the nav into the root layout / app shell following framework conventions detected from the project.
在符合本地命名约定的项目组件目录中编写导航/头部组件。导航应使用步骤2中建立的主题令牌(如果跳过了步骤2,则使用合理的中性默认值)。
按照检测到的框架约定,将导航组件集成到根布局/应用外壳中。

4. Write product list page

4. 编写产品列表页

Write the product list page at the path that fits the project's routing conventions (e.g.
app/page.tsx
,
pages/index.tsx
,
pages/index.vue
,
src/routes/+page.svelte
,
app/routes/_index.tsx
).
Data-fetching pattern: use whatever the framework provides (async server component,
getStaticProps
/ISR,
load
function,
asyncData
, Remix loader). For SSG-capable frameworks, set a reasonable revalidation interval (e.g. 60s).
Apply all data handling rules from section 2: guard nullables, format prices correctly, show empty state.
在符合项目路由约定的路径下编写产品列表页(如
app/page.tsx
pages/index.tsx
pages/index.vue
src/routes/+page.svelte
app/routes/_index.tsx
)。
数据获取模式:使用框架提供的方式(异步服务器组件、
getStaticProps
/ISR、
load
函数、
asyncData
、Remix loader)。对于支持SSG的框架,设置合理的重新验证间隔(如60秒)。
应用第2节中的所有数据处理规则:处理空值、正确格式化价格、显示空状态。

5. Write PDP

5. 编写产品详情页

Write the PDP at the path that fits routing conventions (e.g.
app/p/[slug]/page.tsx
,
pages/p/[slug].tsx
,
pages/p/[slug].vue
,
src/routes/p/[slug]/+page.svelte
).
Apply all data handling rules from section 2.
在符合路由约定的路径下编写产品详情页(如
app/p/[slug]/page.tsx
pages/p/[slug].tsx
pages/p/[slug].vue
src/routes/p/[slug]/+page.svelte
)。
应用第2节中的所有数据处理规则。

6. Write VariantSelector component

6. 编写VariantSelector组件

Write a
VariantSelector
component in the project's component directory. It must be client-interactive (use whatever interactivity primitive the framework provides — React state, Vue
ref
, Svelte store, etc.).
Behaviour:
  • Shows all variants; disables out-of-stock ones (do not hide them)
  • Highlights selected variant
  • Updates displayed price when a variant is selected (variant
    pricing.price
    takes precedence)
  • Add to Cart button is disabled until a variant is selected (when selection is required)
  • Single-variant / no-attribute products: skip selector, show Add to Cart directly
  • Add to Cart is non-functional at this step — placeholder only, note this clearly in a comment
在项目的组件目录中编写
VariantSelector
组件。该组件必须支持客户端交互(使用框架提供的交互原语——React状态、Vue
ref
、Svelte store等)。
行为要求:
  • 显示所有变体;禁用(而非隐藏)缺货变体
  • 高亮选中的变体
  • 选择变体时更新显示的价格(变体的
    pricing.price
    优先)
  • 当需要选择变体时,加入购物车按钮在选中变体前保持禁用状态
  • 单变体/无属性产品:跳过选择器,直接显示加入购物车按钮
  • 加入购物车按钮在本步骤中仅为占位符,无实际功能——需在注释中明确说明

7. Run and verify

7. 运行与验证

Start the dev server using the project's dev command. Direct the user to the product list and a PDP URL to confirm data loads correctly.
Common issues:
  • Empty list: wrong channel slug or products not published in that channel — suggest running
    configurator introspect
    to inspect the store
  • Codegen errors: API URL not set or unreachable
  • Product not found on every slug: channel mismatch or product unpublished in that channel
使用项目的开发命令启动开发服务器。引导用户访问产品列表页和产品详情页URL,确认数据加载正常。
常见问题:
  • 空列表: 渠道别名错误或产品未在该渠道发布——建议运行
    configurator introspect
    检查店面配置
  • 代码生成错误: API地址未设置或无法访问
  • 所有别名都找不到产品: 渠道不匹配或产品未在该渠道发布

Summary

总结

✓ GraphQL queries: [path]/products.graphql
✓ Types generated
✓ Navigation: [path] (wired into root layout)
✓ Product list: [route]
✓ Product detail: [route]
✓ VariantSelector: [path]

Note: "Add to Cart" is present but non-functional — checkout is not covered by this skill

This is the last step currently available in this skill.
After printing the summary, stop.

✓ GraphQL查询: [路径]/products.graphql
✓ 类型已生成
✓ 导航组件: [路径](已集成到根布局)
✓ 产品列表页: [路由]
✓ 产品详情页: [路由]
✓ VariantSelector: [路径]

注意: "加入购物车"按钮已存在但无实际功能——本Skill不包含结账流程

这是本Skill当前提供的最后一个步骤。
输出总结后停止。

Saleor correctness rules (always apply)

Saleor正确性规则(始终适用)

These rules apply across all steps and any future storefront work:
  1. Always pass
    channel
    — every product/pricing/availability query requires it; omitting it returns no data
  2. Parse
    description
    safely
    — it is EditorJS JSON, not plain text or HTML
  3. Never expose
    SALEOR_APP_TOKEN
    to the browser
    — use the two-client pattern; the auth client is server-side only
  4. quantityAvailable
    null = available
    — anonymous users don't receive inventory counts; null means "don't block purchase"
  5. pricing
    is nullable at every level
    — guard
    pricing
    ,
    pricing.price
    ,
    pricing.priceRange
    , and
    gross
    before accessing
    amount
  6. Use
    Intl.NumberFormat
    for prices
    — never hardcode currency symbols or assume locale
  7. PDP media priority:
    media[0]
    thumbnail
    → placeholder
  8. Disable, don't hide, out-of-stock variants — hiding them confuses users about what the product offers
这些规则适用于所有步骤及未来的所有店面开发工作:
  1. 始终传递
    channel
    参数
    ——每个产品/定价/库存查询都需要该参数;省略该参数将返回空数据
  2. 安全解析
    description
    ——它是EditorJS JSON格式,而非纯文本或HTML
  3. 绝不能向浏览器暴露
    SALEOR_APP_TOKEN
    ——使用双客户端模式;认证客户端仅在服务器端使用
  4. quantityAvailable
    为null = 有库存
    ——匿名用户无法获取库存数量;null表示"不阻止购买"
  5. pricing
    在每个层级都可为空
    ——访问
    amount
    前,需对
    pricing
    pricing.price
    pricing.priceRange
    gross
    进行判断
  6. 使用
    Intl.NumberFormat
    格式化价格
    ——绝不能硬编码货币符号或假设区域设置
  7. 产品详情页媒体优先级:
    media[0]
    thumbnail
    → 占位符
  8. 禁用而非隐藏缺货变体——隐藏变体会让用户对产品的可选选项产生困惑