小程序搜一搜的流量别白拿:页面收录、sitemap 配置与标题优化的实操记录
运营同事在群里甩了张截图:搜我们小程序名字,头三条是公众号文章和一条视频号,门店列表页一个没影。去后台看页面收录情况,43 个 page 只有 11 个进了索引,能带来点击的词全是品牌词。最意外的一条是商品详情页被我们自己写的 disallow 规则挡在门外,写这条规则的人去年已经离职。
这篇按改动顺序记一遍:两周里调整了哪些配置,为什么 web-view 页怎么配都不进索引,以及哪些动作做完其实是白费劲。
适用读者:手上已经跑着微信小程序、想在微信搜一搜里拿自然流量,页面写过一堆但从来没碰过 sitemap.json 的前端。文中配置在基础库 2.25 与 3.x 上都验证过,管理后台的菜单名字以你现在看到的为准。
先说结果:改完之后数字变成什么样
先看结论表,省得读到最后才发现不值得动手。我们做的是连锁洗护门店的小程序,采样窗口是上线后第 31 到 60 天,跟前一个 30 天同比。

| 指标 | 改造前(30 天) | 改造后(30 天) | 变化 |
|---|---|---|---|
| 已收录页面数 | 11 / 43 | 38 / 45 | +27 页 |
| 搜一搜曝光次数 | 12480 | 41260 | 约 3.3 倍 |
| 搜一搜进入 UV | 860 | 3410 | 约 4 倍 |
| 非品牌词占比 | 6% | 47% | +41 个百分点 |
| 搜索进入后次日回访 | 12% | 19% | +7 个百分点 |
非品牌词那一行最值钱。收录之前,用户必须知道我们的名字才找得到;收录之后,「附近洗车」「XX 门店电话」「洗护套餐价目表」这类短语开始能命中我们的页面。
拆细到页面看,改造前搜索进入 UV 每天不到 30,改造后稳定在 110 上下,其中带参数的门店详情页占了六成。这也是后面把 params 逐个补齐的直接原因。
搜一搜的收录机制:索引单元不是 URL,是 pagePath
网页搜索的索引单元是 URL,一个地址一个坑位。小程序不是这套玩法,抓取侧拿到的是 pagePath 加上它的参数组合。
理解这一点,后面所有配置就都说得通了。
graph TD
A[后台开通页面收录] --> B[抓取端按 pagePath 集合打开页面]
B --> C[页面从 onLoad 跑到首屏渲染]
C --> D[拍快照:导航栏标题 + 首屏文本]
D --> E{命中的 sitemap 规则是 allow?}
E -- 否 --> F[丢弃,不建索引]
E -- 是 --> G[写入索引:pagePath + 参数 + 标题]
G --> H[用户搜索触发召回]
H --> I[结果卡片:图标 + 标题 + 摘要]
图里 D 那一步最容易被忽略。抓取端不会像真人一样等着你把接口跑完,它在页面跑到首屏渲染这段窗口里取两样东西:
一是导航栏标题,来自页面 JSON 里的 navigationBarTitleText 或运行时 wx.setNavigationBarTitle 的结果;二是首屏那段可见文本,来自 WXML 里写死的文案以及 data 里第一批 setData 的数据。
这里没有类似网页 meta description 的字段可以直接填。摘要是从首屏正文里抽出来的,你写什么正文,它就抽什么。
我们后来定了条硬规矩:详情页的核心信息(门店名、品类、城市、价格区间、营业时间)必须出现在首屏静态 WXML 里,接口只补库存、评分这类会变的字段。之前的写法是整个页面一片骨架屏,等 onLoad 里两个请求回来才 setData 出正文,白屏接近 1.5 秒,抓取端看到的就是个空壳。
sitemap.json 怎么配:配了不等于放行
sitemap.json 放在小程序项目根目录,跟 app.json 平级,作用是告诉微信哪些页面可以被索引。
有个默认值得先知道:根目录不存在 sitemap.json 时,默认所有页面都允许被索引。 我们一开始以为「没配等于没收录」,事实反过来,是写错了规则才把详情页拦住的。
一份能用的配置长这样:
{
"rules": [
{ "action": "allow", "page": "pages/index/index", "params": [], "matching": "exact" },
{ "action": "allow", "page": "pages/store/list", "params": ["city"], "matching": "inclusive" },
{ "action": "allow", "page": "pages/store/detail", "params": ["storeId", "city"], "matching": "inclusive" },
{ "action": "disallow", "page": "pages/order/*" },
{ "action": "disallow", "page": "*" }
]
}
上面这份可以直接抄,四类规则分别是:无参数放行、单参数放行、多参数放行、整段前缀禁止、兜底禁止。
四个字段挨个说:
page填相对页面的 path,不带前导斜杠,*表示全部页面;params列出该页面会用到的参数名,漏写会让带参访问的版本进不了索引;matching决定参数怎么比:exact要求参数集合完全一致,inclusive要求列出的参数都出现,exclusive要求列出的参数不出现;- 规则从上往下匹配,命中一条就停下,后面的不再看。
第三、四条的顺序是刻意的。细粒度放行写前面,兜底的 disallow 写最后;反过来写的话,所有页面在第一条就被打死了。
项目里实际踩到的三条坑
| 我当时以为的效果 | 实际发生的事 |
|---|---|
| 只配 allow 首页,其他页面跟着默认放行 | 一旦写了 rules,未命中的页面会不会进索引取决于兜底规则。当时的兜底是 disallow *,结果只收录了首页 |
详情页写 pages/store/detail 就够了 |
详情页实际访问都带 ?storeId=,params 没列,带参版本不进索引,搜具体门店永远搜不到这页 |
| disallow 只是不让搜,不影响功能 | 对,用户照样能访问,所以配错在开发阶段完全看不出来,只能去后台查收录数 |
第三条最阴。这种错在测试手机上一点症状都没有,全是搜索端的哑巴亏,等运营来问才发现。
标题和描述:抓的是运行时那一帧
很多同学把优化理解成改个标题就完事,实际有两处在同时起作用。
先看标题。静态写在页面同名 JSON 里的 navigationBarTitleText,以及运行时 wx.setNavigationBarTitle 改的值,都会成为索引里的标题。详情页得按内容动态改,我们抽了个小工具统一收口:
// utils/searchable.js
// 标题拼接逻辑收在一个地方,避免每个页面各写一套
const MAX_TITLE = 18
function trim(str, max) {
// 导航栏宽度放不下太长的标题,超了会被截断,白写
if (str.length <= max) {
return str
}
// 留出省略位
return str.slice(0, max)
}
// 城市 + 门店名 + 品类,既是导航栏标题也是摘要素材
function setSearchableTitle(store) {
const title = trim(`${store.city}${store.name}|${store.category}`, MAX_TITLE)
// 改导航栏标题,抓取窗口里要能看到
wx.setNavigationBarTitle({ title })
// 同一份文案塞回 data,让首屏 WXML 渲染出来
const desc = `${store.city}${store.name}提供${store.category}服务,营业时间 ${store.hours}。`
return { title, desc }
}
module.exports = { setSearchableTitle, trim }
调用处统一放在 onLoad,并且先给一份缓存兜底:
// pages/store/detail.js
const { setSearchableTitle } = require('../../utils/searchable')
Page({
// query 里带上从列表页传过来的门店 id
onLoad(query) {
// 先拿全局缓存渲染,保证抓取那一刻页面不是空的
const cache = getApp().globalData.storeCache[query.storeId] || {}
// setData 同时更新标题变量与首屏摘要
this.setData(setSearchableTitle(cache))
// 接口回来再刷一遍真实数据
api.getStore(query.storeId).then((store) => {
// 真实数据到位后标题可能变化,需要重设
this.setData(setSearchableTitle(store))
})
}
})
再看首屏文案。摘要是从正文抽的,所以 WXML 第一段话得是完整的自然语句,别写「门店名称:」「服务类型:」这种纯标签。换成一句整话:「XX 洗护(XX 店)提供洗车、打蜡与内饰清洗,工作日 9:00 至 20:00 营业。」标签式的明细留给下面的详情区。
顺带一个真事:门店列表页原来的标题是「门店」两个字,进搜一搜之后用户看到的是两个字加一行摘要,点击率低得离谱。改成「XX 洗护门店(共 128 家)」之后,同曝光下的点击涨了快一倍。
让脚本替你守住这条线
手工配 sitemap 的问题是新增页面时没人记得回来补规则。我们加了个 Python 脚本挂在 CI 上,扫 app.json 的 pages 列表跟 sitemap.json 的 rules 做比对。
依赖:Python 3.9+,只用标准库,不需要装任何第三方包。
# scripts/check_sitemap.py
# 用法:python scripts/check_sitemap.py <项目根目录>
import json
import os
import sys
# 第一个参数是小程序项目根目录
root = sys.argv[1]
# 读出小程序声明的所有页面
with open(os.path.join(root, "app.json"), encoding="utf-8") as f:
pages = set(json.load(f)["pages"])
# sitemap 不存在时默认全部放行,所以 rules 起始为空
rules = []
sitemap_path = os.path.join(root, "sitemap.json")
if os.path.exists(sitemap_path):
# 只需要 rules 这一段
with open(sitemap_path, encoding="utf-8") as f:
rules = json.load(f).get("rules", [])
# 显式放行的页面集合
allowed = {r["page"] for r in rules if r.get("action") == "allow"}
# 显式禁止的前缀集合
blocked = {r["page"].rstrip("*") for r in rules if r.get("action") == "disallow"}
problems = []
for page in sorted(pages):
# 命中禁止前缀的页面本来就不该出现,跳过
if any(page.startswith(b) for b in blocked):
continue
# 既没被放行又没有兜底 allow,就得提醒一句
if page not in allowed and "*" not in allowed:
# 收集起来统一输出
problems.append(f"页面 {page} 没有落到 allow 规则")
for item in problems:
# 打印给 CI 日志看
print("WARN", item)
# 有可疑就让流水线红掉
sys.exit(1 if problems else 0)
脚本第一次跑出来 14 条告警,其中 3 条是真该放行的结果页,剩下 11 条本来就该关掉,是开发时留下的调试页。
整套动作串起来是这样:
flowchart LR
A[开发提交新页面] --> B[CI 跑 check_sitemap.py]
B --> C{有遗漏页面?}
C -- 是 --> D[构建失败,补齐 rules]
C -- 否 --> E[上传代码并提交审核]
E --> F[后台查看页面收录状态]
F --> G[两周后复查搜索曝光]
G --> H{收录数没涨?}
H -- 是 --> I[回头查标题与首屏文案]
web-view 页为什么不进索引
商品详情我们有一段时间是 web-view 套 H5,运营那边一直抱怨「明明有内容搜不到」。原因挺直白:
web-view 只是个容器,内容归属仍然在 H5 那个域名下。抓取端拆到这个组件时看到的不是文本,是一个跳转地址。里面那张页面要不要被索引走的是另一套规则,跟 sitemap.json 没有关系。你在 sitemap 里把这个 page 配成 allow,充其量是把容器放行,容器里仍是空的。
处理方式没有绕过去的办法,只能按内容归属拆开:
- 稳定、指望被搜索的核心内容,回退到原生页面重写;
- 频繁改动的活动页、协议页,继续用 web-view,本来也不指望它们引流。
再多嘴一句,web-view 的业务域名要在小程序管理后台登记,还得把校验文件放到该域名根目录,配错的表现是「页面打不开」,跟收录是两码事,别混在一块排查。
改完之后该盯哪些数字
收录不是即时生效的,我们观察到的节奏大概是:
- 第 2 到 3 天,后台收录数开始跳,先涨的是列表页这类参数少的页面;
- 第 7 到 10 天,带参数的详情页陆续进来,
params写全的涨得更快; - 第 3 周起长尾短语才有曝光,这时候回头调标题文案才看得出差别。
要盯的四个数是:收录页面数、曝光次数、非品牌词占比、搜索进入后的次日回访。只看曝光会被品牌词带偏,你本身的名气越大这个数字越虚。
回头盘,真正有用的动作只有两个:把 sitemap 的规则顺序和兜底理顺,把标题、正文写成抓取那一帧就能拿到的样子。剩下的都是补丁。
你们要是踩过别的坑,特别是某类页面怎么配都收不进去的,评论区贴一下具体 page 和报错,我这边能复现的就接着往下查。
参考与延伸
- 微信小程序 sitemap 配置官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html
- 页面配置(含 navigationBarTitleText):https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html
- wx.setNavigationBarTitle 接口说明:https://developers.weixin.qq.com/miniprogram/dev/api/ui/navigation/wx.setNavigationBarTitle.html
- web-view 组件与业务域名说明:https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html
微信小程序开发|小程序 SEO|搜一搜优化|sitemap.json|页面收录|动态标题|首屏渲染