主题开发
使用 Liquid 模板语言开发 ShopFaaS 店铺前台主题。
本指南面向主题开发者、二次开发伙伴、AI 主题生成器。以 default/1.0.0 主题为标杆与参照,描述 ShopFaaS 店铺前端的模板体系、上下文变量、自定义标签与过滤器。所有内容均以实际代码为依据。
三层模板机制
ShopFaaS 店铺前端渲染由三层组成,按优先级从高到低:
| 层级 | 角色 | 实现技术 |
|---|---|---|
| L1 主题源码 | 商家可定制的 Liquid 主题包 | Liquid(liquidjs 引擎) |
| L2 运行时存储 | Liquid 引擎实际读取的副本(S3 + Local 双写) | 对象存储 |
| L3 兜底组件 | 主题缺失或模板不存在时的默认实现 | Astro + Solid |
关键点:L3 兜底组件是默认实现,不是简单占位。default/1.0.0 主题的 Liquid 源码与 L3 兜底组件1:1 对齐——每个 Liquid section/snippet 都对应一个 .astro 组件,CSS/JS 也保持同步。开发者二次开发新主题时,可参照 default/1.0.0 的源码作为模板,并以 L3 兜底组件为视觉与交互的”正确性基准”。
主题包结构
主题包根目录为 {themeCode}/{version}/,包含 7 个固定子目录:
my-theme/1.0.0/
├── assets/ # 静态资源(CSS / JS / 图片 / 字体)
│ ├── theme.css # 主题样式
│ └── theme.js # 主题脚本
├── config/ # 主题配置
│ ├── settings_data.json # 主题设置当前值
│ └── settings_schema.json # 主题设置 schema 定义
├── layouts/ # 布局模板
│ └── theme.liquid # 主布局(HTML 骨架 + 全局 section)
├── locales/ # 多语言翻译文件
│ ├── en.default.json # 默认语言(必须)
│ └── zh-CN.json # 中文
├── sections/ # Section 文件(可复用页面区块)
│ ├── header.liquid
│ ├── footer.liquid
│ └── ...
├── snippets/ # Snippet 文件(可被 {% render %} 引用的小片段)
│ ├── card-product.liquid
│ └── ...
└── templates/ # 页面模板(按 PageType 映射)
├── index.liquid # HOME
├── product.liquid # PRODUCT
├── collection.liquid # COLLECTION
└── ...
不存在的目录
以下目录在 ShopFaaS 主题包中不存在:
- ❌
schemas/— section schema 以内联注释形式写在 section 文件首行(见下文”自定义标签”),无独立 schema 目录 - ❌
theme.json— 主题配置统一用config/settings_data.json,不存在根级theme.json文件 - ❌
blocks/— 当前未实现 block 独立目录机制 - ❌
customers/— 客户中心页面模板直接放在templates/下(如customer-login.liquid),无独立子目录
文件命名规范
- 全部使用 kebab-case:
main-checkout.liquid/customer-order-detail.liquid - section 文件名 = section 类型名(
{% section 'main-product' %}对应sections/main-product.liquid) - snippet 文件名 = snippet 名称(
{% render 'cart-drawer' %}对应snippets/cart-drawer.liquid) - 模板文件名 =
PAGE_TYPE_TO_TEMPLATE映射值(见下文)
Liquid 上下文变量
渲染时注入以下全局变量。所有 section / snippet / layout / template 均可直接引用。
核心业务变量
| 变量 | 类型 | 说明 |
|---|---|---|
shop | ShopContext | 店铺信息(id / name / currency / currencyFormat / email / phone / address / domain / timezone / industry / logoUrl) |
customer | CustomerHeaderSummary | null | 当前登录客户摘要(displayName / email,未登录为 null)。注意字段名是 displayName,不是 name |
page | PageData | 当前页面业务数据(联合类型,按 pageType 区分) |
pageType | string | 页面类型枚举值(如 "PRODUCT" / "CUSTOMER_ORDERS"),全大写 |
sections | SectionInstance[] | 当前页面 section 配置列表(snake_case:section_type / section_key / section_config / section_blocks / sort_order) |
collections | CollectionSummary[] | 导航用分类列表(id / handle / title / image / productCount) |
languages | ShopLanguageOption[] | 店铺可用语言列表 |
currencies | ShopCurrencyOption[] | 店铺可用币种列表 |
themeSettings | Record<string, unknown> | 主题设置(扁平字段命名,见下文) |
locale | string | BCP 47 locale(如 en-US / zh-CN) |
currency | string | 当前币种 ISO 代码(如 USD / CNY) |
exchangeRates | ExchangeRateMap | 汇率映射表(key 格式 ${from}_${to},如 USD_CNY) |
chat | ChatContext | 在线客服配置(enabled / welcomeMessage / placeholder / offlineMessage) |
overrides | Record<string, string> | 主题文案 DB 覆盖 |
顶层展开变量
page 数据会展开到顶层(排除 page 和 type 字段),模板内可直接使用展开后的字段:
{{ product.title }}
{{ collection.title }}
{{ cart.items }}
{{ article.title }}
{{ searchQuery }}
CollectionPageData.page(页码数字)单独暴露为 current_page:
{% if current_page > 1 %}
<span>第 {{ current_page }} 页</span>
{% endif %}
请求级变量
| 变量 | 字段 | 说明 |
|---|---|---|
request | locale.iso_code | BCP 47 locale,用于 <html lang="..."> |
request | locale.lang / locale.country | 拆分的语言/国家代码 |
request | page_type | 页面类型小写(如 home / product / cart) |
request | design_mode | 是否主题编辑器预览模式(当前固定 false) |
request | origin | 店铺 origin |
路由变量 routes
完整 28 个路由 URL,供 layout/sections/snippets 中构造链接。所有路由以 / 开头,无尾斜杠。
| 字段 | 值 |
|---|---|
routes.root_url | / |
routes.cart_url | /cart |
routes.cart_add_url | /cart/add |
routes.cart_change_url | /cart/change |
routes.cart_update_url | /cart/update |
routes.cart_count_url | /cart/count |
routes.cart_discount_code_apply_url | /cart/discount/apply |
routes.cart_discount_code_remove_url | /cart/discount/remove |
routes.search_url | /search |
routes.predictive_search_url | /search/suggest |
routes.account_url | /account |
routes.account_login_url | /account/login |
routes.account_logout_url | /account/logout |
routes.account_register_url | /account/register |
routes.account_recover_url | /account/forgot-password |
routes.account_order_list_url | /account/orders |
routes.account_address_new_url | /account/addresses/new |
routes.address_url | /account/addresses |
routes.address_countries_url | /api/countries |
routes.address_country_template_url | /api/countries/template |
routes.address_next_url | /account/addresses/next |
routes.company_account_register_url | /account/company/register |
routes.collections_url | /collections |
routes.all_products_collection_url | /collections/all |
routes.blogs_url | /blogs |
routes.contact_url | /pages/contact |
routes.privacy_policy_url | /policies/privacy |
routes.product_recommendations_url | /recommendations/products |
店铺级设置 settings
固定 3 个字段:
| 字段 | 值 | 说明 |
|---|---|---|
settings.cart_add_type | "drawer" | 加购后打开购物车抽屉 |
settings.predictive_search | true | 启用预测搜索 |
settings.show_sticky_add_to_cart | true | 商品页粘性加购按钮 |
翻译变量
| 变量 | 说明 |
|---|---|
__translations | 当前 locale 完整翻译字典。供 t 过滤器读取。优先级:DB 覆盖 > 主题 locales > 内置字典 > key 本身 |
client_i18n | 客户端 JS 共享字典,由 theme.liquid 注入 window.__STOREFRONT_I18N__ |
__shop_currency | 店铺基础币种(商品存储币种,如 USD),供 money 过滤器读取 |
__currency | 当前用户选择币种(cookie sf_currency 解析结果,如 CNY) |
客户端 JS 共享变量
theme.liquid 应将以下变量注入 window,供 CartDrawer / CartPage / QuickAddModal 等客户端 JS 读取:
window.__STOREFRONT_I18N__ = client_i18n;
window.__STOREFRONT_CURRENCY__ = currency;
window.__STOREFRONT_LOCALE__ = locale;
window.__STOREFRONT_BASE_CURRENCY__ = shop.currency;
window.__STOREFRONT_RATES__ = exchange_rates;
window.__STOREFRONT_LINK_QUERY__ = link_query;
link_query 含义:开发环境为 ?shop_id=xxx(或 ?shop_id=xxx&theme_preview=xxx),生产环境为空字符串。客户端 JS 构造内部链接时需追加此参数。
link_query 使用规范(重要)
link_query 是已含前导 ? 的完整查询串(如 ?shop_id=xxx),追加到 URL 末尾即可。但需注意 URL 是否已有查询参数:
场景 1:URL 无查询参数(最常见)
直接追加 {{ link_query }}:
<a href="/cart{{ link_query }}">购物车</a>
<a href="/products/{{ product.handle }}{{ link_query }}">{{ product.title }}</a>
渲染结果:/cart?shop_id=xxx / /products/premium-hoodie?shop_id=xxx(正确)
场景 2:URL 已有查询参数(如 ?redirect_to=)
必须用 | replace_first: '?', '&' 将前导 ? 转为 &,否则 shop_id 会被并入 redirect_to 的值:
{# ❌ 错误:shop_id 被并入 redirect_to 值 #}
<a href="/account/login?redirect_to={{ '/checkout' | url_encode }}{{ link_query }}">登录</a>
{# 渲染结果:/account/login?redirect_to=%2Fcheckout?shop_id=xxx(redirect_to 值变成 %2Fcheckout?shop_id=xxx)#}
{# ✅ 正确:前导 ? 转 & #}
<a href="/account/login?redirect_to={{ '/checkout' | url_encode }}{{ link_query | replace_first: '?', '&' }}">登录</a>
{# 渲染结果:/account/login?redirect_to=%2Fcheckout&shop_id=xxx #}
场景 3:客户端 JS 构造链接
客户端 JS 通过 window.__STOREFRONT_LINK_QUERY__ 读取(已是 ?shop_id=xxx 或空字符串),直接拼接即可:
var linkQuery = window.__STOREFRONT_LINK_QUERY__ || "";
var productUrl = "/products/" + encodeURIComponent(handle) + linkQuery;
Astro 兜底侧:使用
link()函数(由createLinker创建),内部已自动处理?/&切换,无需手动 replace。Liquid 侧因link_query是固定字符串,需开发者自行处理。
link_query 不要用在哪
- section settings 动态链接:
{{ section.settings.button_link }}是用户在主题编辑器配置的 URL(可能是外链或绝对路径),不应追加link_query - OAuth API 链接:
/api/auth/oauth/*是 API 路由,无需追加查询参数 - 静态资产 URL:
{{ 'theme.css' | asset_url }}等已由asset_url过滤器处理
PageType 与模板映射
完整 PageType 枚举(25 种)
通用页面(12 种):
HOME / PRODUCT / COLLECTION / COLLECTION_LIST / SEARCH / CART / BLOG / BLOG_LIST / PAGE / CHECKOUT / ORDER_CONFIRMATION / NOT_FOUND
客户中心页面(13 种):
CUSTOMER_LOGIN / CUSTOMER_REGISTER / CUSTOMER_FORGOT_PASSWORD / CUSTOMER_RESET_PASSWORD / CUSTOMER_ACCOUNT / CUSTOMER_ADDRESSES / CUSTOMER_ORDERS / CUSTOMER_ORDER_DETAIL / CUSTOMER_WISHLIST / CUSTOMER_BACK_IN_STOCK / CUSTOMER_BROWSING_HISTORY / CUSTOMER_CONSENT / CUSTOMER_SUBSCRIPTIONS
PageType → 模板文件映射
25 个 PageType 全部映射到对应模板文件:
| PageType | 模板文件 |
|---|---|
HOME | templates/index.liquid |
PRODUCT | templates/product.liquid |
COLLECTION | templates/collection.liquid |
COLLECTION_LIST | templates/list-collections.liquid |
SEARCH | templates/search.liquid |
CART | templates/cart.liquid |
BLOG | templates/article.liquid |
BLOG_LIST | templates/blog.liquid |
PAGE | templates/page.liquid |
CHECKOUT | templates/checkout.liquid |
ORDER_CONFIRMATION | templates/order-confirmation.liquid |
NOT_FOUND | templates/404.liquid |
CUSTOMER_LOGIN | templates/customer-login.liquid |
CUSTOMER_REGISTER | templates/customer-register.liquid |
CUSTOMER_FORGOT_PASSWORD | templates/customer-forgot-password.liquid |
CUSTOMER_RESET_PASSWORD | templates/customer-reset-password.liquid |
CUSTOMER_ACCOUNT | templates/customer-account.liquid |
CUSTOMER_ADDRESSES | templates/customer-addresses.liquid |
CUSTOMER_ORDERS | templates/customer-orders.liquid |
CUSTOMER_ORDER_DETAIL | templates/customer-order-detail.liquid |
CUSTOMER_WISHLIST | templates/customer-wishlist.liquid |
CUSTOMER_BACK_IN_STOCK | templates/customer-back-in-stock.liquid |
CUSTOMER_BROWSING_HISTORY | templates/customer-browsing-history.liquid |
CUSTOMER_CONSENT | templates/customer-consent.liquid |
CUSTOMER_SUBSCRIPTIONS | templates/customer-subscriptions.liquid |
自定义标签
{% section 'type' %}
从 sections/{type}.liquid 读取并渲染。
Section schema 内联注释:section schema 以 JSON 注释形式写在文件首行,无独立 schemas/ 目录:
{%- comment -%}{"schema":{"name":"Header"}}{%- endcomment -%}
{%- comment -%}
Header section — 描述文字
上下文变量:shop / locale / currency / languages / currencies / customer / collections
{%- endcomment -%}
<header class="header">
...
</header>
Section 实例数据:{% section %} 标签从上下文 sections 数组按 section_type 匹配实例。匹配的实例数据通过两个变量暴露给 section 模板:
section.settings:section 配置(等价于section_config)section.blocks:section 块数据(等价于section_blocks)
{% render 'snippet' %}
从 snippets/{snippet}.liquid 读取。支持命名参数传递:
{% render 'card-product', product: product, show_vendor: themeSettings.show_vendor %}
{% layout 'name' %}
从 layouts/{name}.liquid 读取布局模板。layout 模板内通过 {{ content_for_layout }} 接收子模板渲染结果。
{# templates/index.liquid #}
{% layout 'theme' %}
<div class="index-template">
{% section 'slideshow' %}
{% section 'featured-collection' %}
</div>
数据模型
核心数据接口
ProductSummary(列表用)
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 商品 ID |
handle | string | URL handle |
title | string | 标题 |
price | number | 价格(单位:分) |
compareAtPrice | number | null | 划线价(分) |
image | string | null | 主图 |
image2 | string | null | 第二张图(hover 切换) |
available | boolean | 是否可售 |
rating | number | 平均评分(1-5,无评论为 0) |
reviewCount | number | 评论总数 |
vendor | string | 品牌/供应商 |
specValues | Record<string, string[]> | 规格值聚合 |
ProductDetail(详情页,extends ProductSummary)
新增字段:description / variants / images / options / ageRestricted / sellingPlans
ProductVariant
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 变体 ID |
title | string | 变体标题 |
price | number | 价格(分) |
compareAtPrice | number | null | 划线价(分) |
available | boolean | 是否可售 |
optionValues | Record<string, string> | 选项值(如 {"COLOR":"Black","SIZE":"M"}) |
quantity | number | 库存数量 |
sku | string | SKU 编码 |
CartItem
| 字段 | 类型 | 说明 |
|---|---|---|
productId | string | 商品 ID |
variantId | string | 变体 ID |
title | string | 商品标题 |
quantity | number | 数量 |
price | number | 单价(分) |
image | string | null | 商品图片 |
specValues | Record<string, string> | null | 变体规格属性 |
CustomerHeaderSummary(Header 登录态)
| 字段 | 类型 | 说明 |
|---|---|---|
displayName | string | 展示名(优先 fullName,无则邮箱前缀)。不是 name |
email | string | null | 客户邮箱 |
金额单位统一为「分」:所有
price/compareAtPrice/totalAmount等字段均为整数分。使用{{ product.price | money }}过滤器自动除以 100 并格式化。
themeSettings 字段
命名规范
扁平字段命名,使用 snake_case(如 color_primary / heading_font / page_width),不是嵌套结构(如 )。colors.primary
完整字段清单
字段定义在 config/settings_schema.json,当前值在 config/settings_data.json 的 current 字段。
Colors(6 个)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
color_primary | color | #1a1a1a | 主色 |
color_secondary | color | #6b7280 | 次色 |
color_accent | color | #c9a961 | 强调色 |
color_background | color | #ffffff | 背景色 |
color_text | color | #111827 | 文字色 |
color_border | color | #e5e7eb | 边框色 |
Typography(2 个)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
heading_font | text | Georgia, serif | 标题字体 CSS font-family |
body_font | text | system-ui, sans-serif | 正文字体 |
Layout(3 个)
| 字段 | 类型 | 默认值 | 范围 | 说明 |
|---|---|---|---|---|
page_width | range | 1280 | 1024-1600 / step 32 | 页面宽度(px) |
container_padding | range | 24 | 8-48 / step 4 | 容器内边距(px) |
section_spacing | range | 48 | 24-96 / step 8 | section 间距(px) |
Header / Footer / Product / Slideshow / Social Media / Social Login
完整字段清单参考 default/1.0.0/config/settings_schema.json。常用字段包括:
- Header:
logo_text/logo_width/sticky_header/show_announcement/mega_menu_show_images/mega_menu_promo_image/mega_menu_promo_title/mega_menu_promo_url - Footer:
show_social/show_payment_icons/footer_text - Product:
show_vendor/show_reviews/quick_add_enabled - Slideshow:
slideshow_image/slideshow_mobile_image/slideshow_overlay_opacity - Social Media:
social_facebook/social_twitter/social_instagram/social_youtube/social_pinterest/social_tiktok - Social Login:
oauth_google_enabled/oauth_facebook_enabled/oauth_apple_enabled
注意:
settings_data.json的current字段可能不包含settings_schema.json中所有字段。模板中读取未设置的字段会得到nil,应使用default过滤器兜底:{{ themeSettings.color_primary | default: '#1a1a1a' }}。
读取方式
主题设置统一从 config/settings_data.json 的 current 字段读取(Shopify 标准,扁平结构)。
自定义过滤器
asset_url
主题静态资产路径 → 访问 URL。
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
<script src="{{ 'theme.js' | asset_url }}" defer></script>
asset_img_url
主题图片资产 → 带尺寸参数的 URL。
<img src="{{ 'slide-1.jpg' | asset_img_url: 1920 }}" alt="...">
image_url
图片路径 → 带尺寸参数的 URL(用于商品图、分类图等业务图片)。
<img src="{{ product.image | image_url: 600 }}" alt="{{ product.title }}">
money
金额(分)→ 格式化字符串(含汇率换算)。
{{ product.price | money }}
关键特性:自动从上下文读取币种,模板无需显式传 currency 参数:
__shop_currency:店铺基础币种(商品存储币种)__currency:当前用户选择币种exchange_rates:汇率映射表
换算逻辑:商品价格以店铺基础币种存储 → 若当前币种 ≠ 店铺基础币种,按汇率换算 → 用 Intl.NumberFormat 格式化(自动除以 100)。
t
多语言翻译。
{{ 'product.addToCart' | t }}
- 从
__translations字典查找 - 不支持
defaultValue参数,未命中翻译时返回 key 本身 - 翻译优先级:DB 覆盖 > 主题 locales 文件 > 内置字典 > key 本身
标准过滤器
liquidjs 内置过滤器全部可用:default / escape / strip_html / truncate / divided_by / times / plus / minus / modulo / json / date / capitalize / downcase / upcase / split / join / slice 等。
注意:标准 Liquid 没有
escape_attr过滤器,仅有escape。
多语言 i18n
双轨制
| 轨道 | 位置 | 用途 | key 格式 |
|---|---|---|---|
| 主题 locales | locales/{locale}.json | Liquid 模板内 t 过滤器 | 嵌套 JSON(如 {"search":{"title":"Search"}},t 过滤器读取时扁平化为 search.title) |
| 内置字典 | 平台内置 | 兜底组件 + 客户端 JS | 扁平化 key(如 search.title) |
locale 文件命名
en.default.json:默认语言(必须存在)zh-CN.json:BCP 47 locale 命名
翻译优先级(最终生效顺序)
- DB 覆盖(店长在后台编辑的自定义翻译)
- 主题 locales 文件
- 内置字典
- key 本身(兜底)
客户端 JS i18n
theme.liquid 应将 client_i18n 注入 window.__STOREFRONT_I18N__:
window.__STOREFRONT_I18N__ = {{ client_i18n | json | default: '{}' }};
主题开发规范
CSS 规范
- 纯手写 CSS + CSS 变量,禁止使用 Tailwind utility class
- CSS 变量在
layouts/theme.liquid的<style>块中定义(:root选择器) - 所有主题样式统一放在
assets/theme.css
JavaScript 规范
- 所有主题脚本统一放在
assets/theme.js - 使用原生 JS(ES2020+),不依赖 jQuery / React / Vue 等框架
- 客户端 JS 通过
window.__STOREFRONT_*全局变量与服务端共享数据 - 事件驱动:CartDrawer / CartNotification / QuickAddModal 等组件通过自定义事件通信(如
storefront:cart-updated)
可访问性(A11y)
- 所有交互元素提供
aria-label/aria-expanded/aria-hidden等属性 - 图标 SVG 添加
aria-hidden="true" focusable="false" - 跳过链接(skip-link)直达正文(WCAG 2.4.1)
- 表单字段使用 WHATWG autocomplete tokens(
given-name/family-name/address-line1/postal-code/address-level2/country-name/tel/email) - 移动端增强:
autocapitalize/autocorrect/spellcheck/inputmode
模板编写约定
- Liquid 输出标签用
{{ }},控制流标签用{% %},去空格用{%- -%} - 多行注释用
{%- comment -%}...{%- endcomment -%} - section 文件首行必须为 schema 内联注释:
{%- comment -%}{"schema":{"name":"Section Name"}}{%- endcomment -%} - 判空用
== blank(检查空字符串/nil),而非unless:{% if query == blank %}
客户中心模板完整性规范(强制,2026-08-16 新增)
背景:2026-08-16 排查 printing 主题客户中心发现”整体较不完整”——consent-item / subscription-card / wishlist 列表区等渲染 class 在
theme.css无定义、订单页字段名臆造(order.orderNumber/order.total等)、地址页误用page.customer恒显”请登录”、订单详情页复用checkout__*class 但未加载定义文件、consent 按钮 JS 切换 class 与模板初始 class 契约不符。本规范为所有主题(含新开发主题)的硬约束,防止再产出残缺模板。
核心原则:客户中心 13 个页面(CUSTOMER_LOGIN/REGISTER/FORGOT_PASSWORD/RESET_PASSWORD/ACCOUNT/ADDRESSES/ORDERS/ORDER_DETAIL/WISHLIST/BACK_IN_STOCK/BROWSING_HISTORY/CONSENT/SUBSCRIPTIONS)的模板、样式、交互必须完整、对齐 Astro 兜底(_storefront-default/pages/customer/)与数据契约(storefront-data.ts 对应 loadCustomer*Data 返回类型),不得”有 section 无样式 / 有渲染无数据”。
① CSS class 全覆盖(强制)
渲染即定义:section/snippet 中每个 class="..." 静态类名都必须在该页面实际加载的 CSS 文件中有对应规则。判定方法:用脚本比对 main-customer-*.liquid 渲染的 class 与 assets/theme.css(及按需加载的 customer.css 等)定义集合,缺一即缺陷。重点高频遗漏:
- consent:
.consent-list/.consent-item/.consent-item-info/.consent-item-title/.consent-item-desc/.consent-item-meta/.consent-item-status(+.granted/.denied/.withdrawn/.not-set)/.consent-item-actions/.consent-btn-grant/.consent-btn-deny/.consent-btn-withdraw/.consent-footer/.consent-message(+.success/.error) /.customer-consent-main/.customer-consent-intro - subscriptions:
.customer-subscriptions-subtitle/.customer-subscriptions-list/.customer-subscription-card(+-header/-body/-footer/-no/-date/-info/-status-badge/-product/-plan/-billing-cycle/-quantity/-amount/-dates/-actions)/.customer-subscriptions-empty(+-text) - wishlist 列表区:
.customer-wishlist-main/.wishlist-message(+.success/.error) /.wishlist-list-section/.wishlist-list-header/.wishlist-section-title/.wishlist-create-form/-input/-public/.wishlist-empty-state/.wishlist-empty/.wishlist-list(-item/-item-main/-name/-badge/-count/-time/-delete)/.wishlist-items-section/.wishlist-items-empty - browsing-history / back-in-stock:
.customer-browsing-history-main/.customer-back-in-stock-main/.browsing-history-item-delete/.back-in-stock-item-cancel(danger 红)/.back-in-stock-item-time
② 数据契约字段精确一致(强制)
模板引用的字段名必须与 storefront-data.ts 对应 CustomerXxxPageData 接口逐字段一致,禁止凭直觉臆造(orderNumber/total/title 等均为错误)。金额一律为分,用 {{ field | money }} 输出。关键契约:
| 页面 | 数据入口(page 顶层展开) | 关键字段(分单位标注) |
|---|---|---|
CUSTOMER_ACCOUNT | customer({id,email,firstName,lastName,phone,countryCode,createdAt}) | — |
CUSTOMER_ADDRESSES | addresses[] + countryOptions[]({code,name}) | 勿用 page.customer.addresses(不存在,恒显”请登录”) |
CUSTOMER_ORDERS | orders[] | orderNo(非 orderNumber)、totalAmount(分,非 total)、orderStatus/paymentStatus/fulfillmentStatus、createdAt |
CUSTOMER_ORDER_DETAIL | order(null 时空态) | orderNo;items[] 用 productTitle/variantTitle/sku/quantity/unitPrice/totalPrice(非 title/price/total);金额 subtotal/discountAmount/shippingCost/taxAmount/totalAmount(分,非 subtotal/discount/shipping/tax/total);shippingAddress 为对象(firstName/lastName/addressLine1…),非字符串;物流用 fulfillments[](trackingNumber/carrierName/events[]),非 order.trackingNumber |
CUSTOMER_WISHLIST | wishlists[] + defaultWishlistItems[] | wishlistName/isPublic/itemCount/createdAt;商品项 productId/variantId/productTitle/productImage/variantTitle/variantPrice(分)/itemId |
CUSTOMER_BACK_IN_STOCK | subscriptions[] | subscriptionId/productId/status/notifiedAt/createdAt/productTitle/productImage/variantTitle |
CUSTOMER_BROWSING_HISTORY | history[] | historyId/productId/createdAt/productTitle/productImage/variantTitle/variantPrice(分) |
CUSTOMER_CONSENT | consents[] | consentType/consentStatus/consentVersion/consentSource/consentedAt/updatedAt |
CUSTOMER_SUBSCRIPTIONS | subscriptions[] | subscriptionNo/status/quantity/unitPrice(分)/billingPolicy/billingIntervalCount/nextRenewalAt/pausedAt/cancelledAt/createdAt/planName/productTitle |
校验方法:写模板前先
Readshopfaas-store/src/pages/_services/storefront-data.ts中对应loadCustomer*Data的返回类型(或直接Read_storefront-default/pages/customer/*Page.astro兜底组件),字段以接口为准。
③ 登录态 / 空态 / 有数据 三态必渲染(强制)
每个客户中心 section 必须处理:
- 未登录:
customer == null(或对应page.*为空数组且接口用customer:null表示未登录)→ 渲染”请登录/注册”引导(customer-auth-page-empty+ 登录/注册链接) - 已登录无数据:空态 + 去购物引导(如
.empty-state/.customer-subscriptions-empty) - 已登录有数据:列表/卡片完整渲染
数据入口判断:CUSTOMER_ADDRESSES 用 page.addresses(未登录为空数组),不能用 page.customer 判断登录态(该接口不含 customer 字段);CUSTOMER_ORDERS 等含 customer 字段的页面用 customer 判断。逐一对照上表”数据入口”列,缺字段即登录态判断失效。
④ 页面级资源可用性(强制)
section 引用的 CSS class 所属文件必须在该页面加载:
- 若复用其他页面的布局 class(如订单详情页复用结账页
checkout__layout/checkout__section/checkout__section-title,定义在checkout.css),必须确认该页面模板已加载checkout.css,或将 base 定义补进该页面实际加载的theme.css——不得”class 存在但该页未加载” - 按需加载拆分时:客户中心页面加载
customer.css(及共享theme.css),订单详情页若复用 checkout 布局类,需在订单详情模板顶部追加checkout.css或把 base 类纳入customer.css
⑤ JS 交互 class 契约(强制)
theme.js 用 classList.toggle('btn-outline'/'btn-primary') 等切换的 class 名必须与模板初始 class 一致:
- consent 按钮:模板应为
class="btn btn-outline btn-sm consent-btn-grant"(btn-outline非btn--outline),JS 对选中态切btn-primary、非选中切btn-outline;若模板用btn--outline,JS 切换的btn-primary会被.btn--outline覆盖导致 active 态不生效 - 若主题只有
btn--outline(BEM 变体)而无btn-outline,须在theme.css补.btn-outline别名(与.btn--outline同规则),或统一模板改用 JS 使用的类名 - 新交互:先读对应 Astro 兜底组件/
theme.js确认 JS 依赖的data-*钩子与 class 切换契约,再写模板
⑥ 下拉类字段用 select + 字典(强制)
地址表单的 countryCode 必须渲染为 <select>,选项来自 page.countryOptions(storefront-data.ts 从 common.country_code 字典预加载);禁止用 <input type="text"> 手输国家码。占位文案用内置字典 key(如 address.field.countryCodePlaceholder,兜底组件 AddressesPage.astro 同款),避免用主题 locales 未收录的自造 key(t 未命中返回 key 本身)。
验收清单(客户中心模板完整性)
- 13 个客户中心模板 + section 齐全,无”有 section 无模板/无 section”
- 脚本比对:渲染 class ⊆ 页面加载 CSS 定义集合(无缺失)
- 字段名逐项对照
storefront-data.ts接口(金额走| money) - 三态(未登录/空数据/有数据)均验证(chrome-mcp 登录态实测)
- 页面级资源:复用 class 的文件已在该页加载
- JS 切换 class 与模板初始 class 契约一致(consent 按钮 active 态实测)
- 国家下拉用
select+page.countryOptions,占位 key 用内置字典 - 与 Astro 兜底组件视觉/交互一致(chrome-mcp 对照)
富文本内容渲染规范(强制,2026-07-20 新增)
后台 RichTextEditor(Tiptap 3.27)输出的 HTML 含 h1-h3 / p / ul / ol / li / blockquote / a / img / pre / code / table / iframe / YouTube 16:9 / 任务列表 / 文字对齐 / 高亮标记 / 文字颜色等元素。所有渲染这类富文本的容器必须追加 rich-text-content class,并在 assets/theme.css 中包含完整样式片段,否则元素会以浏览器默认样式渲染(无边框表格、未限制宽图片、无样式引用块等),与后台编辑器视觉严重不一致。
必须追加 rich-text-content class 的位置
| PageType | 模板文件 | 渲染容器(已有 class) |
|---|---|---|
PRODUCT | sections/main-product.liquid | collapsible-content rich-text-content(商品描述折叠面板) |
BLOG | sections/main-article.liquid | article-content rich-text-content(文章正文) |
PAGE | sections/main-page.liquid | static-content rich-text-content(静态页面正文) |
COLLECTION | sections/main-collection.liquid | page-description rich-text-content(分类描述,2 处:有图/无图布局) |
平台主题(hardware/megastore/printing)已有 class 名可能是
rte/product__description rte/article__content rte/collection__description rte/page-content__body rte,追加rich-text-content与原有 class 共存即可,不替换原 class。
CSS 样式片段要求
assets/theme.css 必须包含覆盖以下元素的完整样式(参考 default/1.0.0/assets/theme.css 末尾的 .rich-text-content 片段):
- 容器:
font-size: 16px/line-height: 1.75/color: var(--color-text, ...)/ 首尾子元素 margin 归零 - 标题:
h1-h4字号 / 字重 / margin / line-height - 段落与列表:
p/ul/ol/li(含ul[data-type="taskList"]任务列表 flex 布局) - 引用:
blockquote左边框 + 斜体 - 链接:
a颜色 + 下划线 + hover - 图片:
imgmax-width: 100%/height: auto/ 圆角 - 代码:
pre深色背景 +code内联背景 + 等宽字体 - 表格:
tableborder-collapse/table-layout: fixed/width: 100%/td/th边框与 padding /th表头背景 /.tableWrapper横向滚动 /.selectedCell选中高亮 /.column-resize-handle列宽调整手柄 - 视频:
iframemax-width / YouTube 16:9 响应式容器(.rich-text-youtube/div[data-youtube-video]padding-bottom: 56.25%) - 文字对齐:
[style*="text-align: center/right/justify"] - 高亮:
mark黄色背景 - 响应式:
@media (max-width: 640px)字号略小
避免与首页 section 限宽规则冲突
default/sunset/aurora/ocean 四个 storefront 主题的 theme.css 原有 .rich-text-content { max-width: 720px; margin: 0 auto; } 规则仅用于首页 sections/rich-text.liquid section(居中限宽富文本区块)。新追加的完整样式片段不能覆盖此规则,否则商品描述/文章/页面/分类描述也会被错误限宽到 720px。
正确做法:将原有规则的选择器改为 .rich-text-section .rich-text-content(限定作用域到首页 section),新追加的完整样式片段用 .rich-text-content 通用选择器(不含 max-width/margin)。CSS 同特异性下后者先声明先生效,首页 section 因有更具体的选择器(.rich-text-section .rich-text-content)仍能正确限宽。
新主题开发检查清单
- 4 个
main-*.liquid模板中渲染富文本的容器已追加rich-text-contentclass -
assets/theme.css末尾已包含完整.rich-text-content样式片段 - 若主题含首页
rich-text.liquidsection,其限宽规则已用.rich-text-section .rich-text-content选择器限定作用域 - 在商品详情/文章/页面/分类描述页面渲染富文本时,元素样式与后台 RichTextEditor 视觉一致
共享资源
平台提供跨主题共享的 JS 模块,主题可直接引用:
chat-widget.js(在线客服)
{# 在 theme.liquid 或 section/snippet 中引入 #}
<script src="/shared/1.0.0/assets/chat-widget.js" defer></script>
引入后无需显式调用 init(),页面加载后自动查找 .chat-widget 元素并初始化。主题需提供 DOM 骨架(含 data-chat-* 配置属性),参考 default/1.0.0/snippets/chat-widget.liquid。
参考实现
default/1.0.0 是 ShopFaaS 官方标杆主题,开发者二次开发时应以此为参照:
- 25 个模板:覆盖全部 25 个 PageType(通用 12 + 客户中心 13)
- 38 个 section:含
header/footer/slideshow/main-product/main-collection/main-cart/main-checkout/main-search/main-article/main-blog/main-page/main-customer-*等 - 20 个 snippet:
card-product/cart-drawer/chat-widget/cookie-consent/customer-sidebar/facets/icon/loading-spinner/meta-tags/pagination/predictive-search/price/product-media-gallery/product-variant-picker/quantity-input/quick-add-modal/social-icons/swatch/breadcrumb/buy-buttons - 1 个 layout:
theme.liquid(主布局) - 2 个 config:
settings_data.json+settings_schema.json - 2 个 locales:
en.default.json+zh-CN.json - 2 个 assets:
theme.css+theme.js
常见错误与排查
模板修改不生效
- 主题源码是否已同步到运行时存储?
- theme-resolver L1 缓存是否未过期?(TTL 10 分钟,可用
?theme_preview=default参数绕过)
money 过滤器显示错误价格
- 检查
__shop_currency/__currency/exchange_rates是否正确注入上下文 - 检查汇率表 key 格式是否为
${from}_${to}(如USD_CNY) - 金额单位是否为「分」(整数),而非「元」(浮点)
t 过滤器返回 key 本身
- key 是否在
__translations字典中? - locale 文件是否已同步?
- 主题 locales key 是否已扁平化?
section 不渲染
- section 类型名是否与
{% section 'xxx' %}参数一致?(含大小写) sections/{xxx}.liquid文件是否存在?- 若 section 类型在
sections数组中无配置实例,标签会用空配置渲染一次(section 模板需自行处理空数据兜底)