A fast, zero-config CLI tool that crawls a website and identifies broken links — locally or in production.
✅ [200] https://mysite.com/ 35ms
✅ [200] https://mysite.com/about 11ms
❌ [404] https://mysite.com/old-post 12ms
❌ [301] https://mysite.com/moved-page 8ms → 404 https://mysite.com/gone
💥 [ERR] https://mysite.com/api/health 8000ms — Request timed out
─────────────────────────────────────────────
Summary
─────────────────────────────────────────────
Total checked : 47
✅ Live : 41
❌ Broken : 4
⚠️ Redirects : 2
💥 Errors : 0
Completed in 4.2s
# Run without installing
npx @initysl/checker https://yoursite.com
# Install globally
npm install -g @initysl/checker
checker https://yoursite.comchecker <url> [options]| Flag | Default | Description |
|---|---|---|
-d, --depth <n> |
2 |
Max crawl depth |
-c, --concurrency <n> |
5 |
Max requests in flight at once |
-t, --timeout <ms> |
8000 |
Per-request timeout |
-o, --output <file> |
— | Export results to JSON |
--ignore <patterns...> |
— | URL patterns to skip (* is a wildcard) |
--only-broken |
false |
Only show broken links and errors |
--exit-code |
false |
Exit 1 if any link is broken or errors |
--no-robots |
— | Skip robots.txt rules |
--no-assets |
— | Check only <a href>, not images/CSS/JS |
| Code | Meaning |
|---|---|
0 |
Crawl finished; nothing broken (or --exit-code not given) |
1 |
Broken links or errors found, or the crawl could not start |
130 |
Interrupted with Ctrl+C (a partial summary is still printed) |
With --exit-code, both ❌ broken (4xx/5xx) and 💥 errors (timeout, DNS
failure, connection refused) fail the build — a visitor cannot follow either.
# Basic crawl
checker https://mysite.com
# Localhost before deploying
checker http://localhost:3000 --depth 2
# Only show broken links
checker https://mysite.com --only-broken
# Ignore API routes and admin pages
checker https://mysite.com --ignore "/api/*" "/admin/*"
# Links only — skip images, stylesheets and scripts
checker https://mysite.com --no-assets
# Export full report to JSON
checker https://mysite.com --output report.json
# CI/CD — exit 1 if any broken links found
checker https://mysite.com --exit-codeWorks out of the box against any local dev server — no restrictions, no rate limits:
# Next.js
npm run dev
checker http://localhost:3000 --depth 2
# Astro / Hugo / Jekyll
npm run build && npm run preview
checker http://localhost:4321
# Any static server
npx serve ./dist
checker http://localhost:3000The port is part of a site's identity, so a link to localhost:8080 found while
crawling localhost:3000 is treated as external — checked, but not crawled.
Drop this into your workflow to automatically block deploys with broken links:
name: Check for broken links
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
broken-links:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run checker
run: npx @initysl/checker https://yoursite.com --depth 2 --only-broken --exit-codeThe
--exit-codeflag causes the job to fail if any broken links are found, blocking the PR from merging.
- name: Deploy to staging
run: # your deploy step
- name: Check broken links on staging
run: npx @initysl/checker ${{ secrets.STAGING_URL }} --exit-code --ignore "/api/*"- name: Check broken links
run: npx @initysl/checker https://yoursite.com --output report.json --exit-code || true
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: broken-links-report
path: report.jsonUse --output to export a full structured report:
checker https://mysite.com --output report.json{
"meta": {
"url": "https://mysite.com/",
"generatedAt": "2026-05-30T10:00:00.000Z",
"elapsedMs": 4231,
"options": {
"depth": 2,
"concurrency": 5,
"timeout": 8000,
"ignore": [],
"robots": true,
"assets": true
}
},
"stats": {
"total": 47,
"live": 41,
"broken": 4,
"redirects": 2,
"errors": 0
},
"results": [
{
"url": "https://mysite.com/moved-page",
"status": 301,
"type": "broken",
"linkType": "internal",
"via": "a",
"sourceUrl": "https://mysite.com/blog",
"finalUrl": "https://mysite.com/gone",
"finalStatus": 404,
"redirects": 1,
"responseTime": 12,
"depth": 1,
"error": null
}
]
}| Field | Meaning |
|---|---|
status |
Status the link itself returned |
type |
live · broken · redirect · error |
linkType |
internal or external |
via |
seed, a (an anchor) or asset (image, stylesheet, script) |
sourceUrl |
Page the link was found on (null for the seed URL) |
finalUrl |
Where the redirect chain ended, if it redirected |
finalStatus |
Status at the end of the chain — this decides broken vs redirect |
redirects |
Number of hops followed |
depth |
Distance from the seed URL |
Seed URL → checked itself (a 404 homepage is a broken link)
↓
Fetch page → parse <a href> + <img>/<link>/<script> sources
↓
For each link → HEAD request (GET fallback), follow redirects to the end
↓
Classify → live / broken / redirect / error
↓
Internal links within depth → enqueue → repeat
- BFS crawl with configurable depth
- Bounded concurrency —
--concurrencycaps every request in flight, not just pages - HEAD first for speed, with a GET fallback when a server rejects HEAD (400/403/404/405/501)
- Redirects resolved to the end — a
301into a404is reported as broken, not as a redirect - Deduplication — each URL checked exactly once across all pages
- Assets checked — images, stylesheets, scripts and fonts are checked but never crawled
- robots.txt respected by default, fetched once per host
- One retry on transient network failures, so a blip doesn't redden CI
- Readable errors —
DNS lookup failed,Connection refused,Request timed out - Ctrl+C stops in-flight requests and still prints a summary
- Bot-protected sites report as broken. Some hosts (npmjs.com, LinkedIn,
Reddit, sites behind aggressive WAFs) return
403to any client that is not a browser, no matter the method. Skip them with--ignore "npmjs.com/*" "linkedin.com/*". - Links are read from the served HTML, so anything a client-side framework renders after hydration is not seen. Point checker at a built/SSR'd site.
- Fragments are not verified —
/page#sectionis checked as/page; the presence of the anchor on that page is not confirmed.
git clone https://github.com/initysl/checker
cd checker
npm install
# Run
node bin/checker.js https://example.com
# Test — spins up a local server and exercises the full crawl
npm testMIT © initysl