原文:https://dev.to/simran_kaur_9eda1e242c31f/how-to-check-a-pages-canonical-url-programmatically-nodejs-1a5o(作者 @simran_kaur_9eda1e242c31f)
canonical 出的问题往往是悄无声息的那种。页面看起来一切正常,排名也维持了一段时间,然后某次更换主题或新装了个插件,rel="canonical" 就指向了错误的 URL。Google 跟着这个标签走,把权重合并到错误的版本上,流量随之下滑。没有报错,没有 404,日志里也查不到任何痕迹。
如果你在建站或维护站点,这类问题完全可以用代码提前抓出来。下面介绍如何在 Node 里读取页面的 canonical URL、判断它属于哪种情况、一次性批量检查多个页面,甚至顺带测一下 IP 规范化。
canonical URL 到底声明在哪里
大多数人只想到查 HTML。实际上 canonical 可以声明在两个地方,一次合格的检查应该两处都看:
-
<head>中的 HTML 标签<link rel="canonical" href="...">。 - HTTP
Link响应头:Link: <https://example.com/>; rel="canonical"。
响应头这种声明方式最容易被遗忘。它在 PDF 和非 HTML 资源上很常见,一些 CDN 和框架也会主动设置。一旦响应头和 HTML 的声明不一致,搜索引擎收到的就是互相矛盾的信号,所以这种情况也必须能查出来。
在 Node 中读取 canonical
Node 18+ 自带全局 fetch,做基础检查不需要引入任何依赖:
async function getCanonical(url) {
const res = await fetch(url, {
redirect: "follow",
headers: { "User-Agent": "canonical-check/1.0" },
});
let canonical = null;
let source = null;
// 1) HTTP Link 响应头
const linkHeader = res.headers.get("link");
if (linkHeader) {
const m = linkHeader.match(/<([^>]+)>\s*;\s*rel=["']?canonical["']?/i);
if (m) {
canonical = m[1];
source = "http-header";
}
}
// 2) HTML 的 <link rel="canonical"> 标签
if (!canonical) {
const html = await res.text();
const tag = html.match(/<link[^>]+rel=["']canonical["'][^>]*>/i);
if (tag) {
const href = tag[0].match(/href=["']([^"']+)["']/i);
if (href) {
canonical = href[1];
source = "html";
}
}
}
return { requested: res.url, canonical, source };
}
如果是生产环境,我会改用 cheerio 来解析 HTML 而不是用正则——正则处理 HTML 总会在边界情况上出问题。不过对一个快速审计脚本来说,上面这种写法已经够用。
判断 canonical 的含义
找到标签只完成了一半。真正有用的部分是它传递给你的信息:
function classify(requested, canonical) {
if (!canonical) return "missing";
const norm = (u) => u.replace(/\/+$/, "").toLowerCase();
return norm(canonical) === norm(requested)
? "self-referencing" // 健康的默认状态
: "points-elsewhere"; // 对重复页面是有意为之,否则就是 bug
}
- self-referencing(自引用):页面指向自身。对正常的、内容唯一的页面来说,这正是理想状态。
- points-elsewhere(指向他处):对重复页面、或需要把权重归并到第一页的分页页面来说没问题;如果是意外发生的,那就是隐患。
- missing(缺失):搜索引擎会自己挑选偏好的版本,等于把结果交给抛硬币,这种运气不值得赌。
组合起来:
const { requested, canonical, source } = await getCanonical("https://example.com/");
console.log(classify(requested, canonical), canonical, `(${source ?? "none"})`);
批量检查整个站点
只查单个页面从来不是真正的场景。网站迁移之后,你想扫的是一整份 URL 列表:
const urls = [
"https://example.com/",
"https://example.com/blog/",
"https://example.com/pricing/",
];
for (const url of urls) {
try {
const { requested, canonical } = await getCanonical(url);
console.log(`${classify(requested, canonical).padEnd(16)} ${url} -> ${canonical ?? "MISSING"}`);
} catch (e) {
console.log(`error ${url} -> ${e.message}`);
}
}
把站点地图(sitemap)里的 URL 喂给它,几秒钟就能拿到一份 canonical 审计结果。列表很长的话,记得加个小的并发限制,别把服务器打趴下。
最容易被漏掉的一项:IP 规范化
这一项比较隐蔽。如果你的服务器除了域名之外、在裸 IP 地址上也能响应,搜索引擎就可能把 http://203.0.113.10/ 和 https://example.com/ 当成两个独立站点分别收录。对内容完全相同的页面来说,这会拆散链接权重。
测试思路:把域名解析成 IP,直接请求这个 IP,然后检查它是否重定向回域名。
import dns from "node:dns/promises";
const host = "example.com";
const { address: ip } = await dns.lookup(host);
const res = await fetch(`http://${ip}/`, { redirect: "manual" });
const location = res.headers.get("location") || "";
if ([301, 302, 307, 308].includes(res.status) && location.includes(host)) {
console.log("PASS: the IP redirects to your domain");
} else if (res.status >= 200 && res.status < 300) {
console.log("FAIL: the IP serves content directly (duplicate content risk)");
} else {
console.log("Blocked or no response, which usually means no IP duplicate content");
}
注意,在共享主机上,同一个 IP 往往承载着很多站点,所以这项测试只有在独立 IP 或自有反向代理的场景下才最有意义。
当你只需要一个答案,而不是一段脚本时
对于一次性的检查,或者要交给不写 Node 的同事使用,浏览器工具会更快。我做了一个免费的工具,涵盖了上述全部功能:Pixellize Canonical URL Checker。它会同时读取 HTML 标签和 HTTP Link 响应头,标记出自我引用、指向别处、缺失三种情况,提供支持最多 20 个 URL 并可导出 CSV 的批量模式,以及 IP 规范化测试。它在浏览器中运行,不会上传任何数据。
如果想了解 canonical 背后的非代码知识,下面两篇指南讲得更深入:如何查找任意网站的规范 URL 和 如何更改规范 URL。
要点总结
- Canonical 存在于两个地方:HTML 的
<link>标签和 HTTP 的Link响应头。两处都要检查,并标记出冲突。 - 在 Node 中,
fetch加一个小型解析器就足以读取并分类 canonical。 - 每次站点迁移或更换主题之后,都要对 sitemap 做批量扫描,因为那正是 canonical 悄无声息失效的时候。
- 如果你运行自己的服务器或反向代理,不要忘记 IP 规范化。
你平时是怎么在自己的技术栈里审计 canonical 的?欢迎在评论区留言。
原文:https://dev.to/simran_kaur_9eda1e242c31f/how-to-check-a-pages-canonical-url-programmatically-nodejs-1a5o(作者 @simran_kaur_9eda1e242c31f)



