用 Deep Link 引导商家安装/启用你的主题应用扩展
本文教你为主题应用扩展(Theme App Extension)构造深度链接(Deep Link):商家点一下链接就被带进主题编辑器,并自动把你的扩展装到指定位置(App Block)或定位到开关引导其开启(App Embed),免去"自己去主题装修里找、加、调位置"的一连串手动操作。
主题应用扩展有两种形态,引导方式也对应两套深度链接:
| 扩展类型 | 商家侧位置 | 引导目标 | 用哪种深度链接 |
|---|---|---|---|
| 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(嵌入扩展) —— 打开「主题编辑 → 应用管理」,看控制台请求:
https://{store_domain}/admin/api/theme-extensions/available-blocks?operation=appEmbeds&limit=25
App Block(基础扩展) —— 打开主题编辑页面,看控制台请求:
https://{store_domain}/admin/api/theme-extensions/available-blocks?template=index&limit=100

在返回里找到你扩展对应的项(按 name 认领),取它的 type。例如:
{
"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 |
全文以 {store_domain} 表示商家店铺的后台域名(如 your-shop.myshoplaza.com),实际拼链接时替换成商家自己的域名。
场景 A · 引导安装 App Block(基础扩展)
App Block 需要商家手动添加到页面的卡片里。用下面的深度链接,商家点击后会跳进主题编辑器,按你指定的位置自动插入,并提示商家保存/预览。

URL 基本形态
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 阻断;默认不阻断 | 否 |
下面按"插入精度"从粗到细给三种用法。
用法 ①:作为新卡片插入到任意页面

不指定具体卡片,直接作为一张新卡片插入到目标页面(target=newAppsSection)。
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=newAppsSection
目标 template 必须在主题支持的规范内。不在规范内时默认跳转到首页,且不插入、不提示。
用法 ②:插入到指定的目标卡片(Section)

指定 sectionType 定位到某张卡片,用 toType 决定是作为相邻卡片插入还是塞进卡片内部。
https://{store_domain}/admin/card?template=${template}&addAppBlockId=${appId}/${blockName}&target=sectionType:${sectionType}/pos:${pos}/toType:${toType}
- 插到"商品详情"卡片下方(作为 section):
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。
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=sectionType:product_detail/toType:block
- 作为 section 插入、且目标 section 存在时,
pos生效(上方/下方),默认下方。 - 作为 block 插入、但目标 block 不存在时,位置信息不生效,默认插到卡片 block 列表最后。
- 目标 block 不存在时必须指定
toType,否则不插入。 - 指定的目标卡片不存在时,默认退化为"作为新卡片插入到指定页面"。
用法 ③:插入到指定 block 的相邻位置

进一步指定 blockType,把扩展插到某个 block 的紧邻上/下方。
https://{store_domain}/admin/card?template=${template}&addAppBlockId=${appId}/${blockName}&target=sectionType:${sectionType}/blockType:${blockType}/pos:${pos}
- 插到"商品详情"的"加购按钮"上方:
https://{store_domain}/admin/card?template=product&addAppBlockId=custom-2386979-653920915276443053/t0609&target=sectionType:product_detail/blockType:buy_buttons/pos:-1
- 插到"加购按钮"下方:把
pos改为1。
- 目标 block 存在时定位生效:
1下方,-1上方。 - 指定到 block 时默认插入类型即为
block,此时toType不生效。 - 目标 block 已被商家删除时,默认插到目标卡片 block 列表最后。
批量插入多个 App Block
addAppBlockId / target / template / desc 都支持用英文逗号 , 分隔传多个,一一对应,编辑器按队列逐个插入,商家用「上一步 / 下一步」翻页确认。
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 定义的上限 | 目标区域已达到最大卡片数量,该卡片未添加,可联系客服进行确认 |
拼接深度链接时,target 等含特殊字符的参数值需先做 encodeURIComponent 编码。
场景 B · 引导激活 App Embed(嵌入扩展)
App Embed 安装后默认停用,需要商家在"主题设置 → 应用嵌入"里手动开启。用下面的深度链接把商家直接带到应用管理面板,定位到你的嵌入扩展并引导其打开开关。

URL 形态
https://{store_domain}/admin/card?context=apps&activateAppBlockId=${appId}/${blockName}
示例(引导开启"加购"嵌入扩展):
https://{store_domain}/admin/card?context=apps&activateAppBlockId=app-add-to-cart/add_to_cart

参数说明
| 参数 | 含义 | 取值说明 |
|---|---|---|
context | 主题编辑器侧边栏上下文 | 固定 apps,定位到"应用管理"面板 |
activateAppBlockId | 要激活的扩展 Id | ${appId}/${blockName};应用管理只支持嵌入扩展(App Embed) |
自定义激活提示文案(guidePageName)
商家点击激活 deeplink 后,主题编辑器(平台侧)会弹出一个确认弹窗,文案结构是固定模板,只有两个槽位可由扩展控制:
- 标题:
已开启{name}——{name}取你 block schema 的name。 - 正文:
可到{guidePageName}查看效果,完成后,请保存你的更改。——{guidePageName}默认是「商品页 / product page」,可由扩展覆盖。

要自定义正文里的页面名,在 block 的 {% schema %} 里加一个多语言对象 guidePageName(写法同 name):
{
"name": { "en-US": "v1-te-embed(dev)", "zh-CN": "v1-te-embed(开发)" },
"target": "body",
"guidePageName": { "en-US": "checkout page", "zh-CN": "结账页" },
"settings": []
}
重新部署后,弹窗正文即变为:可到结账页查看效果,完成后,请保存你的更改。
- 不配置
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 卡片。
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 详情 |

返回按 templates 分组,每组带 block_exist / section_exist,任一为 true 即视为已安装:
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;
};
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
GET https://{store_domain}/openapi/2025-06/themes/default-theme
第 2 步:拉取该主题的 settings_data.json
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:
{
"app_embeds": {
"custom-2386979-653765666276388655": [
{
"disabled": false,
"settings": [],
"type": "shoplazza://apps/custom-2386979-653765666276388655/blocks/v1-te-embed/653765666276388655"
}
]
}
}
按 ${appId}/${blockName} 匹配 type、并看 disabled:
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→ 已开启。
必须按 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 |