# 保哥笔记 — HTML与标记 > 本分片含 6 篇文章,按发布日期倒序。全部分片索引见 https://zhangwenbao.com/llms-full.md **站点**:https://zhangwenbao.com/ **分类**:HTML与标记 **生成**:2026-09-12 13:06:31 CST --- ## SEO爬虫报的死链和Googlebot抓的,从来不是同一批URL - URL:https://zhangwenbao.com/relative-url-resolution-crawler-googlebot-broken-link-seo.html - 分类:HTML与标记 - 发布:2026-07-30 | 更新:2026-07-30 - 摘要:实测同一条href在RFC 3986与WHATWG两套标准下解析不同:152格矩阵51格分歧,尾斜杠让68.4% 的链接改指向,附116站链接形态普查与自查命令。 - 关键词:技术SEO,内链优化,死链检测,HTML与标记 > **TLDR**:摘要:把一份写着 href="api" 的HTML原封不动挂在两个都能返回200的地址下,浏览器算出来的目标链接不是同一个——十九条链接里有十三条会变。再往下拆,同一串字符要经过四道判决才变成一个可抓取的地址:HTML解析器先剥掉里面的换行和制表符,然后URL解析器按标准还原,而现在世面上跑着两套互不兼容的标准,实测一百五十二格判决矩阵有五十一格给出不同答案。最扎眼的一条是 htt<TAB>ps://example.com/x:浏览器把它读成一个跨站的绝对地址,而任何按属性原值取链接的爬虫都会把它当成本站的一个相对路径。谷歌自己声明按RFC 3986处理地址,渲染却跑在Chrome里,而Chrome用的是那套明确说要废弃RFC的标准。 > 摘要:把一份写着 href="api" 的HTML原封不动挂在两个都能返回200的地址下,浏览器算出来的目标链接不是同一个——十九条链接里有十三条会变。再往下拆,同一串字符要经过四道判决才变成一个可抓取的地址:HTML解析器先剥掉里面的换行和制表符,然后URL解析器按标准还原,而现在世面上跑着两套互不兼容的标准,实测一百五十二格判决矩阵有五十一格给出不同答案。最扎眼的一条是 https://example.com/x:浏览器把它读成一个跨站的绝对地址,而任何按属性原值取链接的爬虫都会把它当成本站的一个相对路径。谷歌自己声明按RFC 3986处理地址,渲染却跑在Chrome里,而Chrome用的是那套明确说要废弃RFC的标准。 上一篇同一个页面11种URL写法都返回200,做SEO查重复内容时一条都不会报 (https://zhangwenbao.com/url-path-normalization-case-slash-duplicate-seo.html)数的是入口:一份内容到底有多少种地址能敲开服务器的门。这一篇数的是另一头——这些多出来的入口,会不会把页面上的链接也一起改掉。 结论提前说:会,而且比例高得不像话。 ## 同一份HTML,为什么爬虫和浏览器数出来的链接不一样? 先把问题摆正。 里的 api 不是一个地址,它是一个相对引用——一段需要配合“当前在哪儿”才能算出结果的字符串。从这段字符串到一个真正可以去抓的绝对地址,中间要过四道手: 顺序 | 裁判 | 它干什么 | 谁在用 | 一 | HTML解析器 | 把属性值从字节流里取出来,顺手剥掉某些字符 | 浏览器、渲染型爬虫 | 二 | URL解析器 | 把相对引用和当前地址合成绝对地址 | 所有人,但用的标准不同 | 三 | 基准地址 | 决定“当前在哪儿” | 取决于这个页面是用哪个地址打开的 | 四 | 服务器归一化 | 请求到达后再改一次路径 | nginx及各类web服务器 | 四道手里,只有第四道是上一篇讲过的。前三道全在客户端,而你的SEO工具和Googlebot在这三道上未必是同一套实现。 下面逐道拆,每一道都用实测说话。 ## 尾斜杠一个字符,能把整页链接指向别的地方吗? 从第三道开始讲,因为它最容易验证,后果也最大。 上一篇实测出来,现网57.1% 的站尾斜杠两种写法都返回200,而且字节完全相同。也就是说 /docs/guide 和 /docs/guide/ 很可能是同一份HTML的两个入口。 那么问题来了:这两个入口打开的同一份HTML,里面的链接指向哪儿? ## 十九条链接,十三条会变 我准备了一份包含十九种常见href写法的页面,分别用两个基准地址去解析,用的是Node 24内置的URL解析器(和浏览器同一套标准): href写法 | 基准带尾斜杠 /docs/guide/ | 基准不带 /docs/guide | api | /docs/guide/api | /docs/api | ./api | /docs/guide/api | /docs/api | ../api | /docs/api | /api | api/ | /docs/guide/api/ | /docs/api/ | ./ | /docs/guide/ | /docs/ | .. | /docs/ | / | ?x=1 | /docs/guide/?x=1 | /docs/guide?x=1 | #frag | /docs/guide/#frag | /docs/guide#frag | /api | /api | /api(不变) | //example.net/api | https://example.net/api | 同(不变) | https://example.org/api | https://example.org/api | 同(不变) | 完整矩阵十九条,其中十三条的解析结果不同,占68.4%。 规则本身很简单:尾斜杠决定了“当前目录”是哪个。有尾斜杠时,当前目录就是 /docs/guide/;没有尾斜杠时,最后一段被当成文件名,当前目录退回 /docs/。相对引用是相对目录算的,所以整体平移了一层。 ## 为什么这件事比听起来严重 把上一篇的数字接上来就明白了: - 57.1% 的站,两种尾斜杠写法都能返回200; - 这两个地址下发的是同一份HTML,字节级相同; - 可里面的相对链接,有68.4% 会解析到不同的地方。 于是同一份HTML在两个入口下,长出了两套不同的出边。爬虫从哪个入口进来,接下来走的就是完全不同的一条路——而且其中一条路上的地址多半是不存在的。 这解释了一类很常见但很难查的现象:站点地图里的页面收录得好好的,某些页面却总有一批来路不明的404,而你在站内怎么找都找不到那些链接。因为它们不在HTML里,是解析出来的。而当这些算错的地址又反过来占掉内部权重的流向时,就成了内链结构会自己烂掉:你辛苦攒来的权重正在漏进分页和重定向链里 (https://zhangwenbao.com/internal-link-decay-equity-reclaim.html)讲的那种慢性失血。 好在这个坑现网踩得不多,原因下面第六节会给数字——但踩上的那几个,是满格伤害。 ## 双斜杠入口更狠 上一篇还测出21.9% 的站接受开头双斜杠 //a/b。这个入口对相对链接的杀伤力比尾斜杠更大:十九条里有十二条解析结果和规范入口不同,而且形态很难看——https://ex.com//docs/guide/api 这种带着双斜杠的地址会一路传染下去,因为它算出来的下一跳还是带双斜杠的。 上一篇讲过服务器为什么会收这种地址,配置侧的处置也在那篇的收敛表里。 ## 兜底路由和相对链接凑一块会发生什么? 上一篇实验台上还有一条配置,当时只说了它造出无限个入口,没说这些入口拿来干嘛: location / { try_files $uri $uri/ /app.html; } 这是前后端分离和单页应用的标准写法——文件找不到就交给应用。实测结果是任意深度、任意内容的路径全部返回200,而且是同一份字节。 现在把相对链接放进这份HTML里,两件事就咬合了。 ## 每深一层,链接跟着深一层 同一份写着 href="api" 的HTML,在越来越深的路径下被打开: 页面被哪个地址打开 | href="api" 解析成 | /docs/guide/ | /docs/guide/api | /docs/guide/anything | /docs/guide/api | /docs/guide/anything/deeper/still | /docs/guide/anything/deeper/api | 最后一行是关键:这个新算出来的地址,在兜底路由下同样返回200,同样是那份HTML,于是它又能解析出下一个更深的地址。 循环就此闭合。爬虫每走一步,就从当前页面拿到一个更深的地址;每一个更深的地址又都能打开、又都含着同一条相对链接。这就是老资料里说的“无限空间”,只不过它的成因通常被含糊地写成“动态生成的URL”,而实际上它是两个再普通不过的配置撞在一起的产物。 ## 缺一不可 值得说清楚的是,这两个条件必须同时成立: - 只有兜底路由、没有相对链接:无限个地址确实存在,但没人给爬虫喂第一个,它们永远不会被发现; - 只有相对链接、没有兜底路由:算错的地址第二跳就撞404,爬虫走一步就停了,最多留下几条死链。 所以真正危险的是交集。好消息是这个交集在现网很小:兜底路由把不存在路径当200的站占3.8%,首页含纯相对链接的站占9.5%,两个条件同时踩上的概率不高。 但这也解释了为什么这类事故一旦发生就特别难查——它不是某一处配置写错了,是两处各自都合理的配置凑在了一起。单独审查任何一处都看不出问题,日志里则是一大片深度递增、从未见过的路径。服务器日志分析工具教程:读懂Googlebot抓取与预算浪费 (https://zhangwenbao.com/log-analyzer-crawl-budget-googlebot-guide.html)里讲的那种“抓取量涨了但收录没动”的形态,有一类源头就在这儿。 ## 浏览器在解析href之前先做了什么? 现在回到第一道手。很多人默认“属性值里写的是什么,链接就是什么”,这个假设在有空白字符的时候会翻车。 我在真实Chrome里跑了一组:构造 ,然后分别读两个东西——getAttribute('href')(属性原值)和 .href(浏览器解析后的绝对地址)。基准地址是 https://example.com/docs/guide/。 属性里写的 | getAttribute 拿到 | 浏览器实际去抓 | api | api | /docs/guide/api | a\npi(中间一个换行) | a\npi | /docs/guide/api | a\tpi(中间一个制表符) | a\tpi | /docs/guide/api | a\rpi(中间一个回车) | a\npi(回车被换成换行) | /docs/guide/api | /foo\n/bar | /foo\n/bar | /foo/bar | htt\tps://ex.com/x | htt\tps://ex.com/x | https://ex.com/x | a pi(中间一个空格) | a pi | /docs/guide/a%20pi | 两列一对照,问题就出来了:属性原值和浏览器真正去抓的地址,是两个不同的字符串。 ## 那一行才是真正的杀手 倒数第二行值得单拎出来。https://ex.com/x 这串东西,分成两种读法: - 浏览器:先把制表符剥掉,得到 https://ex.com/x,这是一个指向另一个站的绝对地址; - 按属性原值处理的爬虫:看到的是 htt\tps://ex.com/x,它不以 http:// 或 https:// 开头,也不以斜杠开头,于是被归类成本站的一个相对路径,拼出来是 https://example.com/docs/guide/htt\tps://ex.com/x。 同一行HTML,一个判成外链,一个判成站内死链。这已经不是“数字有偏差”了,是分类完全反了。 这个行为在WHATWG URL标准里写得明明白白: > If input contains any ASCII tab or newline, invalid-URL-unit validation error. Remove all ASCII tab or newline from input. 注意它的措辞:先报一个校验错误,然后照样把字符剥掉继续解析。所谓“校验错误”不是拒绝,只是记一笔,流程一步不停。 ## 为什么空格反而不剥 最后一行的对照很说明问题:中间的空格没被剥掉,而是编码成了 %20。标准里剥的只有制表符和换行,以及首尾的空白,中间的空格属于“非法但要保留并编码”的一类。 所以这不是“浏览器会自动清理脏数据”这么简单的规律,它剥哪些、留哪些,是一张需要照着查的表,凭直觉猜必错。这类“看着是同一件事、实际分了好几档”的坑,在URI编解码器怎么用?中文URL编码、UTM参数转码与GSC乱码网址还原 (https://zhangwenbao.com/uri-codec-percent-encoding-encode-decode-guide.html)里还有一批。 顺带提醒一句:换行和制表符跑进href里,最常见的来源不是手写,是模板引擎。为了让生成的HTML好看一点而在属性值里换行缩进,浏览器毫无察觉,而你的链接审计报告可能已经炸了。 ## 两套URL标准到底差在哪? 第二道手是重头戏。现在世面上并存着两套URL规范: - RFC 3986(也就是IETF STD 66),2005年的老规范,绝大多数服务端语言的标准库按它实现——Python的 urljoin、Java、Go、各类SEO爬虫框架; - WHATWG URL Standard,浏览器实际在用的那套,Chrome、Firefox、Safari、Node的 new URL() 都是它。 两套不是“新版旧版”的关系。WHATWG在自己的目标里写得很不客气: > Align RFC 3986 and RFC 3987 with contemporary implementations and obsolete the RFCs in the process. 直白说就是要把那两份RFC废掉。但RFC并没有被撤销,服务端生态还在按它跑。于是同一串字符,两边算出不同结果。 ## 一百五十二格判决矩阵 我拿八个基准地址乘十九种href写法,共一百五十二格,同时跑Python的 urljoin(RFC 3986)和Node的 new URL()(WHATWG),逐格比对: 范围 | 格数 | 不一致 | 比例 | 全部写法(含刻意构造的怪招) | 152 | 51 | 33.6% | 只算日常会写出来的常规写法 | 104 | 17 | 16.3% | 就算只看日常写法,也有六分之一对不上。 ## 差在哪几条 href | RFC 3986算出 | WHATWG算出 | 差别性质 | \api | /docs/guide/\api | /api | 一个当普通字符,一个当斜杠 | a\b | /docs/guide/a\b | /docs/guide/a/b | 凭空多出一层目录 | %2e%2e/api | /docs/guide/%2e%2e/api | /docs/api | 编码的点段被还原成父目录 | api␣(尾部空格) | /docs/guide/api␣ | /docs/guide/api | 一个保留一个剥掉 | 第一行的落差最大:\api 在RFC那边是当前目录下一个名字带反斜杠的文件,在WHATWG那边直接跳到网站根目录。一个在三层深处,一个在最顶层,中间隔着整个站。 反斜杠这条在标准里也是“报错但照做”: > If c is U+005C (\), invalid-reverse-solidus validation error. 报完错,接着按斜杠处理。这个行为是为了兼容那些手滑把Windows路径分隔符写进href的历史页面,属于现实倒逼标准的典型。 ## 连“指向自己”都能算出两个答案 有一组写法特别值得留意,因为它们看起来根本不该有分歧:href=""(空字符串,规范里表示“当前页面”)、href="?x=1"(只换查询串)、href="#frag"(只跳锚点)。这三种都是“还在这一页”的意思。 可只要当前页面是用一个含点段的地址打开的,比如 /docs/./guide/,两边就分家了: href | RFC 3986算出 | WHATWG算出 | "" | /docs/./guide/ | /docs/guide/ | ?x=1 | /docs/./guide/?x=1 | /docs/guide/?x=1 | #frag | /docs/./guide/#frag | /docs/guide/#frag | 差别在于WHATWG在解析阶段就把点段消掉了,而RFC 3986的合成算法在这几种“不改路径”的分支上直接沿用基准地址的原样路径,点段留在里面。 后果是:一个页面上指向自己的链接,在两套标准下指向两个不同的地址——其中一个还带着点段。而按上一篇的实测,带点段的地址在16.2% 的站上照样返回200。分页导航、语言切换、排序按钮这类控件大量使用 ?x=1 这种写法,一旦页面是从含点段的地址进来的,整组控件的目标地址就集体偏移。 这也是为什么把地址收敛干净是上游动作:基准地址一旦是规范形态,下游这些分歧全部消失。 ## 基准地址越畸形,两边差得越远 按基准地址拆开看,不一致率不是均匀分布的: 基准地址 | 十九格中不一致 | /docs/guide/(规范形态) | 4 | /docs/guide(无尾斜杠) | 4 | /DOCS/GUIDE/(大写) | 4 | /docs/./guide/(含点段) | 7 | //docs/guide/(开头双斜杠) | 12 | /docs//guide/(路径内双斜杠) | 12 | 规范形态下只有4格分歧,而双斜杠入口下飙到12格。原因是RFC那边会把多余斜杠归一化掉,WHATWG原样保留——而上一篇实测,21.9% 的站恰好接受这种双斜杠入口。 两篇的数据在这里合上了:服务器多开的那扇门,正好是两套标准分歧最大的那个位置。 ## Googlebot用的是哪一套? 这个问题的答案有点分裂,而分裂本身就是答案。 谷歌在URL结构文档里明确表过态: > Like any other HTTP client following IETF STD 66, Google Search's URL handling is case sensitive. IETF STD 66就是RFC 3986。谷歌自称按RFC 3986处理地址。 但另一边,谷歌的渲染是跑在Chromium里的——网页被真实加载、执行JavaScript、构建DOM,然后从DOM里取链接。而Chromium的URL解析用的是WHATWG那套。 所以合理的推断是:抓取调度那一层按RFC 3986的语义处理地址(比如大小写敏感),而渲染阶段从页面里提取出来的链接,是Chromium按WHATWG算出来的。这个推断我没法直接验证——谷歌没有公开渲染管线取链接的实现细节,这里必须说清楚是推断而不是实测。 但对干活的人来说,可操作的结论是确定的: > 凡是你用服务端爬虫(Screaming Frog之外的自研脚本、Python写的批量检查、CI里的链接校验)算出来的链接集合,都要假设它和渲染型爬虫算出来的不完全一样。 这不是说工具不能用,是说报告里那些“莫名其妙的404”和“找不到来源的链接”,第一嫌疑人是解析差异,不是站点真的坏了。关于死链报告该怎么分诊、哪些必须修哪些可以放,修复死链对SEO到底有没有用?哪些必须修、哪些纯属白费功夫 (https://zhangwenbao.com/broken-links-seo-worth-fixing-triage-guide.html)里有一套优先级;本文补的是它上游的那一步——先确认这条死链是不是工具自己算出来的。 ## 现网到底有多少页面会踩这个? 讲了半天机制,得给个现实的量。我抓了181个站的首页,其中116个返回200且含链接,一共17988条 ,按形态分类: 形态 | 条数 | 占比 | 受基准地址影响吗 | 根相对 /path | 8965 | 49.8% | 否 | 绝对 https://… | 8218 | 45.7% | 否 | 片段 #x | 303 | 1.7% | 否 | 纯相对 path | 280 | 1.6% | 是 | 协议相对 //host/path | 176 | 1.0% | 否(但会跨域) | 真正受基准地址影响的纯相对链接,只占1.6%。按站看,116个站里只有11个(9.5%)首页含至少一条纯相对链接。 这个数字得诚实地讲:行业整体已经用根相对和绝对地址把这个坑绕过去了。写这篇之前我预期会看到一个更吓人的比例,实测没有。 ## 但那9.5% 是满格伤害 相对链接的分布极度不均。中位数是0.0%,也就是一半以上的站一条都没有;可一旦有,往往是整站风格: 站点 | 纯相对链接占比 | 数量 | mixpanel.com | 72.4% | 223 / 308 | nginx.org | 66.0% | 35 / 53 | 这两个站首页上七成左右的链接是纯相对的。对它们来说,前面讲的所有解析差异全部生效,而且一次影响几百条链接。 结论因此是二元的,不是渐变的:绝大多数站完全不用管这件事,少数站需要当成头等大事。判断成本极低——打开首页源码,搜一下 href=" 后面第一个字符不是 / 也不是 h 的有多少条,一分钟出结果。 ## 顺手把自家站也测了 写到这儿总得拿自己的站过一遍秤,不然只是在说别人。保哥这个站首页459条链接,形态是这样的: 页面 | 链接总数 | 绝对形态 | 纯相对 | | href含换行或制表符 | 首页 | 459 | 458 | 0 | 无 | 0 | 术语表页 | 328 | 297 | 0 | 无 | 0 | 剩下的几十条是页内锚点。也就是说本站在这篇讲的每一个坑上都是零风险——不是因为特意治理过,而是因为模板一开始就是拿绝对地址拼的。这类问题的分布就是这样:要么一条都没有,要么满屏都是,中间地带很窄。 ## 资源引用比链接更干净 顺手统计了 img、script、link 这三类标签的11542条引用:绝对地址67.0%、根相对26.2%、协议相对3.0%,纯相对只有0.5%。 比 还干净三倍,不过协议相对的比例反过来更高(3.0% 对1.0%),那是全站HTTPS之前留下的历史习惯。想把自家站的链接结构整体扒一遍,内链外链分析器使用教程:一次扒清链接结构与SEO扣分项 (https://zhangwenbao.com/link-analyzer-internal-external-audit-guide.html)里那套统计口径可以直接照搬。合理——资源加载一旦出错是肉眼可见的白屏或者裂图,早就被修掉了;而链接指错只是多一个404,页面看起来一切正常。会立刻挨骂的问题总是修得更彻底,这条规律在哪儿都成立。 ## 那base标签为什么几乎没人用了? 是HTML里专门用来改基准地址的标签。理论上它能一劳永逸解决尾斜杠问题:不管页面被哪个地址打开,相对链接一律按 里写的算。 实测116个站里用了它的有两个,占1.7%。而这两个的用法都挺有意思: 站点 | 的值 | 问题 | php.net | https://www.php.net/index.php | 指向一个具体文件,当前目录退回站根 | angular.dev | / | 值本身是相对的,还得先解析一次 | 第二个尤其绕: 的作用是给别人当基准,可它自己的值又是个需要基准才能解析的相对地址。规范对这种情况有明确处理(拿文档地址来解析它),但读代码的人很容易看岔。 用得少不是因为它没用,而是因为它是个全局开关:一旦写上,页面里所有相对引用——链接、图片、脚本、表单提交地址——全部改基准。改对一个的同时可能改坏五个。相比之下,把链接写成根相对地址是局部的、可控的,代价也更低。 这也是为什么现网49.8% 的链接是根相对形态:它是唯一一种既省字数又完全不受基准地址影响的写法。 ## 怎么让四个裁判给出同一个答案? 治理动作按投入产出排,从便宜到贵: 动作 | 成本 | 解决什么 | 适用 | 链接一律写根相对或绝对 | 低 | 基准地址的全部影响 | 所有站,首选 | 尾斜杠301收敛到一种 | 低 | 入口分裂的根源 | 57.1% 的站需要 | 模板检查href里的换行缩进 | 低 | 解析器分歧 | 模板生成的页面 | canonical写成绝对地址 | 低 | 兜底,现网100% 已这么做 | 所有站 | 审计工具换成渲染型 | 中 | 两套标准的差异 | 相对链接占比高的站 | 用 | 高 | 基准地址 | 基本不推荐 | ## 第一条几乎解决全部问题 值得强调:把 href="api" 改成 href="/docs/guide/api",这一个动作就让前三道裁判全部失效。根相对地址不依赖当前目录,尾斜杠怎么变都不影响它;也不存在点段要不要消除的分歧。 现网49.8% 的链接已经是这么写的,这不是什么前卫做法,是主流。顺带一提,同一份普查里116个站有98个写了canonical,形态百分之百是绝对地址,一个用相对写法的都没有——大家在这件事上早就达成共识了,只是没把同样的共识推到 上。 ## 三条自查 # 1. 首页有多少条纯相对链接(不以 / 和 h 开头) curl -s https://你的域名/ | grep -o 'href="[^"/h][^"]*"' | wc -l # 2. href 属性值里有没有换行或制表符 curl -s https://你的域名/ | grep -Pc 'href="[^"]*[\t\n][^"]*"' # 3. 尾斜杠两个写法是不是都 200(是就要收敛) curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://你的域名/某页面 curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://你的域名/某页面/ 想省事也可以直接用现成的工具对照一遍,渲染对比器怎么用?揪出爬虫和用户看到的页面不一样 (https://zhangwenbao.com/render-compare-bot-user-cloaking-detection-guide.html)讲的就是把两侧结果摆在一起看。 第一条如果结果是0,恭喜,这篇文章讲的坑你一个都踩不到,可以直接跳到最后。第三条两行如果状态码和字节数都一样,那就回上一篇看收敛配置。 顺带一提,相对路径算错这件事不只发生在HTML里。llms.md校验器亮绿灯时,5条链接可能全是死的 (https://zhangwenbao.com/llmstxt-validator-structure-check-deadlink-relative-path-guide.html)那篇碰到的是同一个毛病换了个文件格式:校验器只检查结构合不合法,不检查相对地址解析出来还在不在。 ## 保哥的判断 这个话题很容易写成“你的工具全是错的、赶紧换渲染型爬虫”,但实测数据不支持这个结论。91% 的站首页一条纯相对链接都没有,对它们来说四个裁判永远一致,换工具纯属浪费预算。 真正值得做的是那个一分钟的自查:先确认自己在9.5% 里还是在90.5% 里,再决定要不要往下投入。技术SEO里最贵的浪费,从来不是没做优化,而是把资源投在了一个自己压根不存在的问题上。顺便说,那两个纯相对链接占比七成的站,一个是做用户行为分析的,一个是写出本文实验台那个服务器的——可见这事和团队水平无关,只和历史包袱有关。 ## 常见问题解答 ## 我的死链检测工具报了一批404,站内却找不到这些链接,是怎么回事? 先查两件事。一是这些地址是不是比正常路径少一层或者多一层目录,如果是,多半是尾斜杠导致的基准地址错位——同一个页面有带斜杠和不带斜杠两个入口,工具从其中一个进来解析相对链接,算出来的地址整体平移了一层。二是原地址里有没有反斜杠或者奇怪的空白,那属于两套URL标准的分歧。两种情况下站点本身都没坏,坏的是链接是怎么算出来的。 ## 纯相对链接和根相对链接到底差在哪? href="api" 是纯相对,结果取决于当前页面的地址,尾斜杠变一下结果就变。href="/docs/api" 是根相对,只依赖域名,当前页面在哪儿都不影响。现网49.8% 的链接用根相对,纯相对只有1.6%。如果你只想记一条规则,就记这个:href的第一个字符是斜杠,你就安全了。 ## href里的换行和制表符真的会出问题吗?浏览器不是会自动处理吗? 浏览器确实会剥掉它们,WHATWG标准明确要求“Remove all ASCII tab or newline from input”。问题出在按属性原值取链接的工具上——它拿到的是没剥过的字符串,于是算出一个完全不同的地址。最极端的例子是制表符落在协议名里,浏览器读成跨站绝对地址,工具读成本站相对路径,分类直接反了。中间的空格则两边都不剥,会编码成 %20。 ## 该不该给站点加base标签来统一基准地址? 一般不建议。它是全局开关,会同时影响页面里所有相对引用,包括图片、脚本和表单提交地址,改对链接的同时容易改坏别的。现网116个站里只有2个在用。把链接改写成根相对地址是更便宜也更可控的做法,效果完全一样。 ## Googlebot到底按哪套标准解析链接? 谷歌文档里明确说自己的URL处理遵循IETF STD 66,也就是RFC 3986,这解释了它为什么大小写敏感。但渲染阶段跑在Chromium里,而Chromium用的是WHATWG那套。合理推断是两层各用各的,不过谷歌没公开渲染管线取链接的实现细节,这一条是推断不是实测。干活时按最保守的假设来:不要指望自研脚本算出的链接集合和渲染型爬虫完全一致。 ## 两套URL标准并存,以后会统一吗? 短期看不会。WHATWG在自己的目标里写着要让那两份RFC作废,但RFC 3986并没有被正式撤销,服务端语言的标准库还在按它实现,而且改动它会破坏大量既有系统。更现实的做法是承认分歧存在,在会出问题的位置(相对链接、反斜杠、编码的点段)主动避开,而不是等标准统一。 ## 协议相对链接(以 // 开头)现在还能用吗? 能用,但没必要。它在全站HTTPS之前有意义——让资源跟随当前页面的协议。现在所有站都上了HTTPS,直接写 https:// 更清楚。它本身不受基准地址影响,所以不属于本文讲的这类问题,但它有另一个坑:// 开头的地址会跳到另一个主机,一旦域名写错就是跨域,而这个错误在代码审查里看着特别像一个普通的路径。 ## 权威参考资料 ## 工具说canonical在head,浏览器说在body,做SEO的该信哪一边 - URL:https://zhangwenbao.com/head-boundary-canonical-parsed-into-body-seo.html - 分类:HTML与标记 - 发布:2026-05-19 | 更新:2026-07-31 - 摘要:54组单变量样本喂给四个HTML解析器,落点一致只有20组、分歧34组。Python自带那个在17组越界样本上全部漏报,而某大站的规范网址落在第47万字节。 - 关键词:canonical,技术SEO,HTML与标记 > **TLDR**:摘要:浏览器不是读到</head>才关掉head的,而是读到第一个不该出现在那里的东西就当场关掉。保哥拿54组只改一个变量的样本,喂给四个HTML解析器,canonical落点一致的只有20组,分歧34组;同一段谷歌代码管理器的安装片段,关掉脚本的解析器把canonical判进body、开着脚本的判在head,两边都合规。121个站扫下来,真正把标签写越界的极少,但工具查不出来这件事一直在。 > 摘要:浏览器不是读到才关掉head的,而是读到第一个不该出现在那里的东西就当场关掉。保哥拿54组只改一个变量的样本,喂给四个HTML解析器,canonical落点一致的只有20组,分歧34组;同一段谷歌代码管理器的安装片段,关掉脚本的解析器把canonical判进body、开着脚本的判在head,两边都合规。121个站扫下来,真正把标签写越界的极少,但工具查不出来这件事一直在。 ## 一个写在head里的canonical,凭什么会跑到body里去? 先说清楚这篇要处理的是什么问题。它不是把canonical手写在body里 (https://zhangwenbao.com/canonical-tag-common-mistakes-hint-not-directive.html)那种低级错误,也不是模板变量没渲染出来。它是这么一件事:你打开源代码,一眼看过去canonical明明白白排在 上面第三行;可浏览器解析完,它在body里。 这不是浏览器有毛病。HTML标准里head的结束条件从来就不只有 这一个。解析器一边读一边判,读到某个它认为不属于head的东西,就地把head合上、把body打开,然后接着往下走。你写的那个闭合标签,很可能在它眼里早就是多余的了。 这件事之所以值得单独写一篇,是因为它的失效方式特别安静。谷歌官方在讲规范网址那份文档里写得很明确,rel=canonical必须放在head区域里;放在别处,它不认。而你手上大部分的检查工具,用的解析器跟浏览器不是同一个,它们会告诉你一切正常。 ## 三方证词都是真的,只是问的不是同一个问题 保哥第一次撞上这类问题时,最费解的就是这一点:抓取工具说canonical在head,浏览器开发者工具说在body,源代码看上去又在head。三边都没撒谎。工具报的是它自己那个解析器建出来的树,浏览器报的是渲染引擎建出来的树,而源代码只是一串字节——把字节变成树的那一步,才是分歧发生的地方。 ## HTML标准到底怎么规定head的结束? 去翻标准的解析章节,会看到一个叫in head的插入模式。解析器进到head之后就处在这个模式里,它只接受一份很短的白名单: 类别 | 元素 | 说明 | 元数据 | base、link、meta、title | 日常写SEO标签用的就是这几个 | 脚本与样式 | script、style、template、noscript | 内容按原始文本处理,不当标记解析 | 历史遗留 | basefont、bgsound、noframes | 老元素,今天基本不会写 | 其他 | 注释、空白字符 | 不影响判定 | 名单之外的任何起始标签,或者任何非空白的字符,都会触发同一个动作:解析器把head弹出栈,切到after head模式,然后重新处理刚才那个东西。重新处理的结果通常是打开body。也就是说,head的边界是被内容推出来的,不是被闭合标签划出来的。 ## 这份白名单里有两个容易看走眼的成员 一个是 noscript。它在白名单里,但它内部允许放什么,取决于解析器有没有开启脚本这个标志——这条待会儿会变成本篇最反直觉的一段。另一个是 template,它2014年才进标准,很多老解析器压根不认,认不认的差别会直接体现在canonical的落点上。 ## 54组样本喂给四个解析器,分歧出在哪几组? 光读标准没用,得量。保哥搭了一套单变量样本:同一份骨架,head里固定放charset和title,然后插入一段东西,再跟上canonical、description、hreflang、robots四个标签,最后写 。每组样本之间只有插入的那一段不同,其余一个字节都不差。 一共54组,分成四类:标准白名单里的元素、明确越界的元素、现网真实事故形态、畸形写法与边界。四个解析器分别是: - html5lib:Python生态里按标准实现的那个,被当成参照物用 - lxml:底层是libxml2,速度快,大量爬虫和SEO脚本用它 - html.parser:Python自带,也是BeautifulSoup不指定解析器时的默认选择 - Chrome 150:走两条路,一条是DOMParser,一条是真实文档导航 结果先给总数:四个解析器对canonical落点意见一致的只有20组,分歧34组,分歧率63%。 ## 分歧集中在哪几类 样本 | Chrome | html5lib | lxml | html.parser | head里插
| body | body | body | head | head里插 | body | body | body | head | head里插 | body | body | head | head | head里插
| body | body | head | head | head里插