大数跨境
0
0

在同一产品系列置顶不同商品:前端逻辑实现(Shopify 实战)

在同一产品系列置顶不同商品:前端逻辑实现(Shopify 实战) Shopify教程
2025-09-05
14

目标:保持一个 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 重排(快速上线)

思路

  1. 在模板里输出“置顶商品”的 handle 列表(由 collection.metafields.custom[...] 得来)。
  2. 给每个商品卡片 DOM 打上 data-handle="{{ product.handle }}"
  3. 页面加载后,用 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 个开始。

关键思路

  1. 读取置顶列表(Product 数组)。
  2. 构造一个 已显示 ID 集(字符串拼接方式即可)。
  3. 计算本页需要从“非置顶商品”中跳过多少个渲染多少个
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 %}

五、运营与使用建议

  1. 置顶顺序=后台列表顺序:Metafield 是“产品引用列表”,可在后台拖拽排序。
  2. 不同入口追加 URL 参数
  • 导航菜单 → 链接后缀加 ?scenario=ads
  • 广告着陆页 → 使用 ?scenario=sale


  1. 数据校验:置顶列表里的商品务必属于该系列;下架/缺货商品是否仍需置顶需定期检查。
  2. 监测效果:给不同场景添加 UTM,结合 GA/Pixel 追踪 CTR 与转化,优化置顶策略。

六、扩展思路(可选)

  • 按权重置顶:如果你想给每个商品一个“权重”(比如分数越高越靠前),可改用 Product Metafield 数字型(如 custom.pin_score_sale)。由于 Liquid 对“按自定义字段排序”支持有限,推荐仍用“产品引用列表”控制顺序;或用 JS 读取 DOM 上的 data-score 来排序。
  • 与搜索/筛选 App 协作:若 App 会重绘商品列表,记得在其回调里重新执行 JS 重排函数。
  • A/B 测试:准备 pinned_sale_apinned_sale_b 两套列表,通过分流链接测试不同置顶组合的效果。

七、常见问题(FAQ)

Q1:置顶商品不在当前分页里怎么办? A:A 方案(JS)会自动跳过;B 方案(Liquid)是“总体去重 + 分页”,不会重复。

Q2:会影响 SEO 吗? A:B 方案服务端已排序,对 SEO 友好;A 方案仅前端重排,一般问题不大,但建议重要系列用 B 方案。

Q3:第三方筛选导致重排失效? A:封装重排为函数,在筛选完成事件里再次调用。

Q4:如何优先级切换? A:URL 参数优先生效,其次是 Section 的默认场景设置,最后回退到 default 列表。

【声明】内容源于网络
0
0
Shopify教程
专注于Shopify开发,Shopify系统化的知识分享。
内容 4
粉丝 0
Shopify教程 专注于Shopify开发,Shopify系统化的知识分享。
总阅读61
粉丝0
内容4