npm package · MIT · Node ≥ 18

static-link-sentinel

Fail CI on broken links before you deploy.

Crawls your built static site (dist/, build/, out/, public/, _site/) for broken internal links, missing assets, missing #anchors and paths whose letter case only works on macOS. With --external it checks outbound links for 404s and redirect chains, showing every hop. No server needed: it resolves URLs the way Netlify, Vercel, GitHub Pages and Cloudflare Pages do.

npm i -D static-link-sentinel
~/project — zsh
$ npx static-link-sentinel dist --site-url https://mysite.com --orphansstatic-link-sentinel: dist/ 5 page(s), 1 stylesheet(s), 13 file(s), 41 internal reference(s) index.html  ✖ error Broken internal link [broken-link]  line 21          <a href="/blog/post-l/"> resolves to no file in the build (would serve 404.html)          fix: did you mean /blog/post-1/?  ✖ error Path case mismatch [case-mismatch]  line 22          fix: Use /blog/post-1/ or rename the file  ⚠ warn  Missing #fragment anchor [missing-fragment]  line 24          fix: did you mean #team?  ℹ info  Absolute link to own domain [absolute-internal]  line 31          fix: Use a relative link: /about
Sample output from the static-link-sentinel README.

The problem

/img/Logo.png works on your Mac and 404s on the Linux CDN. A typo'd slug, a renamed heading anchor or a dead outbound link ships because nothing checks the built output.

static-link-sentinel reads the files you are about to deploy, resolves every reference exactly like your host will, and fails CI with a fix line such as "did you mean /blog/post-1/?".

Features

Internal links & assets

HTML, CSS url()/@import, web manifests, sitemap*.xml and robots.txt are all checked against the build.

Case-mismatch detection

Case-sensitive matching like Linux hosts, with a dedicated case-mismatch error instead of a silent pass.

#anchor checks

Missing fragments are reported with a "did you mean" suggestion.

External links

--external HEADs each unique URL with GET fallback, per-host throttling, retries and a 24 h cache, recording every redirect hop.

Host-aware resolution

Clean URLs, index.html, trailingSlash, basePath, Netlify _redirects and vercel.json.

Markdown sources

--sources checks links and #heading anchors in .md / .mdx before you even build.

Reports for CI

JSON, SARIF and Markdown output (great for job summaries and PR comments).

Zero dependencies

CLI, API, Vite and webpack plugins, and a GitHub Action. Node ≥ 18.

Usage

# Auto-detect the build dir

 npx static-link-sentinel

# Check external links too

 npx static-link-sentinel dist --external --site-url https://example.com

# GitHub Pages project site

 npx static-link-sentinel dist --base-path /my-repo

# Markdown before build

 npx static-link-sentinel --sources docs content README.md

CI / GitHub Action

name: Links
on: [push, pull_request]
jobs:
  links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci && npm run build
      - uses: MrLouisWright/static-link-sentinel@v1
        with:
          dir: dist
          site-url: https://example.com
          external: ${{ github.event_name == 'push' }}
          fail-on: error
          sarif: links.sarif
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: links.sarif }

Options & flags

FlagDescription
-b, --base-path <path>Site is served under a sub-path (e.g. /docs)
-s, --site-url <url>Production URL: absolute links to it are checked as internal
--trailing-slash <m>always | never | ignore (default: ignore, or vercel.json)
--clean-urlsHost serves /about for about.html
--orphansReport pages no other page links to
-e, --externalAlso check external links (HEAD, GET fallback)
--concurrency <n>Parallel external requests (default 8)
--per-host <n>Parallel requests per host (default 2)
--host-delay <ms>Minimum gap between requests to one host (default 150)
--timeout <ms>Per-request timeout (default 10000)
--retries <n>Retries on timeout/5xx/429 (default 2)
--user-agent <ua>User-Agent for external requests
--skip <pattern>Don't fetch matching external URLs (repeatable)
--cache-file <file>External result cache (default .link-cache.json)
--cache-ttl <hours>Cache lifetime (default 24)
--no-cacheDon't read or write the cache
--sources [pattern...]Also scan markdown/mdx under the project
-i, --ignore <pattern>Ignore matching URLs (glob, substring or re:regex; repeatable)
-c, --config <file>JSON config
--fail-on <level>error | warn | none (default error)
--jsonJSON report on stdout
--sarif [file]SARIF 2.1.0 to stdout or file
--markdown [file]Markdown summary to stdout or file
-q, --quietHide info-level findings

Full reference, config file format and API in the README.

Support this project

Support open-source security tools

I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.

Buy me a coffee (opens Stripe in a new tab)

Secure checkout by Stripe

Portrait of Louis Wright

About the author

Louis Wright

Cyber security researcher and software engineer based in the UK.

More tools by Louis Wright

58 tools

Sentinels security scanners & CI gates · 19