SEO audit script

A post-build script that catches the defects in a static site which look fine in a browser and broken to a crawler.

Year
2026
Role
Everything
Stack
Node.js
Status
Active

Half of this site's build pipeline. It runs against dist/ and checks for one specific class of defect: things that are perfectly fine in a browser and broken to a crawler.

What it checks#

Split into failures and warnings by what the defect actually costs. A failure exits non-zero in CI.

Per page:

  • Whether the canonical exists, is absolute, is on this origin, and matches where the file is actually served
  • Whether two pages claim the same canonical — the exact accident canonical tags exist to prevent
  • Whether og:url agrees with the canonical (when it does not, the shared link and the indexed link are two different URLs)
  • Whether there is exactly one H1
  • Title and description length, with per-language thresholds — a Chinese character is about twice as wide in a result as a Latin one, so a single absolute count is wrong for both locales
  • Whether the hreflang set includes the page itself (self-reference is required), has exactly one x-default, and uses valid BCP 47 tags
  • Whether every JSON-LD block parses, has @type, defines no duplicate @id, and contains no unescaped </script
  • Whether every <img> has alt text and both dimensions (missing dimensions is a direct CLS cost)
  • Whether external links carry rel=noopener

Across pages:

  • Whether hreflang is reciprocal. Google drops the entire language cluster when A points at B and B does not point back, which is why this is a failure and not a warning.
  • Whether every internal link resolves to a file that actually exists in dist/
  • Whether every indexable page is in the sitemap, and every noindex page is not

What it has actually caught#

The immediate payoff was two bugs I would not have found myself.

One: the language switcher on Chinese tag pages pointed at /en/blog/tags/工程实践 — a slug derived from a Chinese label has no English counterpart. You only see it by clicking, and I was never going to click every tag.

Two: several English pages had meta descriptions of 10 to 19 characters. All inherited from UI strings, all fine locally, and all a content-free line in a search result.