封面

Node.js 批量检测页面 canonical URL:从 HTML 标签到 IP 规范化

原文: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 可以声明在两个地方,一次合格的检查应该两处都看:

  1. <head> 中的 HTML 标签 <link rel="canonical" href="...">
  2. 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)

发布评论
全部评论(0)