# 主题扩展参考

> Shoplazza 主题扩展参考：shoplazza.extension.toml 配置、App Block、App Embed Block、snippets、schema、资源引用与多语言。

# Theme extension 参考

本页是 Theme App Extension 的查阅手册：配置文件、两种区块、Snippets、Schema、资源引用与多语言。开发流程见 [创建一个主题扩展](/docs/app/extensions/online-store/add-theme-extension)。

## 配置文件（shoplazza.extension.toml）

每个 Theme App Extension 根目录下有一个 `shoplazza.extension.toml` 配置文件：

```toml
id = "617189634367044154"
name = "my-theme-app-extension"
type = "theme"
```

字段说明：

|字段|说明|
|---|---|
|`id`|扩展的唯一 ID，由 CLI 创建扩展时自动生成，不可手动修改。|
|`name`|扩展名称，在 CLI 操作和后台管理中使用。|
|`type`|固定为 `"theme"`，表示这是一个 Theme App Extension。|

## App Block（应用区块）

App Block 用于在页面中注入内联内容。商家可以在主题编辑器的 **Apps** 区域将 App Block 手动添加到支持的 Section 中。

![Image](https://cnres.appracle.com/5432c74f9117adccdfe04317cef98a80.png)

**适合使用 App Block 的场景：**

- 需要指向动态数据源的功能，如商品评分、评论展示。

- 商家可能需要在页面上调整位置的功能。

- 需要横跨页面全宽显示的功能。

:::note
App Block 默认不会在安装应用后自动出现在主题中，需要商家在主题编辑器中手动添加。
:::

### App Block 对主题的要求

App Block 要正常运行，主题的 Section 必须支持并渲染 `@app` 类型的 Block。

### 示例：app_block.liquid

我们将展示一个获取用来获取商品评分元数据的 App Block 的完整示例。

为了能够使代码能够正确允许，我们需要新增average_rating的元数据，并在商品的后台配置该元数据的值：

1、新增元数据字段average_rating

![Image](https://cnres.appracle.com/fc31a43be7626d1d67e09c71577842ba.png)

2、在商品后台设置average_rating 的值

![Image](https://cnres.appracle.com/61cd550fd50f0bdf10300adc9dd1e96b.png)

3、在商店主题编辑中，在商品详情中，添加该 theme extension

![Image](https://cnres.appracle.com/e7ae51a91baf8db18e38b6f626ed2410.png)

具体代码（`blocks/app_block.liquid`）：

```html
{% use "app_block.css" %}
{%- comment -%} 商品未配置 average_rating 时 default 为 0，避免 nil 导致渲染异常 {%- endcomment -%}
{% assign avg_rating = product.metafields.custom.average_rating | metafield_text | default: 0 | plus: 0 | round %}

<span style="color:{{ block.settings.color }}">
  {% render 'rating_stars', rating: avg_rating %}
</span>

{% if avg_rating >= 4 %}
  <div class="recommendation-text">
    <img src="{{ 'thumbs_up.svg' | asset_abs_url }}" style="width:20px; height:20px;">
    {{ 'i18n.ratings.recommendation_text' | t }}
  </div>
{% endif %}

{% schema %}
{
  "name": {
    "en-US": "Product Rating",
    "zh-CN": "商品评分"
  },
  "settings": [
    {
      "type": "color",
      "id": "color",
      "label": {
        "en-US": "Star Color",
        "zh-CN": "星星颜色"
      },
      "default": "#ff0000"
    }
  ]
}
{% endschema %}
```

`assets/app_block.css`

```css
.recommendation-text {
  color: green;
}
.myapp-floating-button-wrapper {
  color: red;
}
```

效果展示：

![Image](https://cnres.appracle.com/dd87007128299fbb0aa78a823fa83357.png)

**代码说明：**

- `{% use "app_block.css" %}`：Shoplazza 特有的资源加载方式，用于引入 `assets/` 目录中的 CSS 文件（详见后文的 "资源引用" 章节）。

- `block.settings.color`：读取 Schema 中定义的 `color` 设置项的值，商家可在主题编辑器中修改。

- `{% render 'rating_stars', rating: avg_rating %}`：引用 `snippets/rating_stars.liquid` 代码片段，并传入参数。

- `{{ 'thumbs_up.svg' | asset_abs_url }}`：获取 `assets/` 目录中图片的绝对 URL。

- `{{ 'i18n.ratings.recommendation_text' | t }}`：多语言翻译，从 `locales/` 文件中读取对应语言的文本。

- Schema 中不设置 `target` 字段时，默认为 App Block（作用于 Section）。

## App Embed Block（应用嵌入区块） {#app-embed-block}

App Embed Block 用于没有独立 UI 区域、或需要添加浮动/覆盖层元素的功能。Shoplazza 会在 HTML `</head>` 或 `</body>` 闭合标签前渲染并注入 App Embed Block。

![Image](https://cnres.appracle.com/19c262e1278c489acecd5bd91e580b5f.png)

在 Schema 中通过 `"target"` 字段指定注入位置：

|target 值|注入位置|
|---|---|
|`"body"`|`</body>` 标签前（最常用）|
|`"head"`|`</head>` 标签前|

:::note
App Embed Block 安装后默认处于停用状态，商家需要在主题编辑器的 **主题编辑 > 应用管理** 中手动激活。
:::

**适合使用 App Embed Block 的场景：**

- 浮动按钮、聊天气泡等悬浮组件。

- SEO meta 标签、数据分析、埋点追踪脚本。

- 在所有页面上生效的全局功能。

### 示例：app_embed_block.liquid

以下是一个悬浮按钮 App Embed Block 的完整示例（`blocks/app_embed_block.liquid`）。**推荐做法**：CSS / JS 写在 `assets/` 目录的独立文件中，通过 Liquid 过滤器引入，而不是把 `<style>` / `<script>` 内联到 Liquid 文件里——便于复用、CDN 缓存、避免重复加载（详见后文的 "资源引用" 章节）。

```html
{{ 'floating_button.css' | asset_abs_url | stylesheet_tag }}
{{ 'floating_button.js' | asset_abs_url | script_tag }}

<div class="myapp-floating-button-wrapper">
  <button
    type="button"
    class="myapp-floating-button"
    style="
      background-color: {{ block.settings.background_color }};
      color: {{ block.settings.text_color }};
    "
  >
    <span>{{ block.settings.button_label }}</span>
  </button>
</div>

{% schema %}
{
  "name": {
    "en-US": "Floating Button",
    "zh-CN": "悬浮按钮"
  },
  "icon": "https://assets.shoplazza.com/oss/operation/a425c6e10e32f87a1e7271c9ed9347d8.svg",
  "target": "body",
  "settings": [
    {
      "type": "text",
      "id": "button_label",
      "label": {
        "en-US": "Button Label",
        "zh-CN": "按钮标签"
      },
      "default": "Help"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": {
        "en-US": "Background Color",
        "zh-CN": "背景颜色"
      },
      "default": "#ff0000"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": {
        "en-US": "Text Color",
        "zh-CN": "文字颜色"
      },
      "default": "#ffffff"
    },
    {
      "type": "text",
      "id": "link_url",
      "label": {
        "en-US": "Click URL",
        "zh-CN": "点击跳转链接"
      },
      "default": "https://www.shoplazza.dev"
    }
  ]
}
{% endschema %}
```

:::note
**关于 `icon` 字段**：示例中的 `assets.shoplazza.com` 是 Shoplazza 内部 CDN 上的占位图标，**实际发布时请替换为你自己上传的图标 URL**。
:::

`assets/floating_button.css`

```css
.myapp-floating-button-wrapper {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 9999;
}

.myapp-floating-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 16px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
  cursor: pointer;
  transition:
    transform 0.08s ease-out,
    box-shadow 0.08s ease-out;
}

.myapp-floating-button:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
```

`assets/floating_button.js`

```javascript
document.addEventListener('DOMContentLoaded', () => {
  const btn = document.querySelector('.myapp-floating-button');
  console.log('悬浮按钮已加载, button element:', btn);
  if (!btn) return;

  btn.addEventListener('click', event => {
    console.log('悬浮按钮已点击');
    // 跳转 URL 可通过 schema 的 link_url 设置项传入；这里硬编码仅为示意
    window.open('https://www.shoplazza.dev', '_blank');
  });
});
```

效果展示：

![Image](https://cnres.appracle.com/0690078d34081012c277212bff26e220.png)

**代码说明：**

- `"target": "body"`：指定为 App Embed Block，内容注入到 `</body>` 前。App Embed Block 必须在 Schema 中显式设置 `target`。

- `"icon"`：在主题编辑器的应用嵌入列表中显示的图标，使用绝对 URL。

- `block.settings.button_label` 等：读取商家在主题编辑器中配置的设置值。

- **资源引入：** CSS / JS 通过 `asset_abs_url | stylesheet_tag` 与 `asset_abs_url | script_tag` 引入外部文件。如果确实需要在 Liquid 中内联 `<style>` / `<script>`，请把 JS 逻辑包裹在 IIFE 中避免全局变量污染——但推荐还是用外部文件，方便缓存与复用。

## Snippets（代码片段）

`snippets/` 目录用于存放可复用的 Liquid 代码片段，可在多个 Block 中引用，避免重复代码。

### 示例：snippets/rating_stars.liquid

```text
{%- if rating < 0 -%}
  {%- assign safe_rating = 0 -%}
{%- elsif rating > 5 -%}
  {%- assign safe_rating = 5 -%}
{%- else -%}
  {%- assign safe_rating = rating -%}
{%- endif -%}
{%- assign blank_stars = 5 | minus: safe_rating -%}
<span aria-label="{{ safe_rating }}/5 stars">
  {{ 'i18n.ratings.star_label' | t }}:
  {%- for i in (1..safe_rating) -%}
    ★
  {%- endfor -%}
  {%- for i in (1..blank_stars) -%}
    ☆
  {%- endfor -%}
</span>
```

:::note
用 `if / elsif` 把 `rating` 限制在 0~5 之间（Shoplazza Liquid 不提供 `at_least` / `at_most` 这类 clamp filter，可用的 math filter 见 [Math filters](/docs/theme/references/liquid/filters/math-filters)），防止评分为负数或大于 5 时渲染异常；外层 `<span aria-label>` 为屏幕阅读器提供可读文本。
:::

在 Block 中引用该 Snippet：

```text
{% render 'rating_stars', rating: avg_rating %}
```

或：

```text
{% include 'rating_stars' %}
```

**`{% render %}` 与 `{% include %}` 的区别**：

||`{% render %}`|`{% include %}`|
|---|---|---|
|变量作用域|独立作用域，不继承外部变量（需通过参数显式传入）|共享外部变量作用域|
|参数传递|通过 `with`、`for` 或具名参数传递|直接访问外部所有变量|
|推荐场景|需要明确隔离、可复用性强的片段|需要访问上下文变量的片段|

## Schema 配置

每个 Block 的 Liquid 文件末尾都需要添加一个 `{% schema %} ... {% endschema %}` 块，用于定义区块的名称、类型和可配置的设置项。每个文件只能包含一个 `{% schema %}` 块。

### 顶级属性

|属性|类型|是否必填|说明|
|---|---|---|---|
|`name`|Object|是|在主题编辑器中显示的区块名称。**必须同时提供 `en-US` 和 `zh-CN` 两种语言**。建议不超过 25 个字符。|
|`target`|String|App Embed Block 必填|区块注入位置。App Embed Block 需设置为 `"body"` 或 `"head"`；App Block 无需设置。|
|`icon`|String|否|区块在主题编辑器中显示的图标，使用图片绝对 URL。仅 App Embed Block 适用。|
|`settings`|Array|否|提供给商家的自定义设置项列表，在主题编辑器中选中区块时显示。|
|`presets`|Array|否|默认配置|
|`tag`|String|否|如果设置，则用指定 HTML 标签包裹区块输出；不设置时默认用 `div` 包裹。|
|`class`|String|否|添加到包裹标签的额外 CSS 类名。|

### name 的多语言格式

`name` 必须配置为包含 `en-US` 和 `zh-CN` 的多语言对象：

```json
{
  "name": {
    "en-US": "Product Rating",
    "zh-CN": "商品评分"
  }
}
```

### settings 配置项

每个 setting 对象的结构：

```json
{
  "type": "color",
  "id": "color",
  "label": {
    "en-US": "Star Color",
    "zh-CN": "星星颜色"
  },
  "default": "#ff0000"
}
```

|字段|是否必填|说明|
|---|---|---|
|`type`|是|设置项的控件类型（见下表）|
|`id`|是|设置项的唯一标识，在 Liquid 中通过 `block.settings.<id>` 访问|
|`label`|是|在主题编辑器中显示的标签，**必须同时提供 `en-US` 和 `zh-CN`**|
|`default`|否|设置项的默认值|

**常用 type 类型：**

|type|说明|示例默认值|
|---|---|---|
|`text`|单行文本输入框|`"默认文本"`|
|`textarea`|多行文本输入框|`"多行文本"`|
|`color`|颜色选择器|`"#ff0000"`|
|`select`|下拉选择框（需配合 `options` 字段）|`"option_value"`|
|`checkbox`|勾选框|`true`|
|`range`|滑动范围选择（需配合 `min`、`max`、`step` 字段）|`50`|
|`image_picker`|图片选择器，让商家从后台上传或选择图片|—|

**在 Liquid 中访问设置值：**

```text
{{ block.settings.color }}
{{ block.settings.button_label }}
{{ block.settings.background_color }}
```

## 资源引用

### 方式一：`{% use %}` 标签（推荐，用于加载 CSS/JS）

`{% use %}` 是 Shoplazza 提供的 Liquid 标签，用于在 Block 中加载 `assets/` 目录下的 CSS 或 JS 文件：

```text
{% use "app_block.css" %}
{% use "app_block.js" %}
```

**特点：**

- 如果同一文件在页面上被多个区块引用，只会加载一次，避免重复。

- CSS 文件会自动在 `<head>` 中注入 `<link>` 标签。

- JS 文件会自动以异步方式加载。

### 方式二：`asset_abs_url` 过滤器（用于引用图片等路径）

对于需要在 HTML 属性中动态引用资源路径的场景（如图片 `src`），使用 `asset_abs_url` 过滤器获取资源的绝对 URL：

```text
<img src="{{ 'thumbs_up.svg' | asset_abs_url }}">
```

结合其他过滤器使用：

```text
{{ 'app_embed_block.css' | asset_abs_url | stylesheet_tag }}
{{ 'app_embed_block.js' | asset_abs_url | script_tag }}
```

:::note
在 Theme App Extension 中引用 `assets/` 目录下的资源，必须使用 `asset_abs_url`，而不是主题开发中的 `asset_url` 或 `shoplaza_asset_url`。`asset_abs_url` 会根据扩展 ID 和文件哈希生成带唯一标识的路径，确保不同扩展间的同名文件不会冲突。
:::

:::warning 文件命名限制
通过 `asset_abs_url` 引用的资源文件（图片、CSS、JS 等），**文件名不能包含连字符 `-`**，可以使用下划线（`_`）代替，如 `thumbs_up.svg`、`app_block.css`。否则无法生成远程资源。
:::

**两种方式的适用场景：**

|使用场景|推荐方式|
|---|---|
|在 Block 中加载 CSS/JS 文件|`{% use "app_block.css" %}`|
|在 HTML 属性中引用图片或其他资源路径|`{{ 'thumbs_up.svg' \| asset_abs_url }}`|
|动态生成 `<link>` 或 `<script>` 标签|`{{ 'app_block.css' \| asset_abs_url \| stylesheet_tag }}`|

## 多语言（locales）

`locales/` 目录存放店面文本的多语言翻译文件，文件命名格式为 `{语言代码}.json`（如 `en-US.json`、`zh-CN.json`、`zh-TW.json`）。平台支持 15 个地区（含台湾）和 14 个国家的语言文件。

### 文件格式

**locales/en-US.json：**

```json
{
  "ratings": {
    "star_label": "Rating",
    "recommendation_text": "Recommended Product!"
  }
}
```

**locales/zh-CN.json：**

```json
{
  "ratings": {
    "star_label": "评分",
    "recommendation_text": "推荐产品!"
  }
}
```

### 在 Liquid 中使用翻译

通过 `t` 过滤器引用翻译键，键名使用点号分隔的路径，并以 `i18n.` 作为命名空间前缀：

```text
{{ 'i18n.ratings.star_label' | t }}
{{ 'i18n.ratings.recommendation_text' | t }}
```

平台会根据当前店铺语言自动加载对应语言文件中的翻译文本。

:::note
商家无法在主题编辑器中编辑 Theme App Extension 的店面多语言文本，这些文本由开发者在 `locales/` 文件中统一维护。
:::
