URL 规范化与 canonical 地址
同一篇文章可以被无数写法不同、却等价的 URL 指向:大小写不同的 host、带不带末尾斜杠、/index.html 写不写、一串 utm_* 跟踪参数、query 参数顺序颠倒。它们内容完全一样,但在缓存、统计与 SEO
眼里是不同的页面——缓存命中率被打散,搜索引擎判定为重复内容而分散权重。规范化(normalization)就是把这一大堆等价写法收敛到唯一一个规范地址(canonical URL)。
做法是一条变换流水线:小写 host、去默认端口、合并斜杠、删默认文档、统一末尾斜杠、剥跟踪参数、排序 query,逐步把输入擦干净。若擦完发现和输入不一样,就回一个 301 把客户端永久导到规范地址。
注 · 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 这种影响内容的参数必须保留。
// 规范化 = 一串「小变换」依次作用; 每个都幂等, 组合起来也幂等。
//
// 这条正则有个坑: utm_ 要当前缀匹配 (utm_source / utm_medium / …), 其余是完整 key。
// 若写成 /^(utm_|fbclid|gclid|mc_eid|ref)$/, 那个整体的 $ 会让 utm_ 也变成完整匹配 ——
// 于是它只认字面量 "utm_", 真实的 utm_source 一个都剥不掉, 而 fbclid 那几个照常工作,
// 测试里很容易看不出来。
const TRACKING = /^(utm_|fbclid$|gclid$|mc_eid$|ref$)/i;
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));
2 · 幂等是底线
规范化函数必须满足 ,即对规范地址再跑一遍结果不变。图 0-1 里「已经规范」那个预设全程跳过所有变换,就是这个性质。
否则会出大事:若 /a 规范成 /a/、而 /a/ 又规范回 /a,浏览器就在两者间无限 301。所以末尾斜杠这类策略只能二选一并坚持到底,这也是 HTTP 重定向一页「跳转链必须收敛到 200」的另一面。
上面每个小变换单独看都幂等,组合起来也幂等,这不是巧合而是设计要求:只要有一步不幂等,整条流水线就失去这个性质。
国际化路由里「裸 /about 导到 /zh/about」也是一种规范化,让每个语言版本有唯一规范地址,再用 hreflang 互相声明。规范化逐段处理 scheme、host、port、path、query 与 fragment 这六段,URL Anatomy
系列把它们拆得更细。