URL 规范化 / canonical · 让一个内容只有一个地址
同一篇文章,可以被无数写法不同、却等价的 URL 指向:大小写不同的 host、带不带末尾斜杠、/index.html 写不写、一串 utm_* 跟踪参数、query 参数顺序颠倒…… 它们内容完全一样,但在缓存、统计、SEO
眼里是不同的页面——缓存命中率被打散、搜索引擎判定重复内容而分散权重。规范化 (normalization) 就是把这一大堆等价写法收敛到唯一一个规范地址 (canonical URL)。
做法是一条变换流水线:小写 host、去默认端口、合并斜杠、删默认文档、统一末尾斜杠、剥跟踪参数、排序 query…… 逐步把输入「擦干净」。若擦完发现和输入不一样,就回一个 301 把客户端永久导到规范地址(呼应 HTTP 重定向的 301)。输入一条「脏」URL,看流水线一步步把它收敛。
301 重定向与 rel="canonical" 是两手,常一起用。 能在服务端判定的等价写法(端口、斜杠、跟踪参数),直接 301 导到规范地址最干净——用户和爬虫都被带到唯一地址,旧写法的权重也被合并过去。但有些重复无法用一条规则 301(如带必要参数的筛选页、A/B 不同入口),就在页面
<head> 里放一个 <link rel="canonical" href="…">,声明「我的规范地址是它」,让搜索引擎自己合并。前者是路由层强制,后者是给爬虫的提示。
1 · 为什么 query 顺序要排序,跟踪参数要剥掉
?a=1&b=2 和 ?b=2&a=1 语义完全相同,但字节不同——对一个把整条 URL 当 key 的缓存(CDN / 浏览器 cache)来说,它们是两条目,白白降低命中率;对统计也是两个 URL。按 key 排序把它们归一。utm_source /
fbclid / gclid 这类跟踪参数更要剥掉:它们只给分析系统看、不影响页面内容,留在规范地址里只会制造无穷多个「同一页面」的变体。注意分寸:?id=42 这种影响内容的参数必须保留,不能一并删掉。
规范化就是一串幂等的小变换依次作用;每个都幂等,组合起来也幂等,故对规范地址再跑一遍结果不变 (normalize(normalize(x)) === normalize(x))。
// 规范化 = 一串「小变换」依次作用; 每个都幂等, 组合起来也幂等。
const TRACKING = /^(utm_|fbclid|gclid|mc_eid|ref)$/;
function canonicalize(raw) {
const u = new URL(raw);
u.protocol = u.protocol.toLowerCase(); // scheme / host 大小写不敏感
u.hostname = u.hostname.toLowerCase();
if (u.port === '443' || u.port === '80') u.port = ''; // 去默认端口
u.pathname = u.pathname
.replace(/\/{2,}/g, '/') // 合并重复斜杠
.replace(/\/(index|default)\.html?$/, '/') // 删默认文档
.replace(/(.)\/$/, '$1'); // 去末尾斜杠 (根除外)
for (const k of [...u.searchParams.keys()]) // 剥跟踪参数 (不影响内容)
if (TRACKING.test(k)) u.searchParams.delete(k);
u.searchParams.sort(); // query 按 key 排序 → 归一缓存 key
u.hash = ''; // fragment 不发往服务端, 移除
return u.toString();
}
// 用法: 入站请求若 != canonical, 回 301 导过去 (注意别形成重定向环!)
if (req.url !== canonicalize(req.url)) redirect(301, canonicalize(req.url));
幂等是底线。 规范化函数必须满足 normalize(normalize(x)) === normalize(x)——对规范地址再跑一遍,结果不变(上面「已经规范」那个预设全程跳过,就是这个性质)。否则会出大事:若 /a 规范成 /a/、而 /a/ 又规范回
/a,浏览器就在两者间 301 → 301 → … 无限重定向。所以末尾斜杠等策略只能二选一并坚持到底,这也是 重定向页「跳转链必须收敛到 200」的另一面。
2 · 相关页
- 本系列 · HTTP 重定向——规范化的「导到规范地址」这一步用 301 落地;为什么必须收敛、不能形成 301 环,那页讲得最透。
- 本系列 · 国际化路由——「裸 /about → /zh/about」也是一种规范化:让每个语言版本有唯一规范地址,再用 hreflang 互相声明。
- URL Anatomy · 一条网址怎么拆开——规范化逐段处理 scheme / host / port / path / query / fragment;先在那个系列把这六段拆清楚,这里每步对应哪段就一目了然。