自定义分区开发:先分清四层,再写代码
写 Section 最常见的错误不是语法,是层级选错:该做成 theme block 的东西写成了 section 的内联 blocks,结果商家没法跨 section 复用;该留给 app 的位置没开 @app,结果商家装了应用却插不进去,只能来找你改代码。
这一篇的顺序是:四层的区别与选型 → 平台硬限制 → 三个能直接抄的示例 → schema 速查 → 上线自检。语法细节可以查文档,选型错了要重写。
一、四层可编辑单元的区别
主题编辑器里”商家能改的东西”实际有四种载体,很多教程把它们混着讲:
| 载体 | 文件位置 | 能否跨 section 复用 | 能否嵌套 | 典型用途 |
|---|---|---|---|---|
| Section | sections/*.liquid | — | 可包含 blocks | 整条通栏模块:Hero、商品网格 |
| Section blocks(内联) | 写在该 section 的 schema 里 | 不能,只属于这个 section | 不能 | 该 section 专用的重复项,如轮播的 slide |
| Theme blocks | blocks/*.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>type 是 blocks/ 下的文件名,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 的全部顶层属性:name、tag、class、limit、settings、blocks、max_blocks、presets、default、locales、enabled_on、disabled_on。挑几个容易被忽略的:
| 属性 | 作用 | 什么时候用 |
|---|---|---|
limit | 全站/单模板内该 section 最多出现几次 | 页头、轮播这类”只该有一个”的模块设 1 |
max_blocks | 内联 block 上限(不超过 50) | 轮播、图标行这类多了就丑的模块 |
enabled_on / disabled_on | 限制可用的模板与 section group | 只适合首页的模块,避免被插到别处 |
tag / class | 包裹元素与附加类名 | 需要稳定的 CSS 钩子时 |
locales | section 级翻译 | 主题要多语言分发时 |
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。
九、延伸阅读
- Liquid 过滤器 —
image_url、image_tag、money等常用过滤器 - 布局模板开发 —
theme.liquid与 JSON 模板的关系 - 主题设置配置 — 全局设置与 section 设置的分工
- 自定义代码片段 — snippets 与 LiquidDoc
- 性能优化 — 渲染成本与资源加载
- Shopify 元字段与元对象 — 结构化内容的承载
十、官方文档
小结:写 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是存储键,改名等于丢配置。