全部笔记

Canonical URL 是部署契约,不只是 SEO 标签

统一域名、路径和内容身份,避免预览环境与生产环境互相污染。

同一篇文章可以通过主域名、www、Workers 子域名和预览地址打开。如果页面把当前请求域名直接写进 canonical,这些入口就会各自宣称自己是主版本。站点能访问,不代表内容身份一致。

canonical 表达搜索索引的首选地址,但它不是访问控制,也不是强制跳转。本文假设公开站点使用一个稳定主域名,其他入口可以访问或重定向;不讨论多租户站点为不同客户动态分配域名的情况。

从配置生成主地址

---
const productionSite = new URL('https://example.dev');
const canonical = new URL(productionSite.href);
canonical.pathname = Astro.url.pathname;
canonical.search = '';
canonical.hash = '';
---
<link rel="canonical" href={canonical.href} />

示例保留路径,去掉查询与片段,适用于参数不改变正文的文章页面。如果参数代表独立、可索引的内容,这个策略就不成立。尤其不要对所有分页都指向第一页:第二页的文章集合不同,应该根据真实索引策略处理。

克隆可信基准后给 pathname 赋值,避免把路径重新解释为相对 URL。new URL('//other.example/path', productionSite) 会换到另一来源,而 pathname 赋值仍保留配置的协议与主机。双前导斜杠是否是合法站内路径仍由路由政策决定;这个示例只保证生成 canonical 不会因此换源,不能替代真实路由与部署前缀校验。

在 Astro 中可以通过 site 配置提供生产站点基准。布局组件、RSS 和 sitemap 应复用它,而不是分别写三处字符串。否则某次域名迁移会只改标题布局,RSS 仍然指向旧站。

生产站点基准缺失时,一些实现会退回当前请求地址或开发域名,构建仍然成功。对 canonical 来说,这种兜底隐藏了部署错误。生产构建应该要求合法的绝对 HTTPS 基准,并拒绝本地地址和未经批准的预览域名;开发环境可以使用不同规则,但需要显式环境标记。配置失败比发布一站错误身份更容易回滚。

允许域名列表不应该由任意请求动态扩充。对于确实支持多个租户的产品,先从可信租户记录取得公开域名,验证其绑定状态,再生成地址。对于单一个人博客,固定站点配置通常足够,无需为了假想扩展建立复杂推导。部署工具返回的临时地址可以用于验收,不应自动替换内容的永久地址。

同时检查协议与端口。开发服务器的端口残留到生产绝对链接中,会让内容可访问却分享失败;代理错误识别协议可能生成 HTTP 地址,造成多一次跳转。规范化时不要修改路径中的业务字符,域名身份与路径身份分别校验。一个函数包办所有字符串修复,容易在解决一类问题时制造另一类冲突。

将站点基准写入非敏感构建摘要,记录环境、版本和公开条目数量,出现错误后能确认部署究竟使用了什么配置。不要打印全部环境变量,域名不是秘密并不意味着附近的令牌也可以一起输出。配置透明应针对必要字段,而不是无差别调试转储。

路径规范化也必须统一

选用尾斜杠就让站内链接、canonical 和重定向都遵循该规则。是否保留大小写不是纯审美问题:服务器可能把两个路径看作不同资源。最省事的方案通常是内容 slug 一开始就小写,并让部署层对历史变体做明确迁移。

主域名与 www 同时绑定不等于两者都应作为 canonical。可以选根域名为主,www 使用永久重定向。测试重定向时要保留路径和查询参数,避免所有深层链接变成首页;也要检查 HTTPS 到 HTTPS 的跳转,防止中间经过不安全协议。

一篇文章的 #transaction-boundary 指向正文中的一个位置,通常不需要成为另一份 canonical。但删除 canonical 的片段不代表分享功能也该删除片段:读者复制章节链接,是希望别人直接到达这一节。验收分别检查页面身份和定位行为,确认主地址不带片段,同时公开文章中对应 ID 仍存在。两项输出可以不同,因为它们服务的任务不同。

若前端应用把片段解释为另一份内容,例如旧式 #/article/42 路由,就不能把这套普通文章策略直接套上去。服务器收到的请求不含 URL 片段,单纯根据请求路径生成身份会把多个前端页面都当成根页。应先选择可以被独立请求的内容地址,或核对当前应用的渲染与索引方案;不要只修一个标签,留下无法独立访问的正文入口。

测试时准备参数不改变正文、参数改变正文和未知参数三种输入,确认策略不会误合并。尤其检查框架更新后 Astro.url.pathname、尾斜杠和部署基路径是否仍与预期一致。子路径部署时,简单使用根域名可能丢失站点前缀;多租户部署则需要可信租户配置,不能把任意 Host 头当成受信任站点基准。

多语言页面不要全部指向英文正文

中文与英文文章虽然讨论同一个主题,仍然是不同语言内容。通常应让每个语言版本保留自己的 canonical,再用正确的语言替代关系连接它们。把中文版 canonical 指向英文版可能传递“中文只是重复副本”的错误信号,也使分享和索引身份不一致。语言替代标签需要互相对应,不能只由英文页指向中文页。

根路径如果负责浏览器语言协商,应明确它是入口还是独立内容页。入口重定向到稳定语言路径后,canonical 应在最终内容页生成,不把每种浏览器偏好都形成独立身份。搜索爬虫未必携带与真实用户一样的语言头,所以所有语言页面还应通过明确链接可达。浏览器语言策略不能替代站点导航与语言映射。

并非每篇文章都有全部翻译。如果只有简体中文版本,就不要生成不存在的英文替代地址;英文索引页可以链接中文文章,但需要明确语言。翻译落后于原文时,应记录更新时间和内容状态,而不是仅因 slug 相同就宣称完全等价。内容管理系统的语言关联应来自稳定标识,不从标题相似性猜测。

预览环境需要两道保护

第一道是预览页面不产生错误的生产内容身份,第二道是阻止不该索引的预览内容被发现。可以为预览设置 X-Robots-Tag: noindex,但若内容本身保密,应使用访问控制,不能依赖爬虫自觉遵守。

对草稿预览尤其如此:把页面标记 noindex 仍然允许任何拿到链接的人访问。私人草稿应从公开构建中排除,授权预览要验证身份。canonical 指向正式页面也无法隐藏正在预览的文字。

构建时需要明确环境变量来源。不要根据“有没有某个 CI 变量”猜测生产环境,应使用固定部署上下文并在日志中输出非敏感的站点基准地址。预览与生产分别运行构建,能防止一次复用错误产物把预览设置带进线上。

域名切换需要一张身份映射表

假设技术博客从旧域名迁移到新域名,内容路径不变。迁移不应该仅修改布局中的一行字符串。应先列出 HTML canonical、Open Graph 地址、结构化数据、RSS 链接、站点地图、图片绝对地址和分享按钮来源,再明确每项数据使用哪个站点基准。一个来源配置影响多个出口时,统一配置比搜索替换可靠,因为有些地址由第三方插件生成。

迁移表同时记录旧地址与新地址。如果部分文章调整了路径,不能仅把旧域名替换成新域名,需要逐项映射。对于未迁移内容,返回缺失响应比指向不相关首页更诚实。迁移前保存旧路径清单,迁移后检查每个旧地址可以到达对应正文。抽测首页不能覆盖深层路径、分页和语言版本,尤其是边缘规则可能只正确匹配最外层路径。

旧域名重定向与新域名 canonical 是互补关系。前者把访问引导到新位置,后者为仍然可访问的变体提供首选身份。不要因为新站标签正确就停止维护旧域名;也不要因为旧站重定向正确就让新站标签继续指向旧站。域名续费和证书状态因此属于迁移维护,而不是纯粹的搜索优化。

分享按钮若读取浏览器当前地址,来自预览域名或追踪参数的访问者会把这些变体继续传播。文章分享可直接使用已验证的主地址,而复制当前页面位置的功能则可以保留片段,让读者定位到某节。两个操作表达不同意图,不应由同一个未经说明的函数决定。

邮件、社交卡片与站内搜索结果也应使用标准文章地址。测试可以从公开记录生成期望值,再比较各出口,而不是为每个出口维护一份重复配置。需要避免测试与生产使用完全相同的错误生成函数,否则二者一起错仍然通过。准备一个固定域名与路径的独立样本,能验证拼接、编码和前缀处理,而非仅验证两个函数返回一样的字符串。

用一个具体分页案例决定参数身份

考虑 /writing/?page=2&tag=rust&utm_source=mail。追踪参数不改变内容,页码与标签却改变文章集合。先为每类页面写清楚资源定义:文章页的资源是一篇正文,标签页的资源是一个主题集合,分页页的资源是集合中的某段。只有定义完成,才能判断两个 URL 是否应当共享 canonical。把资源定义倒过来服从“URL 越少越好”,往往会合并并不等价的内容。

如果站点不希望筛选组合参与索引,可以明确制定索引规则,仍允许用户分享与返回筛选状态。若决定某个组合值得索引,就给它稳定的地址、可靠标题与内容入口,并让其 canonical 指向自身的规范形式。排序只改变同一集合的呈现,是否归到默认排序也需要判断页面可见内容是否足够一致,而不是仅凭参数名称推断。

为这些决策生成一张页面策略表:路径类别、正文身份、保留参数、规范形式与索引意图。然后使用样本测试该表。page=1 是否与没有页码等价,非法页码返回错误还是重定向,重复参数如何处理,都应由请求处理与身份生成共同遵循。响应已经是缺失页时,不继续沿用正常文章的 canonical,否则无效输入可能宣称拥有有效内容身份。

同一个测试矩阵还需要验证站内链接。页面声明 /writing/?tag=rust&page=2 为主版本,分页导航却不断生成另一个参数顺序或域名,会持续制造变体流量。顺序差异未必使搜索系统无法理解,但统一生成降低诊断成本。不要对未知参数静默执行业务解释,规范化函数只能合并已经证明等价的输入。

域名迁移要检查缓存中的旧身份

站点配置已改为新域名,源站新 HTML 也正确,边缘缓存却仍然提供带旧 canonical 的页面。此时仓库检查通过,但实际信号仍然分裂。迁移流程应把 HTML 缓存、RSS、站点地图和社交卡片缓存列入检查范围。各平台的清理能力不同,应记录已经执行的操作与尚不能控制的客户端缓存,而不是把一次部署完成等同于所有副本更新。

对于可以清理的缓存,先确定清理范围是否覆盖语言、路径与参数变体,再在真实入口请求代表性页面。响应中非敏感的部署版本信息可以帮助识别旧副本,但不能单靠缓存状态头推断正文是哪个版本。直接解析 canonical、Open Graph 地址与对应文本,才是在验收身份。多个入口经过不同规则时,分别请求根域、www 与旧域的深层路径。

社交平台已抓取的预览卡片可能有独立缓存。工程上可以提供新的正确元数据并使用平台支持的刷新机制,但不能保证所有历史分享立即改变。类似地,阅读器保留旧域名链接不表示 RSS 生成器仍然错误。报告应区分服务器当前输出、已知缓存副本与第三方更新状态,避免因为不可控旧副本反复迁移文章标识。

如果必须回滚域名配置,也要把重定向方向一起评估。旧域已经永久指向新域,新域又回指旧域,可能形成环;即使没有环,浏览器缓存的永久跳转也会影响回滚效果。提前保留一份方向图与回滚方案,比事故时交换两个字符串可靠。域名切换涉及公开身份,不适合像临时开关一样频繁翻转。

代理头与请求域名不能成为未经验证的输入

某些运行时布局通过 Host 或转发头生成绝对 URL,开发时很方便,但这些字段的可信度取决于代理链。源站直接暴露时,攻击者可能自行设置转发头;多级代理又可能追加不同格式的值。单域站点使用固定公开基准可以避开这类推导。若确实需要动态域名,可信代理范围与域名批准记录是前提,而不是可选优化。

可以为运行时生成器测试正常主域、www、预览域、陌生域和包含非法端口的输入,断言只使用批准配置。不要把陌生 Host 直接回显到 canonical、密码重置邮件或公开分享链接。canonical 错误本身主要是身份问题,但同一套未经验证的绝对地址函数若被复用到账户流程,影响范围就会扩大。因此工具函数应明确用途,避免“所有地方都能生成 URL”的模糊接口。

生成绝对链接时,以可信基准和明确路径调用 URL API,并检查路径输入是否可能被解释为绝对地址或协议相对地址。new URL 能正确解析,不代表其结果一定保持在期望域名。内容系统受限 slug 通常可以提供这个保证;若路径来自自由文本或第三方导入,结果域名仍需要验证。解析成功是语法性质,同源是业务与安全性质。

给验收报告留下足够的反证

一份报告可以记录请求入口、第一跳状态、最终地址、canonical 数量和内容标识。只保存最终地址会看不到中间 HTTP 降级或多余跳转;只保存 HTML 也看不到边缘重定向。对于预览,单独记录响应头中的索引意图与访问控制是否生效,不能因为 canonical 指向生产就将预览认定为私有。

失败样本应保留可重现请求,但去除会话与私人查询值。它们让后续变更可以回归验证。例如发现 www 的深层路径被送回首页,修复后应长期保留该请求。测试用一个固定且已知的期望正文标识,避免最终地址正确却落在错误文章。身份契约最终服务于读者到达正确内容,而不是凑齐几个 SEO 标签。

订阅条目的身份不要随域名一起重建

域名迁移时,RSS 的文章链接需要指向新地址,但订阅条目的持久标识是另一项决策。假设阅读器过去以旧条目标识去重,生成器这次把所有标识改成新域名,阅读器可能把旧文章当成新条目。不同阅读器行为不能统一保证,因此先检查当前格式与既有标识合同,不从 canonical 的新值机械推导所有订阅身份。

验收保留迁移前的一份合成订阅样本,比较迁移后标题、正文链接、发布时间和条目标识各自的预期变化。已发布文章只换访问位置,不应为了迁移而伪造新的发布时间。需要重新发布的实质修订则单独决定通知策略;域名身份一致与订阅去重稳定不是同一个断言。

验证内容身份的完整链路

列出一篇文章在所有可访问入口的 URL。请求每个入口,记录状态码、最终地址、canonical、Open Graph URL 和 RSS 条目。它们应指向同一个主版本,除非多语言或分页契约明确允许不同身份。

迁移域名时继续保留旧域名重定向,更新 sitemap 并检查图片、结构化数据中的绝对地址。不要声称 canonical 能立刻合并全部搜索信号,搜索引擎仍会结合重定向、内容相似性和链接判断。工程目标是提供一致信号,不是替搜索引擎做结果保证。

模板只有一个 canonical 标签,不代表实际页面只有一个。插件、布局嵌套或旧组件可能再次插入标签,导致同页出现互相冲突的值。构建后解析代表性 HTML,断言只有一个绝对 HTTPS 地址,并验证目标能返回预期正文。不要只用字符串搜索第一个标签,第二个冲突标签同样重要。

至少选择首页、一篇文章、分页、语言页与缺失页做检查。缺失页不应宣称首页是它的正文身份,预览页面也不应因为复用生产布局而失去 noindex。对生产和预览分别保存结果,能发现同一个构建产物被错误复用。响应头与 HTML 的索引策略要一致,避免页面说允许索引而部署层统一禁止。

上线后可以观察索引工具中的实际首选地址,但不要将短期未更新视为代码必然错误,也不要将暂时收录视为全部配置正确。工程验收关注可验证的信号一致性;搜索系统的最终选择还依赖其抓取与判断过程。把可控部分完整做好,再依据真实反馈修正,而不是承诺某个标签会立即改变排名。

参考:Google canonical 指引、Astro 配置参考。