A fast, zero-dependency link checker for markdown-based content today. It scans .md files recursively, checks every HTTP/HTTPS URL, and reports broken links. Built for self-hosted blogs and static sites, with a roadmap toward broader content sources.
Live landing page: https://srmdn.github.io/go-linkchecker/
- Scans all
.mdfiles in a directory recursively - Concurrent HTTP checks (configurable)
- HEAD → GET fallback - handles sites that block HEAD requests
- Global URL deduplication - same URL across multiple files checked once
- Three-section report: Broken, OK, and Skipped
- Optional multipart email delivery via SMTPS (plain-text + styled HTML)
- Skip URLs by regex pattern or ignore specific HTTP status codes (useful for bot-hostile or trusted domains)
- CI-friendly: exits with code
1if broken links found - Zero external dependencies - standard library only
Supported today: any stack that stores content as .md files on disk - Hugo, Jekyll, Astro, Eleventy, VitePress, and similar static site generators.
Not supported yet: WordPress, database-backed CMS, MDX, HTML files, or live site crawling. See issue #2 for the roadmap.
- Planned: MDX and HTML support via broader file extension scanning
- Planned: sitemap input for live sites
- Planned: URL-list input for CMS-backed or exported content
Prebuilt binaries for Linux, macOS, and Windows are published on the GitHub Releases page. You do not need Go installed if you use a release binary.
Replace VERSION below with the release you want, such as 0.5.2.
Linux (amd64):
VERSION=0.5.2
curl -LO "https://github.com/srmdn/go-linkchecker/releases/download/v${VERSION}/go-linkchecker_${VERSION}_linux_amd64.tar.gz"
tar -xzf "go-linkchecker_${VERSION}_linux_amd64.tar.gz"
./go-linkchecker --versionmacOS (arm64):
VERSION=0.5.2
curl -LO "https://github.com/srmdn/go-linkchecker/releases/download/v${VERSION}/go-linkchecker_${VERSION}_darwin_arm64.tar.gz"
tar -xzf "go-linkchecker_${VERSION}_darwin_arm64.tar.gz"
./go-linkchecker --versionWindows (amd64, PowerShell):
$Version = "0.5.2"
curl.exe -LO "https://github.com/srmdn/go-linkchecker/releases/download/v$Version/go-linkchecker_${Version}_windows_amd64.zip"
Expand-Archive "go-linkchecker_${Version}_windows_amd64.zip" -DestinationPath .
.\go-linkchecker.exe --versiongo install github.com/srmdn/go-linkchecker@latestgit clone https://github.com/srmdn/go-linkchecker.git
cd go-linkchecker
go build -o go-linkchecker .go-linkchecker [flags] <directory>Scan current directory:
go-linkchecker .Scan a specific blog content directory:
go-linkchecker ./content/blogOnly show broken links:
go-linkchecker --only-broken ./content/blogSave report to file:
go-linkchecker --only-broken --output report.txt ./content/blogShow the installed version:
go-linkchecker --versionUse --skip-pattern to skip URLs you don't want checked, and --ignore-status to treat specific HTTP status codes as OK. Skipped URLs still appear in the report under a SKIPPED section so you always have full visibility; they are not silently hidden.
Common reasons to skip a URL:
- Bot-hostile sites - some sites return HTTP 403 to all automated requests even though the page is live. They aren't broken, just blocking crawlers. Common examples: Wikipedia, OpenAI, Cloudflare community forum (
community.cloudflare.com). - Affiliate or redirect links - short links that redirect to third-party destinations you don't control. See also
--no-follow-redirects. - Local/dev URLs -
localhost,127.0.0.1, staging domains.
# Skip local URLs
go-linkchecker --skip-pattern "localhost|127\.0\.0\.1" ./content/blog
# Skip known bot-hostile domains
go-linkchecker --skip-pattern "wikipedia\.org|openai\.com|community\.cloudflare\.com" ./content/blog
# Combine multiple patterns
go-linkchecker --skip-pattern "localhost|wikipedia\.org|openai\.com|yourshortlinks\.com" ./content/blog
# Ignore common bot-blocking statuses
go-linkchecker --ignore-status 401,403 ./content/blog
# Combine skip pattern and status ignores
go-linkchecker --skip-pattern "localhost|wikipedia\.org|openai\.com" --ignore-status 401,403 ./content/blogIf you use a URL shortener or affiliate links that redirect to bot-hostile destinations, use --no-follow-redirects instead. This treats any HTTP 3xx response as OK without following the chain:
go-linkchecker --no-follow-redirects ./content/blogThe pattern is a regular expression matched against the full URL. Dots in domain names should be escaped (\.).
The report has three sections:
Checked: 24 | Broken: 1 | OK: 23 | Skipped: 3
------------------------------------------------------------
BROKEN LINKS (1)
[HTTP 404]
https://example.com/old-page
File: ./content/blog/my-post/index.md
------------------------------------------------------------
OK LINKS (23)
[200] https://github.com/...
File: ./content/blog/my-post/index.md
...
------------------------------------------------------------
SKIPPED LINKS (3)
(matched --skip-pattern or --ignore-status, not checked)
https://wikipedia.org/...
File: ./content/blog/my-post/index.md
...
- Broken - checked and returned 4xx/5xx or a network error
- OK - checked and returned 2xx/3xx
- Skipped - matched
--skip-patternor--ignore-status, not checked
Use --only-broken to hide the OK and Skipped sections (useful for email reports or CI).
Example terminal report output:
Pass SMTP credentials via flags or environment variables:
export LINKCHECKER_SMTP_HOST=smtp.example.com
export LINKCHECKER_SMTP_PORT=465
export LINKCHECKER_SMTP_USER=user@example.com
export LINKCHECKER_SMTP_PASS=yourpassword
export LINKCHECKER_SMTP_FROM="Link Checker <user@example.com>"
export LINKCHECKER_SMTP_TO=you@example.com
go-linkchecker --only-broken ./content/blogBy default, email is only sent if broken links are found (--email-only-broken=true). Set --email-only-broken=false to always send.
Email delivery includes:
- Plain-text fallback for simple clients
- Styled HTML layout for inbox readability and screenshots
- Clear summary counts for checked, broken, healthy, and skipped links
- Action-focused broken-link section
- Relative file paths in the email body when files are inside the scanned directory
| Flag | Default | Description |
|---|---|---|
--timeout |
10s |
HTTP request timeout per link |
--concurrency |
5 |
Concurrent link checks |
--only-broken |
false |
Only show broken links in report |
--skip-pattern |
`` | Regex - skip matching URLs (shown as Skipped in report) |
--ignore-status |
`` | Comma-separated HTTP status codes to treat as OK |
--no-follow-redirects |
false |
Treat HTTP 3xx as OK - do not follow redirects |
--output |
`` | Write report to file |
--smtp-host |
`` | SMTP host |
--smtp-port |
465 |
SMTP port (TLS) |
--smtp-user |
`` | SMTP username |
--smtp-pass |
`` | SMTP password |
--smtp-from |
`` | From address |
--smtp-to |
`` | Recipient address |
--email-only-broken |
true |
Only email if broken links exist |
--version |
false |
Print version and exit |
Tagged releases build and publish binaries for:
linux/amd64linux/arm64darwin/amd64darwin/arm64windows/amd64
Each release includes archives and a checksums.txt file for verification.
Example weekly timer on a Linux server:
/etc/systemd/system/linkchecker.service
[Unit]
Description=Weekly link checker
[Service]
Type=oneshot
User=youruser
WorkingDirectory=/your/site/dir
EnvironmentFile=/etc/linkchecker.env
ExecStart=/usr/local/bin/go-linkchecker --only-broken --skip-pattern "localhost|wikipedia\.org" ./content/blog
StandardOutput=journal
StandardError=journal/etc/systemd/system/linkchecker.timer
[Unit]
Description=Weekly link checker timer
Requires=linkchecker.service
[Timer]
OnCalendar=weekly
Persistent=true
[Install]
WantedBy=timers.targetsystemctl enable --now linkchecker.timer| Code | Meaning |
|---|---|
0 |
All checked links healthy (skipped links do not affect exit code) |
1 |
One or more broken links found |
MIT