DEV Community

Cover image for How to Check a Page's Canonical URL Programmatically (Node.js)
Simran Kaur
Simran Kaur

Posted on

How to Check a Page's Canonical URL Programmatically (Node.js)

Canonical bugs are the quiet kind. The page looks fine, it ranks for a while, and then a theme change or a new plugin points rel="canonical" at the wrong URL. Google follows the tag, consolidates the wrong version, and your traffic slides. No error, no 404, nothing in the logs.

If you build or maintain sites, you can catch this in code. Here is how to read a page's canonical URL in Node, classify what it means, check many pages at once, and even test IP canonicalization.

Where canonical URLs actually live

Most people check the HTML. There are really two places a canonical can be declared, and a good check reads both:

  1. The HTML <link rel="canonical" href="..."> in the <head>.
  2. The HTTP Link response header: Link: <https://example.com/>; rel="canonical".

That header version is easy to forget. It is common on PDFs and non-HTML resources, and some CDNs and frameworks set it. If the header and the HTML disagree, search engines can get conflicting signals, so you want to catch that too.

Reading the canonical in Node

Node 18+ ships a global fetch, so no dependencies are needed for a basic check:

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 header
  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 };
}
Enter fullscreen mode Exit fullscreen mode

For production I would reach for cheerio to parse the HTML instead of a regex, since regex on HTML breaks on edge cases. But for a quick audit script, the pattern above is enough.

Classifying what the canonical means

Finding the tag is half the job. The useful part is what it tells you:

function classify(requested, canonical) {
  if (!canonical) return "missing";
  const norm = (u) => u.replace(/\/+$/, "").toLowerCase();
  return norm(canonical) === norm(requested)
    ? "self-referencing" // the healthy default
    : "points-elsewhere"; // intentional for duplicates, a bug otherwise
}
Enter fullscreen mode Exit fullscreen mode
  • self-referencing: the page points to itself. This is what you want on a normal, unique page.
  • points-elsewhere: fine for a duplicate or a paginated page consolidating to page one, a problem when it is accidental.
  • missing: search engines pick their own preferred version, which is a coin flip you do not want to leave to chance.

Put together:

const { requested, canonical, source } = await getCanonical("https://example.com/");
console.log(classify(requested, canonical), canonical, `(${source ?? "none"})`);
Enter fullscreen mode Exit fullscreen mode

Bulk-checking a whole site

One page is never the real job. After a migration you want to scan a list:

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}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Feed it your sitemap URLs and you have a canonical audit in a few seconds. Add a small concurrency limit if the list is long so you do not hammer the server.

The one people miss: IP canonicalization

Here is a subtle one. If your server answers on its raw IP address as well as your domain, search engines can index both http://203.0.113.10/ and https://example.com/ as separate sites. That splits link equity for content that is literally identical.

The test: resolve the domain to its IP, request the IP directly, and check whether it redirects back to the domain.

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");
}
Enter fullscreen mode Exit fullscreen mode

Note that on shared hosting the IP often belongs to many sites, so this test is most meaningful on a dedicated IP or behind your own reverse proxy.

When you just need an answer, not a script

For a one-off check, or to hand to someone who does not write Node, a browser tool is faster. I built a free one that does all of the above: Pixellize Canonical URL Checker. It reads both the HTML tag and the HTTP Link header, flags self-referencing vs points-elsewhere vs missing, has a bulk mode for up to 20 URLs with CSV export, and an IP canonicalization test. It runs in the browser, nothing is uploaded.

If you want the non-code background on canonicals, these two guides go deeper: how to find the canonical URL of any website and how to change a canonical URL.

Takeaways

  • Canonicals live in two places: the HTML <link> and the HTTP Link header. Check both, and flag conflicts.
  • In Node, fetch plus a small parser is enough to read and classify canonicals.
  • Bulk-scan your sitemap after every migration or theme change, since that is when canonicals silently break.
  • Do not forget IP canonicalization if you run your own server or reverse proxy.

How do you audit canonicals in your stack? Let me know in the comments.

Top comments (0)