目标:保持一个 Collection,不复制系列,根据不同“场景/入口”(比如广告落地页、活动页、导航入口),在同一系列中置顶不同商品。实现思路以前端逻辑为主,兼顾可维护性与 SEO/性能。
方案总览
我们用一组可配置的元字段(Metafields)来记录每个“场景”需要置顶的商品列表;在系列页面模板(main-collection-product-grid.liquid)里读取这些配置,并按规则先渲染置顶商品,再渲染其余商品。
两条实现路径:
- A. 快速版(前端 JS 重排 DOM):保留默认服务端渲染顺序,页面加载后用 JS 把“置顶商品” DOM 节点移动到列表顶部。优点是改动小、与分页/筛选冲突少;缺点是 首屏可能有轻微跳动(可用简易占位/隐藏过渡减少)。
- B. 进阶版(Liquid 服务端排序 + 分页处理):在 Liquid 中优先输出置顶商品,再补齐其余商品,并处理好分页数量。优点是 无跳动,SEO 友好;缺点是模板逻辑更复杂,需要仔细处理分页与去重。
建议:若你主题已接入复杂的筛选/搜索(如 Search & Discovery 或第三方过滤 App),先用 A 方案稳定上线;确定需求稳定后再切到 B 方案。
一、数据建模:用 Collection 的 Metafields 配置“置顶列表”
到 设置 → 自定义数据(Metafields) → 系列(Collections) 新建以下字段(示例):
custom.pinned_default:列表 · 产品引用(list.product_reference) — 默认场景置顶商品custom.pinned_sale:列表 · 产品引用 — 促销场景置顶商品custom.pinned_ads:列表 · 产品引用 — 广告投放场景置顶商品
也可以只建 一个字段custom.pinned(列表 · 产品引用),再建一个custom.pinned_context(单行文本)存 JSON 或标记。但多字段更直观、非技术运营也易维护。
场景选择方式
- URL 参数法(推荐):通过
?scenario=sale/?scenario=ads切换场景。便于给不同入口(菜单/广告)加不同参数。 - 固定入口法:在主题设置里给某个菜单项/Section 配置默认场景,不带 URL 参数也能生效。
二、A 方案:前端 JS 重排(快速上线)
思路
- 在模板里输出“置顶商品”的 handle 列表(由
collection.metafields.custom[...]得来)。 - 给每个商品卡片 DOM 打上
data-handle="{{ product.handle }}"。 - 页面加载后,用 JS 将在“置顶列表”中的卡片节点移动到容器顶部(保持原卡片结构),并防止重复。
模板改造(Liquid 片段)
放在 sections/main-collection-product-grid.liquid(或主题中渲染系列商品的 Section)里。
{% comment %} 1) 解析场景 {% endcomment %}
{% assign scenario_param = request.params.scenario %}
{% assign scenario = scenario_param | default: section.settings.default_scenario | default: 'default' %}
{% comment %} 2) 根据场景取对应 metafield 列表 {% endcomment %}
{% assign pinned_key = 'pinned_' | append: scenario %}
{% assign pinned_products = collection.metafields.custom[pinned_key].value %}
{%- capture pinned_handles_json -%}
[{% for p in pinned_products %}"{{ p.handle }}"{% unless forloop.last %},{% endunless %}{% endfor %}]
{%- endcapture -%}
{%- comment -%} 3) 输出一个 JSON,供前端脚本读取 {%- endcomment -%}
<script type="application/json" id="pinned-handles-json">{{ pinned_handles_json | strip_newlines }}</script>
{%- comment -%} 4) 渲染商品网格,给卡片加 data-handle 标识 {%- endcomment -%}
<div class="CollectionGrid" id="collection-grid" style="visibility:hidden;">
{%- paginate collection.products by section.settings.products_per_page | default: 24 -%}
<div class="GridInner">
{%- for product in collection.products -%}
<div class="GridItem"p">{{ product.handle }}">
{%- render 'product-card', product: product -%}
</div>
{%- endfor -%}
</div>
{%- render 'pagination', paginate: paginate -%}
{%- endpaginate -%}
</div>
{%- comment -%} 5) 轻量 JS:把置顶商品移动到顶部,最后显示容器 {%- endcomment -%}
<script>
(function(){
var jsonEl = document.getElementById('pinned-handles-json');
if(!jsonEl) return;
try {
var pinned = JSON.parse(jsonEl.textContent || '[]');
if(!Array.isArray(pinned) || pinned.length === 0) {
document.getElementById('collection-grid').style.visibility = 'visible';
return;
}
var grid = document.querySelector('#collection-grid .GridInner');
if(!grid) return;
// 从后往前插,保持 pinned 列表的既定顺序
for (var i = pinned.length - 1; i >= 0; i--) {
var h = pinned[i];
var el = grid.querySelector('[data-handle="' + CSS.escape(h) + '"]');
if(el) grid.insertBefore(el, grid.firstChild);
}
} catch(e) { console.error(e); }
document.getElementById('collection-grid').style.visibility = 'visible';
})();
</script>
说明与技巧
- 用
visibility:hidden避免首屏“跳动”(移动完成后再显示)。 - 置顶顺序由 Metafield 列表顺序决定(你在后台拖拽排序即可)。
- 与分页/筛选兼容性较好:如果某置顶商品不在当前页,脚本不会移动它(自然跳过)。
- 如使用第三方筛选 App 重绘商品列表,需要在其「渲染完成」事件后再次执行上述移动逻辑(可封装为函数并在
document.addEventListener('collection:rendered', fn)之类的自定义事件中调用)。
三、B 方案:Liquid 服务端排序(无跳动,含分页处理)
目标
- 第 1 页:先输出置顶商品,再补齐“非置顶商品”,总数恰好等于
per_page; - 第 2 页及以后:只输出“非置顶商品”的后续部分——即整体去重后的第
per_page+1个开始。
关键思路
- 读取置顶列表(Product 数组)。
- 构造一个 已显示 ID 集(字符串拼接方式即可)。
- 计算本页需要从“非置顶商品”中跳过多少个、渲染多少个。
Liquid 没有复杂数据结构,我们用简单的计数器与 contains 判断来实现。
代码示例(可直接替换产品网格渲染区)
{% assign per_page = section.settings.products_per_page | default: 24 %}
{% assign scenario = request.params.scenario | default: section.settings.default_scenario | default: 'default' %}
{% assign pinned_key = 'pinned_' | append: scenario %}
{% assign pinned_list = collection.metafields.custom[pinned_key].value %}
{%- paginate collection.products by per_page -%}
{% comment %} 1) 生成置顶 ID 串,去重用 {% endcomment %}
{% assign shown_ids = '' %}
{% for p in pinned_list %}
{% assign shown_ids = shown_ids | append: p.id | append: ',' %}
{% endfor %}
{% comment %} 2) 计算第一页应该展示多少“非置顶”:remain = per_page - pinned_count {% endcomment %}
{% assign pinned_count = pinned_list.size | default: 0 %}
{% assign remain_first = per_page | minus: pinned_count %}
{% if remain_first < 0 %}{% assign remain_first = 0 %}{% endif %}
<div class="CollectionGrid">
<div class="GridInner">
{%- if paginate.current_page == 1 -%}
{%- comment -%} 3) 首页先渲染置顶 {%- endcomment -%}
{%- for p in pinned_list -%}
<div class="GridItem"p">{{ p.handle }}">
{%- render 'product-card', product: p -%}
</div>
{%- endfor -%}
{%- comment -%} 4) 再补齐非置顶,数量为 remain_first {%- endcomment -%}
{% assign filled = 0 %}
{% for product in collection.products %}
{% if filled >= remain_first %}{% break %}{% endif %}
{% unless shown_ids contains product.id %}
<div class="GridItem"p">{{ product.handle }}">
{%- render 'product-card', product: product -%}
</div>
{% assign filled = filled | plus: 1 %}
{% endunless %}
{% endfor %}
{%- else -%}
{%- comment -%} 5) 非首页:需要跳过首页已经展示过的“非置顶”数量 {%- endcomment -%}
{% assign skipped = 0 %}
{% assign rendered = 0 %}
{% for product in collection.products %}
{% unless shown_ids contains product.id %}
{% if skipped < remain_first %}
{% assign skipped = skipped | plus: 1 %}
{% continue %}
{% endif %}
{% if rendered >= per_page %}{% break %}{% endif %}
<div class="GridItem"p">{{ product.handle }}">
{%- render 'product-card', product: product -%}
</div>
{% assign rendered = rendered | plus: 1 %}
{% endunless %}
{% endfor %}
{%- endif -%}
</div>
{%- render 'pagination', paginate: paginate -%}
</div>
{%- endpaginate -%}
注意点
- 置顶列表中的商品必须属于该系列,否则会白白占用
pinned_count。可在循环前用if collection.products contains p做校验(但该判断对 Product 对象不总是可靠,建议运营侧保证数据正确)。 - 若置顶数量 ≥ 每页数量,则首页会被置顶商品“占满”,第二页开始才显示其余商品。
- 如果你主题还有排序切换(价格/销量/最新),进阶版逻辑要在该排序之后再做“置顶 + 补齐”。
四、主题设置(可选):在自定义面板中配置默认场景
在该 Section 底部 schema 中加一个下拉选项:
{% schema %}
{
"name": "Collection 产品网格(置顶可变)",
"settings": [
{
"type": "select",
"id": "default_scenario",
"label": "默认场景",
"options": [
{"value": "default", "label": "默认"},
{"value": "sale", "label": "促销"},
{"value": "ads", "label": "投放"}
],
"default": "default"
},
{
"type": "range",
"id": "products_per_page",
"label": "每页数量",
"min": 8,
"max": 48,
"step": 4,
"default": 24
}
],
"presets": [
{"name": "Collection Grid(置顶可变)"}
]
}
{% endschema %}
五、运营与使用建议
- 置顶顺序=后台列表顺序:Metafield 是“产品引用列表”,可在后台拖拽排序。
- 不同入口追加 URL 参数:
- 导航菜单 → 链接后缀加
?scenario=ads - 广告着陆页 → 使用
?scenario=sale
- 数据校验:置顶列表里的商品务必属于该系列;下架/缺货商品是否仍需置顶需定期检查。
- 监测效果:给不同场景添加 UTM,结合 GA/Pixel 追踪 CTR 与转化,优化置顶策略。
六、扩展思路(可选)
- 按权重置顶:如果你想给每个商品一个“权重”(比如分数越高越靠前),可改用 Product Metafield 数字型(如
custom.pin_score_sale)。由于 Liquid 对“按自定义字段排序”支持有限,推荐仍用“产品引用列表”控制顺序;或用 JS 读取 DOM 上的data-score来排序。 - 与搜索/筛选 App 协作:若 App 会重绘商品列表,记得在其回调里重新执行 JS 重排函数。
- A/B 测试:准备
pinned_sale_a与pinned_sale_b两套列表,通过分流链接测试不同置顶组合的效果。
七、常见问题(FAQ)
Q1:置顶商品不在当前分页里怎么办? A:A 方案(JS)会自动跳过;B 方案(Liquid)是“总体去重 + 分页”,不会重复。
Q2:会影响 SEO 吗? A:B 方案服务端已排序,对 SEO 友好;A 方案仅前端重排,一般问题不大,但建议重要系列用 B 方案。
Q3:第三方筛选导致重排失效? A:封装重排为函数,在筛选完成事件里再次调用。
Q4:如何优先级切换? A:URL 参数优先生效,其次是 Section 的默认场景设置,最后回退到 default 列表。

