Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
Liquid 开发自定义分区

自定义分区开发:先分清四层,再写代码

写 Section 最常见的错误不是语法,是层级选错:该做成 theme block 的东西写成了 section 的内联 blocks,结果商家没法跨 section 复用;该留给 app 的位置没开 @app,结果商家装了应用却插不进去,只能来找你改代码。

这一篇的顺序是:四层的区别与选型 → 平台硬限制 → 三个能直接抄的示例 → schema 速查 → 上线自检。语法细节可以查文档,选型错了要重写。


一、四层可编辑单元的区别

主题编辑器里”商家能改的东西”实际有四种载体,很多教程把它们混着讲:

载体文件位置能否跨 section 复用能否嵌套典型用途
Sectionsections/*.liquid可包含 blocks整条通栏模块:Hero、商品网格
Section blocks(内联)写在该 section 的 schema 里不能,只属于这个 section不能该 section 专用的重复项,如轮播的 slide
Theme blocksblocks/*.liquid,任何开放的 section 都能用,可多层嵌套通用组件:文字、按钮、图片、分组
App blocks由应用提供能(section 需显式开放)不能评价、订阅、尺码表等应用注入内容

选型只看两个问题:

这个东西会不会在别的 section 里也想用? 会 → theme block(放 blocks/);不会 → section 内联 blocks 就够。

商家可能想在这里插应用内容吗? 只要是内容区,答案基本都是”可能” → 在 blocks 里加 { "type": "@app" }。这一行的成本是零,缺了它商家就得改代码。

红线:不要为了”看起来现代”把所有内联 blocks 都改成 theme blocks。轮播的 slide 只服务这一个 section,做成 theme block 反而让它出现在所有开放区域的候选列表里,污染商家的选择。


二、平台硬限制(先记住,再设计)

这些是官方限制,不是建议值。设计阶段就要按它们规划:

限制数值撞上会怎样
单个 JSON 模板可渲染的 section 数25超出的 section 不渲染
单个 section 的 block 数50商家加不进第 51 个
{% for %} 循环的迭代上限50第 51 项起静默丢失,必须用 {% paginate %}
单个 section 文件的 {% schema %}1多写一个直接语法错误

第三条最容易埋雷:{% for product in collection.products %} 在集合超过 50 个商品时会静默截断,页面看着正常、少了商品。集合类循环一律套 {% paginate %}

{% schema %} 还有两个位置约束:可以放在文件任意位置,但不能嵌套在其他 Liquid 标签里;里面必须是合法 JSON,且不渲染 Liquid。


三、示例一:开放给 theme blocks 与 app blocks 的 section

这是现在最该掌握的写法。section 本身只负责”容器 + 排版”,具体内容交给 blocks。

文件sections/content-rows.liquid

<section class="content-rows" style="--content-gap: {{ section.settings.gap }}px" > {% if section.settings.heading != blank %} <h2 class="content-rows__heading">{{ section.settings.heading | escape }}</h2> {% endif %} <div class="content-rows__body"> {% content_for 'blocks' %} </div> </section> {% stylesheet %} .content-rows__body { display: grid; gap: var(--content-gap, 16px); } {% endstylesheet %} {% schema %} { "name": "内容行", "tag": "section", "class": "section-content-rows", "settings": [ { "type": "text", "id": "heading", "label": "标题" }, { "type": "range", "id": "gap", "label": "间距", "min": 0, "max": 64, "step": 4, "unit": "px", "default": 16 } ], "blocks": [{ "type": "@theme" }, { "type": "@app" }], "presets": [ { "name": "内容行", "blocks": [{ "type": "text" }, { "type": "text" }] } ] } {% endschema %}

三个要点:

{% content_for 'blocks' %} 就是那个”商家能往里塞东西”的位置。 它渲染 JSON 模板或 section group 里配置好的全部 theme blocks,顺序由商家在编辑器里拖动决定——你不需要写 for 循环,也不需要写 block.shopify_attributes(那是内联 blocks 的活)。

"blocks": [{ "type": "@theme" }, { "type": "@app" }] 是两张许可证。 @theme 允许所有 theme blocks,@app 允许应用块。也可以只列具体类型来限制范围,比如 [{ "type": "text" }, { "type": "@app" }]

动态值走 CSS 变量,不要往 {% stylesheet %} 里写 Liquid。 {% stylesheet %} 内的 Liquid 不会被渲染,写了也是字面量。把值输出到根节点的 style="--x: ...",CSS 里用 var(--x) 接。


四、示例二:theme block 本体

theme block 放在 blocks/ 目录,自带 schema。它能声明自己接受哪些子 block,从而支持嵌套。

文件blocks/text.liquid —— 文件名就是别处引用的 type,所以上一节 preset 里写的是 { "type": "text" }。这两处必须对得上,否则主题编辑器会报找不到该 block。

<div class="text-block" style="--text-align: {{ block.settings.alignment }}" {{ block.shopify_attributes }}> {% if block.settings.heading != blank %} <h3 class="text-block__heading">{{ block.settings.heading | escape }}</h3> {% endif %} {% if block.settings.body != blank %} <div class="text-block__body rte">{{ block.settings.body }}</div> {% endif %} </div> {% stylesheet %} .text-block { text-align: var(--text-align, left); } {% endstylesheet %} {% schema %} { "name": "文字", "settings": [ { "type": "text", "id": "heading", "label": "小标题" }, { "type": "richtext", "id": "body", "label": "正文" }, { "type": "text_alignment", "id": "alignment", "label": "对齐", "default": "left" } ], "presets": [{ "name": "文字" }] } {% endschema %}

注意 {{ block.shopify_attributes }} 写在最外层节点上。少写这一次,编辑器里的表现是”点了没反应""拖不动”——这个症状极常见,排查时先查这里。

需要嵌套时,在 theme block 的 schema 里同样加 "blocks",并用 presets 预置一套子结构。官方的 Group 块就是这么做的:它的 preset 里嵌了 Text 块,商家添加时直接得到一个可用的组合,而不是一个空壳。


五、示例三:静态 block(位置由你定,内容由商家填)

有些位置你希望固定存在、不允许商家删除或移动,但内容仍可编辑。用 {% content_for 'block' %} 单数形式。

文件:任意 section,例如 sections/product-header.liquid

<div class="product-header"> {% content_for 'block', type: 'price', id: 'product-price' %} </div>

typeblocks/ 下的文件名,id 是这个实例的稳定标识(用来存配置,改了等于换一个新实例,原配置丢失)。还可以传任意额外参数,在该 block 内用 {{ 参数名 }} 取到。

被静态渲染的 theme block 必须{% doc %} 头。这不是风格建议,是硬要求。

文件blocks/price.liquid

{% doc %} 渲染商品价格,含划线价与折扣标签。 @example {% content_for 'block', type: 'price', id: 'product-price' %} {% enddoc %} <div class="price" {{ block.shopify_attributes }}> {{ block.settings.product.price | money }} </div> {% schema %} { "name": "价格", "settings": [ { "type": "product", "id": "product", "label": "商品" } ] } {% endschema %}

顺带一个现代写法:"type": "product" 这类资源型设置返回的是真正的对象,不是 handle 字符串,所以可以直接 block.settings.product.price,不需要再 all_products[handle] 绕一圈。


六、内联 section blocks 什么时候还该用

不是所有东西都要迁到 theme blocks。内联 blocks 仍然是对的选择,当这个重复项只服务这一个 section

文件sections/slideshow.liquid

<div class="slideshow"> {% for block in section.blocks %} <div class="slideshow__slide" {{ block.shopify_attributes }}> {% if block.settings.image != blank %} {{ block.settings.image | image_url: width: 1600 | image_tag: widths: '400, 800, 1200, 1600', sizes: '100vw', loading: 'lazy', alt: block.settings.image.alt }} {% endif %} </div> {% endfor %} </div> {% schema %} { "name": "轮播", "limit": 1, "max_blocks": 5, "settings": [], "blocks": [ { "name": "幻灯片", "type": "slide", "settings": [ { "type": "image_picker", "id": "image", "label": "图片" } ] } ], "presets": [ { "name": "轮播", "blocks": [{ "type": "slide" }, { "type": "slide" }] } ] } {% endschema %}

这里 for + block.shopify_attributes 是必须的(和 theme blocks 相反)。图片用 image_url + image_tag 出响应式尺寸——首屏 LCP 图要改成 loading: 'eager' 并加 fetchpriority: 'high',其余装饰图保持 lazy


七、schema 速查

section schema 的全部顶层属性:nametagclasslimitsettingsblocksmax_blockspresetsdefaultlocalesenabled_ondisabled_on。挑几个容易被忽略的:

属性作用什么时候用
limit全站/单模板内该 section 最多出现几次页头、轮播这类”只该有一个”的模块设 1
max_blocks内联 block 上限(不超过 50)轮播、图标行这类多了就丑的模块
enabled_on / disabled_on限制可用的模板与 section group只适合首页的模块,避免被插到别处
tag / class包裹元素与附加类名需要稳定的 CSS 钩子时
localessection 级翻译主题要多语言分发时

enabled_on 的写法是对象,例如 { "templates": ["*"], "groups": ["footer"] }

关于 presets:它决定商家点”添加分区”时得到什么。空 preset 是很差的体验——预置两三个 block 和合理默认值,商家加进去立刻能看到成品,而不是一片空白。


八、上线自检

  • 每个 section 只有一个 {% schema %},且没嵌在其他 Liquid 标签里;
  • 内容区的 blocks 里有 { "type": "@app" }
  • 内联 blocks 的最外层写了 {{ block.shopify_attributes }}
  • 集合类 for 循环套了 {% paginate %}(别踩 50 条上限);
  • 首屏图 loading: 'eager' + fetchpriority: 'high',其余 lazy
  • {% stylesheet %} 里没有 Liquid,动态值通过 CSS 变量传入;
  • 商家可填的文本都做了 != blank 判断,空值不留空标签;
  • 用户可见文案走 {{ 'key' | t }} 并同步了 locales/en.default.json
  • 改过 schema 的 id 后,在预览主题里点一遍编辑器确认旧配置没有残留错位。

最后一条值得强调:schema 里的 id 就是存储键名。改名等于让商家已保存的配置失联,页面会静默回落到默认值。要改就写迁移说明,或者干脆保留旧 id。


九、延伸阅读


十、官方文档


小结:写 Section 前先定层级——会跨 section 复用的做成 blocks/ 里的 theme block,只服务一个 section 的留作内联 blocks,内容区一律加 { "type": "@app" }。容器 section 用 {% content_for 'blocks' %} 开口子,不需要自己写循环;内联 blocks 才需要 for{{ block.shopify_attributes }}。设计时按硬限制来:模板 25 个 section、单 section 50 个 block、for 循环 50 次(集合务必 paginate)。最后记住 schema 的 id 是存储键,改名等于丢配置。

最后更新时间: