GitHub Actions Link Checker Guide
Purpose
This guide explains how to set up automated broken-link detection for markdown documentation repositories using GitHub Actions. When enabled, every push and pull request is checked for broken internal links (cross-references between docs) and optionally broken external URLs.
Why this matters: Documentation repos rely on relative markdown links ([Pipeline SOP](sales-pipeline/ghl-prospecting-pipeline-sop.md)). When files are moved, renamed, or archived, those links break silently. A CI link checker catches them before they reach readers.
Tool: markdown-link-check
markdown-link-check is a Node.js tool that parses markdown files and verifies every link resolves. The GitHub Action wrapper (gaurav-nelson/github-action-markdown-link-check) runs it on every push/PR with zero configuration required.
Why This Tool
| Criteria | markdown-link-check | Alternatives |
|---|---|---|
| Markdown-native | Yes -- understands [text](url) and reference links | lychee (Rust, faster but less markdown-aware) |
| Relative link support | Yes -- resolves paths relative to the file | Most alternatives also support this |
| Config file | .markdown-link-check.json in repo root | Varies |
| Free for public/private repos | Yes (GitHub Actions minutes apply) | Same |
| Active maintenance | Yes | lychee is also actively maintained |
Setup
Step 1: Create the Workflow File
Create .github/workflows/check-links.yml:
name: Check Markdown Links
on:
push:
branches: [master]
paths: ['**/*.md']
pull_request:
branches: [master]
paths: ['**/*.md']
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
use-verbose-mode: 'yes'
config-file: '.markdown-link-check.json'
Key settings:
paths: ['**/*.md']-- only runs when markdown files change (saves CI minutes)use-quiet-mode-- suppresses passing links, only shows failuresuse-verbose-mode-- shows which file each failure came from
Step 2: Create the Configuration File
Create .markdown-link-check.json in the repo root:
{
"ignorePatterns": [
{ "pattern": "^https://app.symphonycore.com" },
{ "pattern": "^https://storage.googleapis.com" },
{ "pattern": "^mailto:" }
],
"replacementPatterns": [],
"httpHeaders": [],
"timeout": "10s",
"retryOn429": true,
"retryCount": 3,
"fallbackRetryDelay": "5s",
"aliveStatusCodes": [200, 206]
}
Configuration explained:
| Setting | Purpose |
|---|---|
ignorePatterns | Skip URLs that are known-good or require auth to resolve |
timeout | How long to wait for external URLs (10s is reasonable) |
retryOn429 | Retry when rate-limited (common with GitHub, LinkedIn, etc.) |
retryCount | Number of retries before marking a link as dead |
aliveStatusCodes | HTTP status codes that count as "alive" |
Step 3: Tune Ignore Patterns
Add patterns for URLs that will always fail in CI (require login, block bots, or are dynamically rendered):
{
"ignorePatterns": [
{ "pattern": "^https://app.symphonycore.com" },
{ "pattern": "^https://storage.googleapis.com" },
{ "pattern": "^https://app.gohighlevel.com" },
{ "pattern": "^https://www.linkedin.com" },
{ "pattern": "^https://symphonycore.com" },
{ "pattern": "^mailto:" },
{ "pattern": "^#" }
]
}
Why these are ignored:
app.symphonycore.com/app.gohighlevel.com-- require GHL authlinkedin.com-- blocks automated requests (returns 999)symphonycore.com-- JS-rendered, returns 200 but link checker can't verify anchorsstorage.googleapis.com-- signed URLs or CDN assets, may require authmailto:-- not HTTP links#-- in-page anchors (these are checked by default but can produce false positives with heading ID mismatches)
What It Catches
| Scenario | Detected? | Example |
|---|---|---|
| Renamed file, old link remains | Yes | [SOP](old-name.md) when file is now new-name.md |
| Moved file to different directory | Yes | [SOP](/sops/file) when file moved to sales-pipeline/ |
| Deleted/archived file | Yes | Link to a file that no longer exists in the repo |
| Typo in relative path | Yes | [SOP](sales-pipline/file.md) (misspelled directory) |
| Broken external URL (404) | Yes | Link to a webpage that's been taken down |
| External URL behind auth | No | Covered by ignorePatterns |
| Anchor link to wrong heading | Partial | Detects missing files but not all heading-ID mismatches |
Reading the Output
Passing Run
FILE: 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md
[OK] ../ghl-prospecting-audit-accuracy-sop.md
[OK] ghl-prospecting-pipeline-sop.md
[OK] ../../06-team-training/platform-training/kb-015-ghl-prospecting-tool.md
7 links checked. 0 broken.
Failing Run
FILE: 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md
[OK] ../ghl-prospecting-audit-accuracy-sop.md
[FAIL] ghl-prospecting-pipline-sop.md --> Status: 400
[OK] ../../06-team-training/platform-training/kb-015-ghl-prospecting-tool.md
7 links checked. 1 broken.
ERROR: 1 dead link found!
The [FAIL] line shows the broken path. Fix the link in the source file, commit, and push.
Running Locally
To check links without pushing to GitHub:
# Install globally
npm install -g markdown-link-check
# Check a single file
markdown-link-check 04-operations/sops/sales-pipeline/prospect-email-discovery-sop.md
# Check all markdown files in the repo
find . -name '*.md' -not -path './node_modules/*' -exec markdown-link-check --config .markdown-link-check.json {} \;
For faster local checks, lychee is a Rust alternative:
# Install (Windows via scoop or cargo)
scoop install lychee
# or
cargo install lychee
# Check all markdown files
lychee "**/*.md" --exclude-path node_modules
Maintenance
When to Update ignore Patterns
- When you add a new external service URL that blocks bots (add to
ignorePatterns) - When a previously-ignored service becomes publicly accessible (remove from
ignorePatterns) - When false positives appear in CI for specific URLs
When to Update the Workflow
- When the default branch changes (update
branches: [master]) - When you want to add scheduled checks (e.g., weekly external URL validation):
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9:00 UTC
Cost
| Factor | Impact |
|---|---|
| GitHub Actions minutes | ~30-60 seconds per run for a repo this size |
| Free tier (public repos) | Unlimited |
| Free tier (private repos) | 2,000 minutes/month (this uses < 1% of that) |
| External request rate limits | Mitigated by retryOn429 and ignorePatterns |
Related Documents
- SC Archive & Retention Standard -- archiving process that creates broken-link risk
- SC Tagging Standard -- frontmatter conventions for docs in this repo
Document History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-03-02 | Initial version -- setup, configuration, ignore patterns, local usage |
Document Information
- Version: 1.0
- Created: 2026-03-02
- Updated: 2026-03-02
- Location:
11-engineering/github-actions-link-checker-guide.md