接口和feed这类地址没有head标签,它们拿什么说自己不想被收录
本文目录
- 你的站上到底挂着几套网址?
- JSON没有head标签,它拿什么说自己不想被收录?
- WordPress早就替你说了那句noindex,你知道吗?
- 在robots.txt里封掉wp-json,为什么反而更糟?
- 一个.json结尾的网址返回了整页HTML,会怎么样?
- feed这一层为什么一直没人管?
- 前端框架站多出来的那套网址长什么样?
- 页面数据端点
- 构建号变了,旧地址怎么办
- 那份公开的路由清单
- 同一个地址的第二种格式
- 我本来以为会看到一场重复内容灾难,为什么没看到?
- 一个废参数就能看出canonical是自己想的还是照抄的?
- 这套机读表面该怎么盘一遍?
- 第一步:把端点列出来
- 第二步:看Content-Type,不看后缀
- 第三步:给每个端点定一个意图
- 第四步:用响应头表达,别用robots.txt堵
- 第五步:找第二形态
- 顺手把这几处也看一眼
- 常见问题解答
- 接口和feed被搜索引擎收录了,到底算多严重的问题?
- 直接在robots.txt里封掉这些路径,不是最省事吗?
- 怎么知道自己的站有哪些机读端点?
- X-Robots-Tag写noindex会不会连正文页一起误伤?
- 静态站生成器搭的站,是不是就没有这些问题?
- 做这套检查需要什么工具吗?
- 权威参考资料
摘要:我把60个真实站点丢进探测脚本,挨个去敲它们的接口、feed、构建产物这些机读地址,一共敲了79次,34个返回200。这34个里,只有16个在响应头里带了索引指令,而且全部集中在同一个来源——WordPress核心自己发的那句
X-Robots-Tag: noindex。10个WordPress站有8个的REST接口能直接打开,8个全带。前端框架那一侧的数据端点,29个站,一次都没有。更别扭的是第二件事:3个站在robots.txt里把
/wp-json/封了,本意是不让爬虫进去。可Google的文档写得明明白白,被robots.txt挡住的地址,上面的索引指令根本读不到。等于把核心替你说的那句noindex,亲手捂住了嘴。而换成?rest_route=这个写法,同一份JSON一字不差地又出来了,封锁形同虚设。
事情的起因很无聊。我想统计一批海外站的前端技术栈,写了个脚本挨个抓首页,顺手把响应头也存了下来。跑完翻日志的时候,发现有一列数据特别扎眼:绝大多数请求的X-Robots-Tag是空的,唯独一批JSON响应上,整整齐齐挂着noindex。
顺着这条线往下扒了一晚上,最后扒出来的东西跟我最初想查的完全无关,但比原计划有意思得多。
先说结论:你的站在给人看的那些页面之外,还挂着一套给机器看的地址。这套地址有个共同的麻烦——它们没有<head>,所以那些你熟悉的索引控制手段,一个都用不上。
你的站上到底挂着几套网址?
做SEO的人盘URL,习惯从sitemap开始。sitemap里有多少条,站上就有多少页,超出的部分算意外。这个习惯在十年前的PHP站上基本够用,今天不够了。
一个现代站点至少有三套并行的地址体系。第一套是页面,人在浏览器里看到的那些。第二套是资源,图片、CSS、字体,大家默认它们不参与索引,实际上Google的可索引文件类型清单里图片和SVG都在。第三套最容易被漏掉,我暂且叫它机读表面:它既不是页面也不是静态资源,而是CMS或前端框架为了自己运转而开出来的数据出口。
这套东西的清单比多数人想的长:
| 类型 | 典型地址 | 谁生成的 | 内容是什么 |
|---|---|---|---|
| REST接口 | /wp-json/wp/v2/posts | WordPress核心 | 文章全文的JSON版 |
| 接口的第二形态 | /?rest_route=/wp/v2/posts | WordPress核心 | 和上面一字不差 |
| 内容订阅 | /feed/、/rss.xml | 几乎所有CMS | 常带正文全文 |
| 店铺数据 | /products.json、/search/suggest.json | Shopify | 商品与搜索建议 |
| 页面数据 | /_next/data/<构建号>/xxx.json | Next.js | 那一页的全部数据 |
| 构建产物 | /_next/static/<构建号>/_buildManifest.js | Next.js | 全站路由清单 |
| 组件流 | 同一页面地址,请求头带RSC | Next.js | 另一种格式的同一页 |
| 给模型看的 | /llms.txt | 近两年新增的约定 | 站点摘要与文章清单 |
这张表里的东西,跨平台的共性比大家想的多——站内那篇独立站CMS第一年SEO隐性失分排查盘的12项通病,有好几项的根子就在这一层。
这八行里,只有最后一行是站长主动想让机器读的。其余七行都是副产品——它们存在的理由是让前端能翻页、让阅读器能订阅、让手机App能取数据,跟搜索引擎一点关系都没有。
可它们全都是HTTP地址,全都返回200,其中相当一部分内容和正式页面高度重合。爬虫不认识副产品这个概念,它只认识地址。
我选了16个站做端到端的实测:10个WordPress站、5个Shopify店、加保哥自己这个Typecho站。每个站按平台敲4到5个已知端点,一共79次请求,34次返回200。这个数字本身不重要,重要的是34个能打开的地址里,有多少个说清楚了自己想不想被收录。
JSON没有head标签,它拿什么说自己不想被收录?
先把一件基础的事挑明。你平时用的所有索引控制手段,按位置分只有三处:robots.txt在站点根目录,meta name="robots"在HTML的<head>里,rel="canonical"也在<head>里。
后两处的前提是这份响应得是HTML。JSON不是。XML不是。纯文本更不是。它们的语法里压根没有能塞元信息的地方——你总不能在JSON里插一个<meta>,那玩意儿一插进去,JSON就解析不了了,前端先崩给你看。
所以对非HTML的响应来说,能表达索引意愿的位置只剩一个:HTTP响应头里的X-Robots-Tag。这不是我的推断,Google的文档原话是——要屏蔽非HTML资源(比如PDF、视频、图片文件)的索引,改用X-Robots-Tag响应头。
那接口这种东西,搜索引擎到底会不会去索引?这里有个细节值得单独拎出来:Google判断文件类型看的是Content-Type响应头,不是网址后缀。它明确支持索引的类型里,包含XML和TXT,不包含JSON。
很多人看到这行就松口气了:JSON不在清单里,那接口就是安全的。这个推论中间少了一环。清单说的是"支持索引这些格式的内容",不是"其他格式一律不抓"。抓和索引是两件事,抓取本身就要花掉预算;更麻烦的是,后缀写着.json但Content-Type回的是text/html的地址,在Google眼里就是一个HTML页面——后面第五节会看到一个活生生的例子。
还有一层容易忽略的:XML在清单里。feed是XML,llms.txt是TXT,两个都在清单上。这两类东西被当成正经内容处理,是有文档依据的。
把这几条串起来,机读表面的处境就清楚了:
- 它没有
<head>,所以meta robots和canonical这两张牌打不出去; - 它唯一能出的牌是
X-Robots-Tag,而这张牌得由服务端主动发; - 如果你改用robots.txt去堵,爬虫连头都读不到,反而把这张牌也废了。
最后这条不是我推的,是Google写在文档里的:如果某个页面被robots.txt禁止抓取,那么关于索引或展示规则的任何信息都不会被发现,因此会被忽略。紧跟着还有一句更狠的——如果索引或展示规则必须被遵守,那么包含这些规则的网址就不能被禁止抓取。
这句话我建议做技术SEO的人背下来。它是本文后面一半内容的判据。
WordPress早就替你说了那句noindex,你知道吗?
翻WordPress核心源码的时候,我在WP_REST_Server::serve_request()里看到这么一段:
$content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json';
$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
$this->send_header( 'X-Robots-Tag', 'noindex' );设完Content-Type,下一行就是noindex。这个方法从4.4.0就在了,也就是REST API并入核心的那个版本。换句话说,WordPress从把接口开出来的第一天起,就顺手替你交代了这句话。
实测能对上。10个WordPress站,8个的/wp-json/wp/v2/posts能直接打开返回JSON,8个全部带着X-Robots-Tag: noindex。剩下两个一个是301跳走了,一个的前端已经换成了框架,接口路径直接404。
这8个站里有做SEO工具的,有做主机的,有做媒体的,技术团队水平和关注点差得很远,但这一项完全一致——因为它压根不需要谁去关注,是核心默认行为。
顺带说个有意思的细节:elementor.com那个接口返回的内容长度是2个字节,也就是一对空的方括号。文章被过滤掉了,接口本身照样开着,noindex照样发。这就是默认行为的好处,它不挑场景。
然后我去看前端框架那一侧。29个Next.js站,所有数据端点、所有构建产物,X-Robots-Tag的命中次数是0。
这个对比挺让人意外的。这些年"WordPress老了"的说法没断过,可在"接口要不要跟搜索引擎打个招呼"这件事上,被嫌弃的那一套是唯一替你想过的。新一代框架不是想错了,是这件事压根不在它的职责表上——框架管的是渲染和路由,索引控制在它眼里属于业务层的事,谁的业务谁自己写。
这里有个可迁移的判断:凡是一件事没有明确的责任人,它的默认状态就是"没做"。框架不做,因为它认为这是业务层的事;业务层不做,因为大家默认框架已经处理好了。两边都讲得通,中间那块就空着。
在robots.txt里封掉wp-json,为什么反而更糟?
知道接口能被抓之后,最直觉的动作是什么?去robots.txt里加一行Disallow: /wp-json/。
我实测的10个WordPress站里,有3个就是这么干的。这3个站的robots.txt都不短,一个45条禁止规则,一个17条,一个15条,看得出来是有人认真维护的。
问题在于,这个动作和核心那句noindex是互斥的。
回到上面那两句Google的原话:被禁止抓取的地址,上面的索引规则不会被发现,会被忽略。也就是说,你封了/wp-json/之后,爬虫不会去请求它,自然也读不到响应头里的noindex。核心替你说的那句话,被你自己的robots.txt捂住了。
那会怎样?如果这个地址从来没有外链指向它,无事发生。如果有——比如某个开发者在论坛上贴了一段调用你接口的示例代码——搜索引擎知道有这么个地址,却被禁止进去查看,只能凭外部信号决定要不要收录。这就是GSC里那条"已编入索引,但被robots.txt屏蔽"的经典成因,站内那篇讲GSC索引覆盖状态机制的文章把这条状态的判定路径拆得更细。
更尴尬的是第二件事。WordPress的REST API有两种写法:
/wp-json/wp/v2/posts?per_page=1
/?rest_route=/wp/v2/posts&per_page=1第二种是固定链接没开启时的兜底形态,官方一直支持。我对那3个封了/wp-json/的站分别敲了两次:
| 站点 | /wp-json/形态 | ?rest_route=形态 | 前200字节是否一致 | robots.txt封了它吗 |
|---|---|---|---|---|
| 科技媒体站A | 200,28252字节 | 200,28252字节 | 是 | 没有 |
| 电商插件官网 | 200,24701字节 | 200,24701字节 | 是 | 没有 |
| SEO媒体站 | 200,24611字节 | 被前端接管,返回HTML | 否 | 没有 |
前两个站,同一份JSON从另一扇门原封不动地出来了,长度一个字节不差,X-Robots-Tag: noindex也还在。我一共查了6个站的robots.txt,写了rest_route三个字的是0个。
所以这个封锁的实际效果是:把带noindex的那条路封了,把同样带noindex的另一条路留着。两条路通向同一份数据,唯一的差别是爬虫在第一条路上读不到指令,在第二条路上读得到。
正确的做法反而更省事——什么都不做。核心已经发了noindex,让爬虫进去读到它,比拦在门外强。如果确实不希望接口被爬(比如接口很慢、占资源),那要解决的是性能问题,用限速或者鉴权,不是用robots.txt。robots.txt和meta robots各自的适用场景,站内那篇专门讲两者别搞反的文章列了完整的分工表。
一个.json结尾的网址返回了整页HTML,会怎么样?
Shopify这边我原本预期会很平淡。/products.json是个流传很广的地址,很多人拿它扒竞品的商品数据;/search/suggest.json是官方文档里写明的预测搜索接口,路径就长这样。
5个店里有4个的结果确实平淡:403或者404,前端换成了自建框架,这些老路径已经不通了。第5个店让我盯着看了很久。
| 请求的地址 | 状态码 | Content-Type | 响应体大小 | 页面里的canonical | meta robots |
|---|---|---|---|---|---|
/collections/all/products.json | 200 | text/html | 627840字节 | 指向自己(带.json) | index,follow |
/search/suggest.json?q=shorts | 200 | text/html | 626730字节 | 指向自己(带.json) | index,follow |
/collections/all/products.atom | 200 | text/html | 627840字节 | 指向自己(带.atom) | index,follow |
/collections/all(真页面) | 200 | text/html | 3921665字节 | 指向自己 | index,follow |
三个后缀是.json和.atom的地址,各自返回了一整页HTML,六十多万字节,带着完整的<head>、一条JSON-LD结构化数据、一个自称是规范网址的canonical,以及一句响亮的index,follow。
把第二节那条规则套上来:Google按Content-Type判断文件类型。这三个地址的Content-Type是text/html。所以在爬虫眼里,它们不是接口,就是三个普通网页,而且是三个自我认证过、明确表示欢迎收录的普通网页。
这个组合的杀伤力在于它是自洽的。它不像常见的重复内容那样露出破绽——没有指向别处的canonical,没有noindex,没有404,甚至连标题都在。审计工具扫过去,它是一个健康页面。
成因不难猜:headless前端接管了路由之后,凡是匹配不上已知页面的路径,一律走兜底渲染,把首页或者集合页的内容吐出来。开发的时候这么写没问题,用户不会去访问.json结尾的地址。问题是这些地址不是凭空来的,它们是平台的标准端点,外部早就有链接、有工具、有教程指着它们。
这里能提炼出一条判据,比这个案例本身更值钱:网址后缀不决定这个地址的身份,Content-Type才决定。你以为你在提供一个接口,服务器却在提供一个网页,两边的认知差就是索引膨胀的入口。站内讲索引膨胀诊断的那篇是从全站机制的角度讲成因地图,这类兜底渲染属于其中最难自查的一种,因为你根本不知道该去查哪些地址。
还有个反过来的观察也值得记:同一个平台,前端换一套,结论就整个反过来。同样5个Shopify店,4个的这些路径已经彻底不通,1个全部200。所以"Shopify会不会有这个问题"这种问法本身就是错的,得一个站一个站地敲。
feed这一层为什么一直没人管?
feed是这批端点里最老的一个,老到大家已经不把它当回事了。但它有三条性质凑在一起,其实挺值得看一眼。
第一,它是XML,而XML在Google明确支持索引的文件类型清单里。第二,很多站的feed带content:encoded字段,也就是正文全文,不是摘要。第三,它同样没有<head>,索引控制只能靠响应头。
我测了8个feed:
- 响应头带索引指令的:1个,写的是
noindex, nofollow,明显是插件或者人为加的; - 另外7个:响应头干干净净,什么都没说;
- 带全文的:一个主机商站点的feed里有15条
content:encoded,一个SEO博客有3条,保哥自己的站有13条。
顺手量了一下体积,保哥这个站的/feed/是628918字节,六十多万。七篇文章的全文,加上一堆命名空间声明,就这么大。
然后我查了自己站的robots.txt,发现/feed/是被Disallow掉的。
这就有点难看了——上一节我刚说完封/wp-json/是个错误动作,自己这边对feed干的是同一件事,而且更糟:WordPress的接口至少有核心发的noindex兜着,feed这边我什么都没发,封掉之后爬虫既读不到指令,也判断不了意图。
写到这儿我把它记进了待修清单,处置方式想好了两步:feed本身改成发X-Robots-Tag: noindex, follow,让链接权重还能流走;robots.txt里那条Disallow删掉,否则前一步白做。这两步的先后顺序不能反,先删封锁再发头,中间会有一小段窗口期是完全裸奔的。
要不要给feed加noindex,其实取决于你怎么用它。如果feed是给聚合站和阅读器用的,加;如果你指望feed被搜索引擎当内容收录(有些新闻站确实这么用),那就别加,但要把canonical的问题想清楚——feed里的每个<link>指向原文,这本身就是最强的归属声明。内容一稿多发时的归属判定,站内讲内容联合发布的那篇把机制讲得更透。
前端框架站多出来的那套网址长什么样?
60个候选域名跑下来,29个用的是Next.js。这一节的数据全部来自这29个站。
框架给客户端导航准备的数据出口,主要是这么几类:
页面数据端点
老一点的路由模式下,每个页面都有一个对应的JSON地址,形如/_next/data/<构建号>/about.json。我实测到的几个:一个视频工具站61370字节,一个DTC品牌站85118字节,一个流媒体站654915字节——六十多万字节的JSON,内容就是那一页渲染所需的全部数据。
这些地址返回的Content-Type是application/json,符合预期。X-Robots-Tag一个都没有。
构建号变了,旧地址怎么办
那串构建号每次部署都会重新生成。也就是说昨天那批JSON地址,今天全部作废。我拿一个瞎编的构建号去敲,看各站怎么应对:
| 站点类型 | 伪造构建号的响应 | 响应体大小 |
|---|---|---|
| 协作文档站 | 404 | 97471字节 |
| 视频工具站 | 404 | 49947字节 |
| 身份认证服务商 | 404 | 138562字节 |
| DTC营养品站 | 404 | 71932字节 |
| 企业金融站 | 200 | 91128字节的HTML |
| 大型零售站 | 307,跳到风控页 | 21字节 |
大部分站是404,这个行为是对的。但有两件事顺带暴露出来:一是这些404页面动辄五万到十三万字节,一个真404回这么多内容,抓取预算上不太划算;二是那个返回200的站,等于把所有作废的旧数据地址都变成了软404,每一个都是能被收录的HTML。
那份公开的路由清单
还有个东西比数据端点更有意思:_buildManifest.js。它是框架用来做预取的路由映射表,公开可读,缓存策略是public, max-age=31536000, immutable——缓存一年,永不变更。
我从中数出来的路由条数:一个协作文档站133条,一个身份认证服务商136条,一个DTC品牌站108条,一个金融站55条,一个视频工具站41条。
这份清单和sitemap是两回事。sitemap是你主动声明"这些页面希望被收录",路由清单是构建工具客观记录"这个应用有这些路径"。后者通常是前者的超集,里面会有登录页、内部工具页、还没上线的路由。
它本身不是漏洞,路由存在不等于内容能访问。但对做审计的人来说,这是一份免费的对照组——你可以用它反过来查sitemap漏了什么,也可以查有哪些路由是你根本不想让人知道存在的。
同一个地址的第二种格式
新的路由模式下,同一个页面地址在带上特定请求头之后,返回的不是HTML而是组件流。差别有多大?一个部署平台的首页,HTML版586751字节,组件流版286007字节;一个数据库服务商的首页,1313647字节对67539字节。
正常情况下这没问题,因为响应里带了Vary头,告诉缓存层"这个地址的响应取决于请求头"。但我在两个站上看到了不一致:普通请求回的Vary只有Accept-Encoding,带上那个请求头之后回的Vary才包含它。
这种不一致意味着缓存键的算法取决于请求本身。中间任何一层缓存如果按第一种响应建了键,就有可能把组件流喂给一个普通访客——包括爬虫。这类"层与层接缝上的错"最难查,因为每一层单独看都是对的。
最后是这29个站的整体设防情况:robots.txt里写了/_next相关规则的,1个;提到组件流那个参数的,1个;任何一个数据端点带X-Robots-Tag的,0个。
我本来以为会看到一场重复内容灾难,为什么没看到?
这一节讲的是我的判断被自己的数据推翻的过程。留在这里不是为了自嘲,是因为推翻的过程本身有方法论价值。
我的原始假设是这样的:框架为客户端导航生成的那些地址,尤其是带特殊参数的那种,会造出一大批和正式页面近乎一致的URL。它们能返回200,内容重复度接近100%,又没有任何索引控制,所以应该能看到成规模的重复内容问题。
第一步的数据完全支持这个假设。我给29个站的首页都加上那个框架参数,29个全部返回200,而且返回的是完整HTML。字节差异小到荒唐:一个站是0字节差,另一个站差4个字节。这就是两个一模一样的页面挂在两个不同的地址上。
然后我加了个对照组,把参数换成一个毫无意义的?zzz=1。结果也是29个全部200,内容同样一致。
这个对照组很重要,它一下子改变了结论的性质:这不是框架特有的问题,这是所有站的通病。任何站,任何地址,加上任何一个它不认识的查询参数,都会返回一个完整页面。这个行为从Web诞生起就是这样,跟前端框架半点关系没有。做参数治理的人对此再熟悉不过,站内讲重复内容排查和srsltid参数处置的两篇都是围着这件事转的。
那为什么这么多年,参数变体没有把所有站淹掉?我接着往下查,答案是canonical。
29个站的首页里,canonical指回干净地址的有26个。也就是说,那道防线一直在,而且是唯一在的那道。robots.txt没设防,响应头没设防,参数处理没设防,全靠<head>里那一行<link rel="canonical">。
那另外3个呢?两个站的首页压根没有canonical,一个站的canonical跟着请求地址变了。我又扩大样本,把45个具体页面(覆盖WordPress、Shopify、静态站生成器、Typecho、Next.js)挨个测了一遍,没有canonical的有8个,其中包括两个用静态站生成器搭的技术文档站——它们的模板默认就不输出canonical。
Next.js的官方文档把这件事说得很直白:即使一个路由完全没定义元数据,框架也只会自动加两个meta标签,一个是字符编码,一个是视口。canonical要自己在alternates.canonical里写。没写就是没有。
所以最终的结论要改一个方向。原来我以为要写的是"框架制造了一场重复内容灾难",实际情况是"灾难没发生,因为最后一道防线恰好在;而这道防线是站长自己或者插件补上的,不是框架给的,也没有第二道"。
这个转向对我自己也是个提醒:一个风险没有爆发,不等于防护体系是健全的,可能只是恰好剩了一道防线还没塌。审计的时候要数的不是"出没出事",是"还剩几层"。
一个废参数就能看出canonical是自己想的还是照抄的?
上一节留了个尾巴:有个站的canonical跟着请求地址变了。这件事值得单独说,因为它引出了一个我认为极其好用的自查动作。
canonical这个标签,生成方式其实分两种:
- 声明式:由内容的身份算出来。这篇文章的固定链接是什么,canonical就是什么,跟你怎么访问它没关系。
- 回声式:由当前请求拼出来。取一下当前URL,或者取
Host加REQUEST_URI,拼一拼输出。
两种写法在正常访问下的输出一模一样,看页面源码分辨不出来。但回声式有个致命性质:你访问它的每一个变体地址,它都会认证那个变体是规范的。这等于canonical这道防线自动对所有变体投降。而canonical本来就只是个强信号不是命令,Google最终选哪个版本还有一整套自己的判定逻辑,站内拆解这套选择逻辑的那篇讲了它会参考哪些信号。
分辨这两者只需要一次请求:
curl -s "https://example.com/some-page/?zz9pza=1" | grep canonical加一个绝不可能有业务含义的参数,然后看canonical里有没有它。变了,就是回声式;没变,就是声明式。十秒钟的事,不用登服务器,不用看代码。
我用这个方法测了45个地址,横跨5类平台,结果比预期的好:
| 平台 | 测了几个地址 | canonical跟着参数变 | 没有canonical |
|---|---|---|---|
| WordPress | 10 | 0 | 0 |
| Shopify | 5 | 0 | 被风控挡掉4个 |
| 静态站生成器 | 2 | 0 | 2 |
| Typecho(本站) | 3 | 0 | 0 |
| Next.js | 24 | 0 | 2 |
深层页面里一个回声都没抓到。抓到的两个案例都在首页或者登录页这种特殊位置:一个眼镜DTC品牌的首页,一个流媒体站的登录页,它们在带框架参数访问时,canonical把参数原样带上了。
WordPress那一栏的0需要一句解释,不然会误读。核心的rel_canonical()只对单篇内容页输出canonical,分类页、首页、分页它是不管的。我测的10个WordPress地址里有好几个是分类页和列表页,它们的canonical是SEO插件补的。
所以这一栏真正的结论是"主流插件写的是声明式",不是"WordPress核心保证了这件事"。一个没装SEO插件的WordPress站,分类页根本没有canonical可测。插件和主题各写一份canonical导致页面上冒出两条的情况也不少见,站内讲插件与主题标签冲突怎么归一的那篇专门拆过这类事故。
Typecho这边同理,本站三类页面的canonical是主题模板输出的,站内那篇讲Typecho各类页面meta robots和canonical配置的文章记录了这套规则怎么定的。
这个测法还能顺手扩展到另外三个字段,成本是同一次请求:
og:url:很多站的它和canonical不是一套逻辑生成的,45个地址里我没测出回声,但值得一起看;- JSON-LD里的
url和@id:这两个字段回声的概率比canonical高,因为它们经常是模板里随手拼的; hreflang:多语言站上如果它是回声式,会把参数变体互相指认成语言版本,这个错误比canonical回声更难发现。
把这四个字段放进同一个检查动作,是我现在做站点体检的固定项。JSON-LD那几个字段的检查更细的做法,站内讲结构化数据审计的那篇有完整流程。
⚡ 顺手一起用:API测试工具
本文里的每一条判断——状态码是多少、Content-Type回的什么、有没有X-Robots-Tag——都是从响应头里读出来的。逐个开终端敲curl太慢,把地址丢进去一次看全,还能改请求头做对照。
这套机读表面该怎么盘一遍?
把前面的东西收成一套能执行的动作,五步,半小时之内能跑完一个站。
第一步:把端点列出来
按平台对号入座,用本文第一节那张表当清单。WordPress查/wp-json/和它的?rest_route=形态、/feed/、各分类各标签的feed;Shopify查/products.json、/collections/xxx/products.json、/search/suggest.json、各种.atom;框架站查/_next/下面那几类,构建号从页面源码里直接能读到。
前端和内容层分开之后,sitemap、重定向这些基建都得自己重搭一遍,站内讲Headless上线后基建重建的那篇列了完整的搬迁清单,这份端点表可以直接并进去。
第二步:看Content-Type,不看后缀
每个地址记三样:状态码、Content-Type、响应体大小。后缀写.json而Content-Type回text/html的,立刻标红——它在搜索引擎眼里是网页,不是接口。
第三步:给每个端点定一个意图
只有三种选项,别含糊:
| 意图 | 典型端点 | 怎么表达 |
|---|---|---|
| 希望被读、被收录 | sitemap、llms.txt | 什么都不加,放行 |
| 允许被读,但别收录 | REST接口、feed、页面数据端点 | 发X-Robots-Tag: noindex,robots.txt里不要封 |
| 压根不该对外 | 后台、内部接口 | 鉴权或限速,别指望robots.txt |
第四步:用响应头表达,别用robots.txt堵
这是本文最想传递的一条。robots.txt管的是"能不能抓",X-Robots-Tag管的是"抓到之后能不能收"。想让第二句话被听见,就必须放行第一件事。两个一起上,等于把话说了又捂住嘴。Nginx上加这个头是一行的事:
location ~* ^/(feed|wp-json)/ {
add_header X-Robots-Tag "noindex, follow" always;
}提醒一句,Nginx的add_header是整层覆盖的——同一层级只要写了一条,上层的所有add_header都不再继承。加之前先确认这个location里原有的头还在不在。
第五步:找第二形态
这一步最容易漏,也最能救命。同一份数据往往有多个入口:接口有?rest_route=,列表有.atom,页面有参数变体和大小写变体。你封了一个,另一个还开着,而且两个的索引指令经常不一致。
找第二形态没有捷径,只能靠平台知识加上一句一句读文档。但有个偷懒的办法:把第一步列出来的端点,挨个试试它的常见别名,成本很低。
顺手把这几处也看一眼
- 404页面的体积:几万到十几万字节的404很常见,抓取预算上不划算,模板里的推荐位和侧栏是主要来源。
- feed里有没有全文:
content:encoded的数量数一下,全文feed加不加noindex的决策完全不同。 - 构建产物里的路由清单:拿它对一遍sitemap,看有没有该收录却漏了的,或者不该暴露却在里面的。
- 那个废参数测试:canonical、
og:url、JSON-LD的url,一次请求全看。
最后说一句可能有点扫兴的话。这套东西盘完,多数站不会立刻涨流量。它属于把地基上的裂缝补掉的那类工作——补之前和补之后,房子看起来一模一样。真正的收益在于,当某一天你的站因为改版、换前端、上CDN而动到这一层的时候,你手上有一份清单,知道哪些地址会跟着变,哪些指令会跟着失效。
没有这份清单的时候,这类问题的典型发现方式是:三个月后在GSC里看到一堆你没见过的URL被收录了,然后花两周去追它们是从哪儿冒出来的。
常见问题解答
接口和feed被搜索引擎收录了,到底算多严重的问题?
看内容重合度。如果接口返回的是文章全文,那它和正文页是重复内容,会分散归属信号,也会占抓取额度;如果返回的是配置或者空数组,实际损失接近于零,最多浪费几次抓取。真正严重的是第三种:地址后缀像接口、内容却是完整HTML页面,这种既重复又自称规范,纠正起来最麻烦。先按这个标准分级,再决定动不动手。
直接在robots.txt里封掉这些路径,不是最省事吗?
省事但方向反了。Google的文档写得很清楚,被robots.txt禁止抓取的地址,上面的索引规则不会被发现。所以封了之后,你既没让它不被收录,还让已有的noindex失效了。如果这个地址有外链指过来,它反而更容易变成那种"被收录但没内容"的条目。想让它不被收录,就得让爬虫进去读到noindex。
怎么知道自己的站有哪些机读端点?
三个来源。一是平台默认,按CMS和框架对着文档列;二是页面源码,前端要用的接口地址通常直接写在HTML或者JS里,搜一下.json和/api/能捞出一批;三是服务器访问日志,把非HTML的Content-Type筛出来按路径聚合,能看到实际有人在访问哪些端点,包括你自己都忘了的。第三种最准,因为它反映的是真实流量而不是理论清单。
X-Robots-Tag写noindex会不会连正文页一起误伤?
会,而且这是这套做法最常见的事故。add_header写在server层而不是特定location里,整站就都带上了noindex,页面照常显示,收录会慢慢掉光。加完必须做两件事:用请求头查看工具确认正文页没有这个头;在正式环境改之前,先在测试环境用几个典型地址跑一遍对照。另外Nginx的add_header有整层覆盖的特性,加新头的时候容易把原有的头挤掉,一并确认。
静态站生成器搭的站,是不是就没有这些问题?
问题类型换了,没有消失。静态站通常不带接口,但常见的两件事是:模板默认不输出canonical(我实测的两个技术文档站都没有),以及搜索功能需要的索引文件(那种几百KB的JSON)直接放在站点根目录下,能被抓到。前一件的影响更大,因为参数变体一来就没有任何防线。生成器的默认模板不等于SEO最佳实践,这条对所有平台都成立。
做这套检查需要什么工具吗?
一个能看响应头的工具就够了,命令行的curl或者浏览器的网络面板都行。关键不是工具,是记录方式:把地址、状态码、Content-Type、X-Robots-Tag四列记进一张表,一个站几十行,改版前后各跑一次做对比。这张表的价值在改版的时候才会显出来——它能告诉你哪些地址原来是什么样,现在变成了什么样。
权威参考资料
本文标题:《接口和feed这类地址没有head标签,它们拿什么说自己不想被收录》
本文链接:https://zhangwenbao.com/machine-readable-endpoints-x-robots-tag-noindex-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0