Catch broken links in CI/CD before they reach production
Add automated broken link detection to your GitHub Actions pipeline. Get native build annotations that appear directly on your pull requests and commits — no extra tools or API keys needed.
Broken links appear as errors directly in your GitHub PR checks
Quick mode checks a single page in under a second
Free tier works without authentication
Set a threshold to fail builds when broken links exceed your limit
When broken links are found, GitHub shows annotations like these on your build:
When all links are healthy:
Download the ready-made workflow directly into your repo:
mkdir -p .github/workflows && curl -o .github/workflows/check-links.yml https://51-68-119-197.sslip.io/tools/check-links.yml
Then edit SITE_URL in the file to point to your website. Or create it manually — add .github/workflows/check-links.yml to your repository:
name: Check for broken links
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 6am
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- name: Check for broken links
run: |
curl -sSL https://51-68-119-197.sslip.io/tools/check-links.sh | bash -s -- --threshold 0 https://yoursite.com
Or use the API directly with curl:
- name: Check for broken links
run: |
RESULT=$(curl -s "https://51-68-119-197.sslip.io/api/deadlinks?url=https://yoursite.com&mode=quick&format=github")
echo "$RESULT"
Change https://yoursite.com to your actual website URL.
That's it. The check runs on every push and PR to main, plus weekly monitoring.
Use the threshold parameter to fail your build when too many broken links are found:
name: Check for broken links
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- name: Check for broken links
run: |
# threshold=0 means ANY broken link fails the build
RESULT=$(curl -sf "https://51-68-119-197.sslip.io/api/deadlinks?url=https://yoursite.com&mode=quick&threshold=0&format=github")
echo "$RESULT"
# RESULT: FAIL/PASS is appended when threshold is set
if echo "$RESULT" | grep -q 'RESULT: FAIL'; then
exit 1
fi
Focus your check with the check_only parameter:
# Check only internal links (same domain)
curl -s "https://51-68-119-197.sslip.io/api/deadlinks?url=https://yoursite.com&mode=quick&check_only=internal&format=github"
# Check only external links (other domains)
curl -s "https://51-68-119-197.sslip.io/api/deadlinks?url=https://yoursite.com&mode=quick&check_only=external&format=github"
New in v2.7: Check all links in any public repository's README.md. Uses github=owner/repo instead of url:
name: Check README links
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1'
jobs:
check-readme:
runs-on: ubuntu-latest
steps:
- name: Check README for broken links
run: |
RESULT=$(curl -sf "https://51-68-119-197.sslip.io/api/deadlinks?github=${{ github.repository }}&format=github&threshold=0")
echo "$RESULT"
if echo "$RESULT" | grep -q 'RESULT: FAIL'; then
exit 1
fi
Uses ${{ github.repository }} to automatically use the current repo. Supports main/master branches and README.md/readme.md variants.
| Parameter | Value | Description |
|---|---|---|
url | string | URL to check (or use github instead) |
github | owner/repo | Check links in a GitHub repo's README.md |
mode | quick | full | quick for CI/CD (sub-second, single page). full for deep crawl. |
format | github | json | csv | markdown | github for annotations, markdown for PR comments |
threshold | integer | Max broken links before fail. JSON: "pass": false. GitHub format: RESULT: FAIL. Use 0 for zero tolerance. |
check_only | internal | external | Filter by link type. Omit to check all. |
name: Link health check
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch: # Manual trigger
jobs:
check-links:
runs-on: ubuntu-latest
strategy:
matrix:
site:
- https://yoursite.com
- https://docs.yoursite.com
- https://blog.yoursite.com
steps:
- name: Check ${{ matrix.site }}
run: |
echo "Checking ${{ matrix.site }}..."
curl -s "https://51-68-119-197.sslip.io/api/deadlinks?url=${{ matrix.site }}&mode=quick&threshold=0&format=github"
Use format=markdown to get a formatted report you can post as a GitHub PR comment:
name: Link check on PR
on:
pull_request:
branches: [main]
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- name: Check for broken links
id: linkcheck
run: |
REPORT=$(curl -s "https://51-68-119-197.sslip.io/api/deadlinks?url=https://yoursite.com&mode=quick&threshold=0&format=markdown")
echo "report<> $GITHUB_OUTPUT
echo "$REPORT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Post PR comment
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `${{ steps.linkcheck.outputs.report }}`
})
The markdown report includes a summary table, broken links detail, redirect chains, and mixed content warnings — all formatted for GitHub rendering.
Show your project's link health with a live badge. Add this to your README.md:

The badge updates on each request and shows:
Also available: /badge/seo?url= (SEO score A-F) and /badge/perf?url= (response time).
Combine automated checking with a README badge for full link health visibility:
name: Check links and update badge
on:
schedule:
- cron: '0 6 * * 1' # Weekly
workflow_dispatch:
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- name: Check for broken links
run: |
curl -sSL https://51-68-119-197.sslip.io/tools/check-links.sh | bash -s -- --threshold 0 https://yoursite.com
# In your README.md, add:
# 
The free tier allows 1 check per 5 minutes. For higher rate limits and deep crawl mode, check our pricing plans or use the API via RapidAPI.
check_only=internal or check_only=external to filter.format=markdown to get a formatted Markdown report with summary table, broken links detail, and redirect chains. Pipe it into actions/github-script to post as a PR comment automatically. See the "Post Results as a PR Comment" section above.format=github output uses standard ::error:: and ::warning:: syntax. For other platforms, use format=json or format=markdown and parse the response./badge/deadlinks?url=YOUR_URL returns a live SVG image showing the current broken link count. It checks on each request (cached briefly). Add the markdown image to your README and it updates automatically.