小程序搜一搜的流量别白拿:页面收录、sitemap 配置与标题优化的实操记录

2026-09-23 01:21:42 0 次浏览
微信小程序微信搜一搜前端开发性能优化sitemap.json页面收录

运营同事在群里甩了张截图:搜我们小程序名字,头三条是公众号文章和一条视频号,门店列表页一个没影。去后台看页面收录情况,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 的业务域名要在小程序管理后台登记,还得把校验文件放到该域名根目录,配错的表现是「页面打不开」,跟收录是两码事,别混在一块排查。

改完之后该盯哪些数字

收录不是即时生效的,我们观察到的节奏大概是:

  1. 第 2 到 3 天,后台收录数开始跳,先涨的是列表页这类参数少的页面;
  2. 第 7 到 10 天,带参数的详情页陆续进来,params 写全的涨得更快;
  3. 第 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|页面收录|动态标题|首屏渲染

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。