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:urlagrees 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.