notes
Nov 16, 2025 · 1 min read

A Fifty-Line Script That Catches My Dead Links

A reader would occasionally be the first to tell me a link in an old post was dead — a project I’d linked to that got taken down, a docs page that moved. That’s a bad way to find out. The fix wasn’t a service or a plugin, it was a script small enough to fit in one file.

import { readdir, readFile } from 'node:fs/promises';

const files = await readdir('./src/content/blog');
const linkPattern = /\[.*?\]\((https?:\/\/[^)]+)\)/g;

for (const file of files) {
  const text = await readFile(`./src/content/blog/${file}`, 'utf8');
  for (const [, url] of text.matchAll(linkPattern)) {
    const res = await fetch(url, { method: 'HEAD' }).catch(() => null);
    if (!res || res.status >= 400) {
      console.log(`${file}: ${url} -> ${res?.status ?? 'no response'}`);
    }
  }
}

It’s not clever. It reads every markdown file, pulls out every external link with a regex that’s forgiving rather than fully correct, and does a HEAD request against each one. It runs as a scheduled check rather than on every build, since a link that’s fine today can die on its own schedule, unrelated to anything I push.

The first real run flagged six dead links across two-year-old posts, one of which had been dead long enough that the domain now redirected to an unrelated storefront. Fifty lines of unglamorous code found something that would otherwise have sat there quietly embarrassing me until a reader mentioned it, which is about the best return on fifty lines I’ve gotten from anything on this site.