### [收录主动推送插件|瓜奇内容发布后自动提交百度与 IndexNow](https://guaqi.com/en/topic/77948) **Published:** 2026-09-22T09:08:50 **Author:** 星动优创 **Excerpt:** 这个插件解决什么问题做内容站的人大概都干过这件事:新发一篇东西,然后一天点开三遍搜索引擎的站长后台,盯着「索引… ## **这个插件解决什么问题** 做内容站的人大概都干过这件事:新发一篇东西,然后一天点开三遍搜索引擎的站长后台,盯着「索引量」看它涨了没有。涨了,说明蜘蛛来过;没涨,就继续等,也不知道要等多久。 这不是错觉。搜索引擎发现新页面主要靠「爬」,而爬需要时间——站点权重越低、页面越深、外链越少,蜘蛛来的越慢,几天到几周都算正常。一个新发的地址,就这样躺在那里等着被摸到。 「收录主动推送」把这件事反过来做:不等蜘蛛自己找过来,内容一发布,就把地址主动递到搜索引擎的收录通道口。百度普通收录 API 和 IndexNow 都是官方为此开放的接口——你给我地址,我去抓。 但这里有个前提必须说在前面:**推送只负责「告诉对方这儿有个新页面」,收不收、什么时候收,是对方算法说了算。**这句话不是免责声明,它贯穿了这个插件的每一处文案:状态名、后台页脚、前端模块页脚,以及对外接口的字段命名。整个插件里没有一处写「已被收录」或「收录成功」。 ## **插件概览** - **插件名称**:收录主动推送 - **插件标识**:`guaqi/search-push` - **当前版本**:v1.0.0 - **推送引擎**:searchpush/1.0(纯 PHP 实现,零外部依赖) - **接入通道**:百度普通收录、IndexNow(必应 / Yandex / Seznam / Naver 共用) - **运行环境**:瓜奇(GuaQi)框架运行时插件,需 WordPress 侧支持 - **适用前端**:Nuxt SSR 站点,推送状态在服务端直出 - **上线状态**:v1.0.0 已完成交付与四套本地验证,尚未在线上启用 ## **推送不等于收录:状态分五档** 推送结果不合并成一个「成功」,而是分成五档。`accepted` 单独占一档是刻意的设计,下面会说为什么: - **已接收**(`ok`):对端明确确认收到 - **已提交待验证**(`accepted`):收下了,但还没验证身份 - **被拒绝**(`rejected`):对端明确不接受(token 无效、域名不符、限流等) - **请求失败**(`error`):请求本身没发成功(网络、超时、返回的不是 JSON) - **未启用**(`skip`):这家没开或没配,压根没发 读者在详情页上看到的不是这些代号,而是按同一原则写的中文: ```subunit ok → 已接收 accepted → 已提交待验证 rejected → 被拒绝 error → 请求失败 ``` 复制 ## **为什么「202」要单独占一档** IndexNow 有个容易被忽略的行为:**它对无效的 key 也返回 202。**202 的字面意思是「已接受处理」,但它的 key 校验是异步的——先收下,之后再自己去抓你站上的 key 文件核对。2026-09-22 拿一个明显不存在的假 key 打过去,对方照样回 202。 所以 202 说明不了任何事。如果把它写成「成功」,站长会以为自己配好了,实际上 key 文件根本没部署,推一百次也是白推。 这就是 `accepted` 单列一档的原因:它和 `ok` 都算「已送达」,但对站长说的话完全不同——一个是「对方确认了」,一个是「对方收下了,但还没验证」。汇总档位回答的是「提交这一步有没有出错」,而「验证过了没有」这个细节,由每一行的状态文案和后台上那个 key 文件自检结果来承载。 ## **两家通道,以及界面上为什么没有 Google** 能用的只有两家,都是实测确认存活的: - **百度普通收录**:`http://data.zz.baidu.com/urls`,单次最多 2000 条 - **IndexNow**:`https://api.indexnow.org/indexnow`,单次最多 10000 条 百度那条必须带一个细节:**它只能走** `http://`**,不能走 https。**实测 HTTPS 版的证书主机名跟域名对不上,握手阶段就失败了。这不是将就,是这条路目前只有这一种走法。为了防止以后有人「顺手把它改成 https」,这条已经写成构建期的断言——改了就构建失败,否则线上百度通道会永远报错,而光看代码完全看不出问题。 界面上没有 Google 的开关,不是漏做了,是**做不了**:Google 的 Indexing API 只对 `JobPosting` 和 `BroadcastEvent` 两种结构化数据开放,普通文章提交上去会被忽略;而 IndexNow 是必应发起的,Google 不参与。既然没有可用的通道,就不摆一个按下去只会显示「提交成功」的假开关——后台页里直接写明了这条原因。 ## **推送的地址是怎么算出来的** 这里有个容易翻车的点:**WordPress 的固定链接和前端路由完全不是一回事。**源站上按 `get_permalink()` 拿到的地址,切到前端域名会直接 404——前端是另一套路由,路径长得也不一样。 所以插件不用 `get_permalink()`,而是按「内容 ID + 类型路由表」现拼前端地址: ```bash post → /article/ page → /page/ docs → /doc/ product → /shop/ link → /link/ community → /topic/ ``` 复制 其中 `product` 那一行值得单独说:商品在前端的路径是 `/shop/`,不是 `/product/`。这类差异靠猜是猜不出来的。 这张表不是拍脑袋写的,是从站点 sitemap 的 24 个分片、1487 条地址里数出来的——每一种内容类型实际落在哪个前缀下,一目了然。 ## **怎么防止把配额烧光** 百度官方明确写过:重复提交旧链接会浪费配额,长期这么干会被下调配额,甚至收回权限。所以对这类插件来说,「什么时候推」比「能不能推」更要紧。做法有五条: - **内容指纹**:每篇存一个 `md5(标题 + 正文)`。这个指纹**刻意不含修改时间**——改个分类、换个标签也会刷新 `post_modified`,但正文一个字没变,推了就是白烧配额。 - **最小重推间隔 3600 秒**:刚推完又顺手改了一下的那次,不会被自动重推。 - **保存只入队,不发请求**:保存文章时同步打网络请求会把编辑页卡住,对方超时还会让保存看起来像失败了。真正的推送交给站点计划任务分批做,每批 15 篇。 - **按批打接口**,不是一篇一个请求。每篇的推送记录里会存下这一批有多大——让人知道这个状态的**精度边界**,不假装对每一条都有独立确认。(百度例外:它会逐条返回哪些 URL 无效、哪些不属于本站,那两种情况按 URL 精确落状态。) - **默认只勾 文章 / 文档 / 商品**,网址导航和社区帖子默认不勾。站上导航链接有 725 条(sitemap 实测),社区帖子上百条,全量推一遍能一次性把配额吃掉,而这些内容大多不需要主动推送。后台有黄色提示写明了这一点,真想推也能勾。 ## **IndexNow 的 key 文件要分两侧配** IndexNow 要求你的 key 能在站点根目录访问到:`https://你的域名/你的key.txt`。这里有个真实踩到的坑——**这件事在 WordPress 侧和前端侧是两件事。** **WordPress 侧(源站)插件自己处理好了**:插件在 `init` 钩子上注册了一条虚拟路由,`/任意key.txt` 直接返回 key 本身。不写物理文件、不改 nginx,绕开了面板环境下目录权限不可写的麻烦。 **但前端侧必须自己加一步。**实测:`https://8maoku.com/任意.txt` 全部由 Nuxt 接管,返回的是它自己的「页面未找到」——**前端并不把 .txt 转发给 WordPress。**所以要在前端服务器上二选一: ```abnf location = /你的key.txt { default_type text/plain; return 200 "你的key"; } ``` 复制 或者直接把 `你的key.txt` 放进 Nuxt 的 `public/` 目录(静态托管根目录),文件内容就写 key 本身。 配完之后回到后台点**「校验 key 文件」**:插件会真的去抓一次,把结果原样告诉你——`ok` / `missing` / `html` / `mismatch` / `redirect` / `empty` / `unreachable` 七种,是什么就说什么。 这一步不能省,原因前面已经说过:**IndexNow 对无效 key 也返回 202,所以推送接口的成功回执根本说明不了 key 配好了没有**,只有这个抓取校验能说明问题。 ## **前台怎么呈现** 模块 (组件类型 `guaqi.module.searchPush`) 挂在内容详情页,显示「提交给了哪几家通道」以及每家的状态。有三种情况模块会整体隐藏,不留空壳: - 当前页面不是内容详情页; - 这篇内容还没有推送过; - 推送记录里一家引擎的结果都没有。 「还没推」如果显示成一个空架子,比不显示更误导人,所以宁可不显示。 数据来自 `GET /wp-json/gqsp/v1/status?post_id=<内容ID>`,允许匿名访问,并且**刻意只给这几个字段**:`ok` / `pushed` / `engine` / `pushed_at` / `type` / `counts` / `level` / `engines`。每个引擎条目只有「名字 + 状态」两项——不下发 token、不下发配额余量、不下发原始响应。少一个字段少一分风险,读者也不需要这些。 展示顺序按「有问题的先被看到」排:`rejected` → `error` → `accepted` → `ok`。 ## **后台配置** 模块的配置项只有三个: ```json { "apiBase": "https://www.q0di.top", // 推送状态接口地址 "showEngines": true, // 是否显示各通道明细 "showNote": true // 是否显示状态说明文案 } ``` 复制 `apiBase` 留空会自动回落到站点源站。注意**不要填前端域名**——前端只反代页面,`/wp-json/` 这类接口路径在前端域名下是 404。 后台另一个页面(`工具 → 收录推送`)管的是推送本身:推送域名、启用哪几家、百度 token、IndexNow key、要推的内容类型、是否随保存自动入队、最近的推送记录表。 ## **技术规格** - **纯 PHP 引擎**:不调用 `exec`、`proc_open`、`shell_exec` 等外部程序,面板默认禁用这些函数也不影响。 - **双通道 HTTP 出口**:站点有 `wp_remote_request` 就走 WordPress 自己的请求栈,没有就回落到 curl,两条路都支持。 - **证书兼容**:两条通道遇到证书错误都会降级重试一次,避免把「我们没发成」误报成「这个通道有问题」。 - **解压判断**:按响应头声明与 gzip 魔数**双条件**判断,只看声明会被假声明坑掉。 - **同构组件**:客户端与服务端渲染共用同一份工厂源码(构建时从标记段抽取),前端不会出现「服务端渲染一套、浏览器接管后另一套」。 - **三语支持**:简体中文 / English / Español,前台文案按语言分开存放,三份语言文件与组件文案键双向对齐。 - **可复现构建**:删掉产物目录重建两次,产出的插件包逐字节一致——同样的源码永远得到同样的包。 ## **上线提醒** 下面几条是部署时真会卡住的地方: ### **1\. 模块拖上去之后必须点「发布」** 前端挂的是 Nuxt SSR 加 CDN,模块布置到页面模板后写入的是编辑态,**必须手动点一次「发布」才会生效**。不点的话前端还是旧样子,很容易被误判成插件没装上。 ### **2\. 前端侧的 key 文件要自己加** 前面对应章节说过:插件只管 WordPress 侧,前端 `.txt` 不转发。加完记得回后台点「校验 key 文件」,别靠推送接口的回执判断——它给不了这个结论。 ### **3\. 第一次真实推送得在站点上做** 交付前的验证用的是假凭据(对端只回鉴权错误),所以真实配额消耗没有被测过,插件也没有在线上跑过一次真实推送。装好之后建议按顺序走一遍:配 token 和 key → 前端加 key 文件 → 点「校验 key 文件」→ 后台点「推送待推送的」→ 再发一篇新文章,看它是否自动入队、计划任务跑完后状态是否落库。 ### **4\. 计划任务要靠站点访问带动** 分批推送走的是 WordPress 计划任务,而计划任务需要站点有访问才会执行。站点长时间没流量时,队列会一直挂着等下一次访问。这种情况可以在后台点一次「推送待推送的」手动跑一轮。 ### **5\. 内容类型别一次全勾** 默认只勾 文章 / 文档 / 商品 是有原因的:导航链接和社区帖子数量大、变动少,推它们的收益远不如推内容页。想清楚再勾,尤其是新站。 ## **小结** 「收录主动推送」把「等蜘蛛爬过来」换成「主动告诉它一声」:内容发布或更新时,把它的对外地址递到百度普通收录与 IndexNow 的通道口,状态挂在详情页上随时可查。 它把话也说得很清楚:**递出去了,不等于被收录了。**五档状态里专门留了「已提交待验证」这一档,就是不想让 202 这种「收下了但还没验」的回执,被读成「成功了」。 本文介绍的插件为 8号码库原创,v1.0.0 已完成构建与四套本地验证(引擎层、WP 层端到端、客户端组件、服务端入口),尚未在线上启用。装到站点上之后,推送地址按上面的顺序自己过一遍。 ![收录主动推送插件|瓜奇内容发布后自动提交百度与 IndexNow](http://7b2.com/wp-content/themes/b2/Assets/fontend/images/default-img.jpg) ---