# 用 Deep Link 引导商家安装/启用扩展

> 构造 Deep Link，让商家一键把 Shoplazza 主题扩展放到主题编辑器的指定位置：参数、插入规则与错误码。

# 用 Deep Link 引导商家安装/启用你的主题应用扩展

本文教你为主题应用扩展（Theme App Extension）构造**深度链接（Deep Link）**：商家点一下链接就被带进主题编辑器，并自动把你的扩展装到指定位置（App Block）或定位到开关引导其开启（App Embed），免去"自己去主题装修里找、加、调位置"的一连串手动操作。

:::note
本文假设你已经会编写主题应用扩展。如果还不熟扩展的代码结构与 Schema，请先阅读 [创建主题扩展](/docs/app/extensions/online-store/add-theme-extension) 与 [主题扩展参考](/docs/app/extensions/online-store/reference)。
:::

主题应用扩展有两种形态，引导方式也对应两套深度链接：

| 扩展类型 | 商家侧位置 | 引导目标 | 用哪种深度链接 |
|---|---|---|---|
| **App Block（基础扩展）** | 主题编辑器中手动添加到支持 `@app` 的卡片（Section） | 自动**插入**到指定页面 / 卡片 / block 位置 | `…/admin/card?template=…&addAppBlockId=…&target=…` |
| **App Embed（嵌入扩展）** | 主题编辑器 → 主题设置 → 应用嵌入，默认停用需手动开启 | 跳转到**应用管理**面板，定位并引导**开启开关** | `…/admin/card?context=apps&activateAppBlockId=…` |

## 适用场景

什么时候需要给商家做这种引导：

- 商家装完你的 app 后，不知道要去主题编辑器把扩展加上 / 开启，导致扩展实际没生效、使用率低。
- 版本升级：把"待配置"入口放进 app 后台，一键把商家带到正确的安装/开启位置。

## 扩展标识 `${appId}/${blockName}`

两种深度链接都要用到扩展标识，格式统一为 `${appId}/${blockName}`：

- **`${blockName}`**：扩展 `blocks/` 目录下对应 block 的文件名（去掉 `.liquid`）。例如 `blocks/t0609.liquid` → `t0609`。**本地即可确定。**
- **`${appId}`**：平台分配给 app 的 **handle 标识**（如 `painterb`、`public`、`app-add-to-cart`）。**它不在你的项目文件里**，必须从平台侧取。

### 获取方式：抓接口取 `type` 字段

接口返回里每个块的 **`type` 字段就是完整的资源标识**，形如 `shoplazza://apps/{appId}/blocks/{blockName}/{实例id}`。**两种扩展类型抓的接口不同：**

**App Embed（嵌入扩展）** —— 打开「主题编辑 → 应用管理」，看控制台请求：

```text
https://{store_domain}/admin/api/theme-extensions/available-blocks?operation=appEmbeds&limit=25
```

**App Block（基础扩展）** —— 打开主题编辑页面，看控制台请求：

```text
https://{store_domain}/admin/api/theme-extensions/available-blocks?template=index&limit=100
```

![在主题编辑器控制台抓取 available-blocks 接口](https://cnres.appracle.com/5eb32e3598ae8fcf90777c201437fef4.png)

在返回里找到你扩展对应的项（按 `name` 认领），取它的 `type`。例如：

```json
{
  "name": { "en-US": "t0609(dev)", "zh-CN": "t0609(开发)" },
  "type": "shoplazza://apps/custom-2386979-653920915276443053/blocks/t0609/653920915276443053"
}
```

去掉 `shoplazza://apps/` 前缀和末尾实例 id，即得 `${appId}/${blockName}`：

| `type` 资源标识 | 推导出的 `${appId}/${blockName}` |
|---|---|
| `shoplazza://apps/custom-2386979-653920915276443053/blocks/t0609/653920915276443053` | `custom-2386979-653920915276443053/t0609` |
| `shoplazza://apps/public/blocks/video_hero/41113124874493855` | `public/video_hero` |
| `shoplazza://apps/painterb/blocks/strengthen_trust/279078739021670375` | `painterb/strengthen_trust` |
| `shoplazza://apps/app-add-to-cart/blocks/add_to_cart/279078739021670375` | `app-add-to-cart/add_to_cart` |

:::note
全文以 `{store_domain}` 表示商家店铺的后台域名（如 `your-shop.myshoplaza.com`），实际拼链接时替换成商家自己的域名。
:::

---

## 场景 A · 引导安装 App Block（基础扩展）

App Block 需要商家手动添加到页面的卡片里。用下面的深度链接，商家点击后会跳进主题编辑器，按你指定的位置自动插入，并提示商家保存/预览。

![场景 A 引导安装 App Block 效果](https://cnres.appracle.com/14899c4a691e0502d639885d4c09a6f9.png)

### URL 基本形态

```text
https://{store_domain}/admin/card?template=${template}&addAppBlockId=${appId}/${blockName}&target=${target}
```

### 参数说明

| 参数 | 含义 | 取值说明 | 是否必填 |
|---|---|---|---|
| `template` | 目标页面模板 | 模板名见文末附录的 `template` 取值表，如 `product`、`index` | 是 |
| `addAppBlockId` | 要插入的扩展 Id | `${appId}/${blockName}`，见上文 | 是 |
| `target` | 插入目标位置 | 两种写法：`newAppsSection`，或 `sectionType:…/blockType:…/pos:…/toType:…` | 是 |
| `sectionType` | 目标卡片类型 | 支持逗号分隔多个以兼容不同主题，如 `product,product_detail`；**仅支持主题卡片**，不支持扩展/插件卡片 | 插入到指定卡片时必填 |
| `blockType` | 目标 block 类型 | 支持逗号分隔多个，如 `buy_buttons,buttons`；仅支持卡片内置的 block 类型 | 插入到指定 block 时必填 |
| `toType` | 插入类型 | `section`（作为新卡片）或 `block`（作为卡片内 block），不传时默认按 `section` 处理 | 目标 section 存在、目标 block 不存在时必填 |
| `pos` | 插入位置 | 默认 `1`（锚定点下方）；`-1`（锚定点上方） | 否 |
| `is_auto_close` | 安装完成后关闭编辑器、自动聚焦回原页面 | 配合 `window.open` 打开编辑器，返回时调用 `window.close()` | 否 |
| `desc` | 插入成功后引导弹窗的提示文案 | 多个用 `,` 分隔，与 `addAppBlockId` 一一对应 | 否 |
| `is_block_when_error` | 批量插入时某项出错是否阻断后续 | `true` 阻断；默认不阻断 | 否 |

下面按"插入精度"从粗到细给三种用法。

### 用法 ①：作为新卡片插入到任意页面

![用法①：作为新卡片插入到任意页面](https://cnres.appracle.com/1428e2120f7914bba1f34507b7142a1b.png)

不指定具体卡片，直接作为一张新卡片插入到目标页面（`target=newAppsSection`）。

```text
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=newAppsSection
```

:::caution
目标 `template` 必须在主题支持的规范内。不在规范内时默认跳转到首页，且不插入、不提示。
:::

### 用法 ②：插入到指定的目标卡片（Section）

![用法②：插入到指定的目标卡片](https://cnres.appracle.com/4e4f746808c0388403231a5c718e1136.png)

指定 `sectionType` 定位到某张卡片，用 `toType` 决定是作为相邻卡片插入还是塞进卡片内部。

```text
https://{store_domain}/admin/card?template=${template}&addAppBlockId=${appId}/${blockName}&target=sectionType:${sectionType}/pos:${pos}/toType:${toType}
```

- 插到"商品详情"卡片**下方**（作为 section）：

```text
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=sectionType:product_detail/pos:1/toType:section
```

- 插到"商品详情"卡片**上方**（作为 section）：把 `pos` 改为 `-1`。
- 塞进"商品详情"卡片**内部**（作为 block）：把 `toType` 改为 `block`。

```text
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=sectionType:product_detail/toType:block
```

:::caution
1. 作为 section 插入、且目标 section 存在时，`pos` 生效（上方/下方），默认下方。
2. 作为 block 插入、但目标 block 不存在时，位置信息不生效，默认插到卡片 block 列表最后。
3. 目标 block 不存在时**必须**指定 `toType`，否则不插入。
4. 指定的目标卡片不存在时，默认退化为"作为新卡片插入到指定页面"。
:::

### 用法 ③：插入到指定 block 的相邻位置

![用法③：插入到指定 block 的相邻位置](https://cnres.appracle.com/b65663d9b0772d2d847cd22823a0f9d0.png)

进一步指定 `blockType`，把扩展插到某个 block 的紧邻上/下方。

```text
https://{store_domain}/admin/card?template=${template}&addAppBlockId=${appId}/${blockName}&target=sectionType:${sectionType}/blockType:${blockType}/pos:${pos}
```

- 插到"商品详情"的"加购按钮"**上方**：

```text
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=sectionType:product_detail/blockType:buy_buttons/pos:-1
```

- 插到"加购按钮"**下方**：把 `pos` 改为 `1`。

:::note
1. 目标 block 存在时定位生效：`1` 下方，`-1` 上方。
2. 指定到 block 时默认插入类型即为 `block`，此时 `toType` 不生效。
3. 目标 block 已被商家删除时，默认插到目标卡片 block 列表最后。
:::

### 批量插入多个 App Block

`addAppBlockId` / `target` / `template` / `desc` 都支持用英文逗号 `,` 分隔传多个，**一一对应**，编辑器按队列逐个插入，商家用「上一步 / 下一步」翻页确认。

```text
https://{store_domain}/admin/card?addAppBlockId=appA/block1,appB/block2&target=sectionType:product_detail/toType:block,newAppsSection&template=product,product&desc=组件A已添加,组件B已添加
```

- 用 `desc` 给每个插入项单独设置成功提示文案。
- 加 `is_block_when_error=true` 后，队列中某项失败会中断、不再处理后续项；默认不中断，商家可「下一步」继续。

### 插入规则与边界（适用于以上所有用法）

平台按"页面 → 卡片 → block"逐级定位，找不到就按下面的规则回退。**若存在多个匹配的锚定位置，取最下面的那个作为基准。**

| 插入动作 | 行为 |
|---|---|
| 插入 block（页头、页尾等固定卡片同理） | 有锚定位置 → 按锚定位置插入；无锚定位置 → 插到当前卡片最后；要插入的卡片不存在 → 不插入 |
| 插入 section（卡片） | 有锚定卡片位置 → 按锚定位置插入；无锚定位置 → 插到当前可配置卡片的最下方；整个页面找不到 → 不插入、也不提示 |

此外还有几条自动行为：

- **跨页跳转**：`template` 指定的页面不是当前页时，会先跳转到目标页面再执行插入。
- **降级到底部**：找不到目标 Section、但页面未达 Section 数量上限时，组件块会被插到页面**底部**，并提示商家自行拖拽调整。
- **`@app` 嵌套查找**：目标 Section 不直接支持 `@app` 类型 Block 时，会递归向下查找第一个支持 App 插入的子 Section。

### 错误码与提示

插入失败时，编辑器会弹出对应错误提示：

| 错误码 | 含义 | 提示文案 |
|---|---|---|
| `TEMPLATE_NOT_MATCH` | 当前主题没有目标 `template` 对应的页面 | 当前主题未找到"\{page\}"模板，该卡片未添加，可联系客服进行确认 |
| `APP_NOT_FOUND` | 可用组件列表里找不到目标 App | 当前主题未找到"\{app\}"插件组件块，该卡片未添加，可联系客服进行确认 |
| `NOT_MATCH_TARGET` | `target` 格式不对或没匹配到插入位置 | 当前主题未找到目标位置，该插件卡片未添加，可联系客服进行确认 |
| `MAX_BLOCK_LIMIT` | 目标区域 Block 数已达 schema 定义的上限 | 目标区域已达到最大卡片数量，该卡片未添加，可联系客服进行确认 |

:::caution
拼接深度链接时，`target` 等含特殊字符的参数值需先做 `encodeURIComponent` 编码。
:::

---

## 场景 B · 引导激活 App Embed（嵌入扩展）

App Embed 安装后默认停用，需要商家在"主题设置 → 应用嵌入"里手动开启。用下面的深度链接把商家直接带到**应用管理**面板，定位到你的嵌入扩展并引导其打开开关。

![场景 B 引导激活 App Embed 效果](https://cnres.appracle.com/575126a0ed17f2d19242a4cf889fa586.png)

### URL 形态

```text
https://{store_domain}/admin/card?context=apps&activateAppBlockId=${appId}/${blockName}
```

示例（引导开启"加购"嵌入扩展）：

```text
https://{store_domain}/admin/card?context=apps&activateAppBlockId=app-add-to-cart/add_to_cart
```

![激活 App Embed 的深度链接示例](https://cnres.appracle.com/1d01148c2d7cbb78ad9944bd731bd00c.png)

### 参数说明

| 参数 | 含义 | 取值说明 |
|---|---|---|
| `context` | 主题编辑器侧边栏上下文 | 固定 `apps`，定位到"应用管理"面板 |
| `activateAppBlockId` | 要激活的扩展 Id | `${appId}/${blockName}`；应用管理**只支持嵌入扩展（App Embed）** |

### 自定义激活提示文案（`guidePageName`）

商家点击激活 deeplink 后，主题编辑器（平台侧）会弹出一个确认弹窗，文案结构是固定模板，只有两个槽位可由扩展控制：

- 标题：`已开启{name}` —— `{name}` 取你 block schema 的 `name`。
- 正文：`可到{guidePageName}查看效果，完成后，请保存你的更改。` —— `{guidePageName}` 默认是「商品页 / product page」，可由扩展覆盖。

![激活后的确认弹窗](https://cnres.appracle.com/1927f78abe897c86e4848acfd21aa600.png)

要自定义正文里的页面名，在 block 的 `{% schema %}` 里加一个多语言对象 `guidePageName`（写法同 `name`）：

```json
{
  "name": { "en-US": "v1-te-embed(dev)", "zh-CN": "v1-te-embed(开发)" },
  "target": "body",
  "guidePageName": { "en-US": "checkout page", "zh-CN": "结账页" },
  "settings": []
}
```

重新部署后，弹窗正文即变为：`可到结账页查看效果，完成后，请保存你的更改。`

:::note
- 不配置 `guidePageName` 时，正文默认使用「商品页」。
- 除这两个槽位外，提示语的其余文字（「可到…查看效果，完成后，请保存你的更改」）由主题编辑器写死，扩展无法修改。
:::

---

## 检测扩展是否已安装

在 app 后台展示「待配置 / 已安装」状态、决定是否给商家显示引导入口（深度链接）之前，先检测扩展当前是否已装到主题里 / 已开启。两种扩展类型的检测方式不同：

| 扩展类型 | 检测方式 |
|---|---|
| **App Block（基础扩展）** | 调 `themes/card-exist` 接口，查页面模板里是否存在该 section / block 卡片 |
| **App Embed（嵌入扩展）** | 拉主题 `settings_data.json`，在 `current.app_embeds` 里匹配并看 `disabled` |

### App Block：用 `card-exist` 接口

App Block 是被加进页面模板的 section / block 卡片。`themes/card-exist` 用于查认某个页面模板里是否包含某个 section / block 卡片。

```text
GET https://{store_domain}/admin/api/themes/card-exist?templates=...&ext_app_id=...&ext_app_block=...
```

参数：

| 参数 | 必填 | 含义 |
|---|---|---|
| `templates` | 是 | 页面模板名称，多个逗号隔开（如 `product`、`global_sections`） |
| `theme_id` | 否 | 店铺主题 id（默认当前主题） |
| `card_name` | 否 | 卡片名称 |
| `ext_app_id` | 否 | 扩展 id |
| `ext_app_block` | 否 | 扩展卡片名（blockName） |
| `show_more_info` | 否 | 是否返回 template 详情 |

![card-exist 接口返回示例](https://cnres.appracle.com/13963434d3e1df2922e70e9745d5318a.png)

返回按 `templates` 分组，每组带 `block_exist` / `section_exist`，任一为 `true` 即视为已安装：

```js
const fetchCardExist = async () => {
  const { data } = await request.get('themes/card-exist', {
    params: {
      templates: 'global_sections',
      ext_app_id: 'Geolocation',
      ext_app_block: 'multi_market',
    },
  });
  const info = data?.template_info?.global_sections;
  return info?.block_exist || info?.section_exist;
};
```

:::note
`card-exist` 是 `/admin/api/` 下的后台内部接口。app 嵌在商家后台里运行时，请求 base 已是当前店铺 admin 域名，代码里用相对路径 `themes/card-exist` 即可，无需写死域名。该接口只覆盖 App Block；App Embed 的开启状态查不到，见下一节。
:::

### App Embed：读 `settings_data.json` 匹配

App Embed 没有专用查询接口，开启状态记录在已发布主题的 `settings_data.json` 里。流程：拉配置 → 在 `current.app_embeds` 里按扩展标识匹配 → 看 `disabled`。

#### 第 1 步：拿到当前发布主题的 `theme_id`

```text
GET https://{store_domain}/openapi/2025-06/themes/default-theme
```

#### 第 2 步：拉取该主题的 `settings_data.json`

```text
GET https://{store_domain}/openapi/2025-06/themes/{theme_id}/doc?type=config&location=settings_data.json
```

（OpenAPI 开放接口，需要 access token + 主题读取 scope。）

#### 第 3 步：在 `current.app_embeds` 里匹配

`app_embeds` 以 `appId` 为键，值是该 app 的嵌入块数组，每条带 `type` 和 `disabled`：

```json
{
  "app_embeds": {
    "custom-2386979-653765666276388655": [
      {
        "disabled": false,
        "settings": [],
        "type": "shoplazza://apps/custom-2386979-653765666276388655/blocks/v1-te-embed/653765666276388655"
      }
    ]
  }
}
```

按 `${appId}/${blockName}` 匹配 `type`、并看 `disabled`：

```js
function isAppEmbedEnabled(settingsData, appId, blockName) {
  const entries = settingsData.current?.app_embeds?.[appId] ?? [];
  const targetType = `shoplazza://apps/${appId}/blocks/${blockName}`;
  return entries.some(
    (e) =>
      e?.disabled === false &&
      typeof e?.type === 'string' &&
      e.type.includes(targetType)
  );
}
// isAppEmbedEnabled(data, 'custom-2386979-653765666276388655', 'v1-te-embed') === true
```

判定：

- appId 键不存在 → 没安装。
- 命中但 `disabled: true` → 安装了但未开启。
- 命中且 `disabled: false` → 已开启。

:::caution
必须按 `blockName` 匹配 `type`，不能只看 appId 键——一个 app 可能挂多个嵌入块（同一 appId 数组里多条）。
:::

---

## 附录：`template` 取值表

`template` 参数对应商家店铺的页面模板。常用取值如下（按使用频率排序）：

| 页面 | `template` | 加载模板文件 |
|---|---|---|
| 首页 | `index` | `index.liquid` |
| 商品详情 | `product` | `product.liquid` |
| 专辑（分类）详情 | `collection` | `collection.liquid` |
| 自定义页面 | `page` | `page.liquid` |
| 购物车 | `cart` | `cart.liquid` |
| 搜索 | `search` | `search.liquid` |
| 博客文章 | `article` | `article.liquid` |
| 博客专辑 | `blog` | `blog.liquid` |
| 404 | `404` | `404.liquid` |
| 密码访问页 | `password` | `password.liquid` |
| 个人中心 | `customers/account` | `customers/account.liquid` |
| 订单列表 | `customers/order` | `customers/order.liquid` |
| 订单详情 | `order` | `order.liquid` |
| 登录 | `login` | `customers/login.liquid` |
| 注册 | `register` | `customers/register.liquid` |
| 结账页 | `checkout` | chick-next 实现 |
| 支付成功页 | `checkout` | `order_status.liquid` |
