Loading...
Validates all links in markdown files to detect broken links and references
{
"hookConfig": {
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/markdown-link-checker.sh",
"matchers": [
"write",
"edit"
]
}
}
},
"scriptContent": "#!/usr/bin/env bash\n\n# Read the tool input from stdin\nINPUT=$(cat)\nTOOL_NAME=$(echo \"$INPUT\" | jq -r '.tool_name')\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // .tool_input.path // \"\"')\n\nif [ -z \"$FILE_PATH\" ]; then\n exit 0\nfi\n\n# Check if this is a markdown file\nif [[ \"$FILE_PATH\" == *.md ]] || [[ \"$FILE_PATH\" == *.mdx ]] || [[ \"$FILE_PATH\" == *.markdown ]]; then\n echo \"🔗 Markdown Link Validation for: $(basename \"$FILE_PATH\")\" >&2\n \n # Initialize validation counters\n ERRORS=0\n WARNINGS=0\n VALIDATIONS_PASSED=0\n TOTAL_LINKS=0\n EXTERNAL_LINKS=0\n INTERNAL_LINKS=0\n \n # Function to report validation results\n report_validation() {\n local level=\"$1\"\n local message=\"$2\"\n \n case \"$level\" in\n \"ERROR\")\n echo \"❌ ERROR: $message\" >&2\n ERRORS=$((ERRORS + 1))\n ;;\n \"WARNING\")\n echo \"⚠️ WARNING: $message\" >&2\n WARNINGS=$((WARNINGS + 1))\n ;;\n \"PASS\")\n echo \"✅ PASS: $message\" >&2\n VALIDATIONS_PASSED=$((VALIDATIONS_PASSED + 1))\n ;;\n \"INFO\")\n echo \"ℹ️ INFO: $message\" >&2\n ;;\n esac\n }\n \n # Check if file exists and is readable\n if [ ! -f \"$FILE_PATH\" ]; then\n report_validation \"ERROR\" \"Markdown file not found: $FILE_PATH\"\n exit 1\n fi\n \n if [ ! -r \"$FILE_PATH\" ]; then\n report_validation \"ERROR\" \"Markdown file is not readable: $FILE_PATH\"\n exit 1\n fi\n \n # Get file information\n FILE_NAME=\"$(basename \"$FILE_PATH\")\"\n FILE_DIR=\"$(dirname \"$FILE_PATH\")\"\n FILE_SIZE=$(wc -c < \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n LINE_COUNT=$(wc -l < \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n \n echo \"📊 Markdown file: $FILE_NAME ($(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines)\" >&2\n \n # 1. Extract All Links from Markdown\n echo \"🔍 Extracting links from markdown...\" >&2\n \n # Create temporary files for link analysis\n TEMP_LINKS=\"/tmp/markdown_links_$$\"\n TEMP_IMAGES=\"/tmp/markdown_images_$$\"\n TEMP_ANCHORS=\"/tmp/markdown_anchors_$$\"\n \n # Extract markdown links [text](url)\n grep -oE '\\[([^\\]]+)\\]\\(([^)]+)\\)' \"$FILE_PATH\" | sed 's/\\[.*\\](\\(.*\\))/\\1/' > \"$TEMP_LINKS\" 2>/dev/null || true\n \n # Extract image links \n grep -oE '!\\[([^\\]]*)\\]\\(([^)]+)\\)' \"$FILE_PATH\" | sed 's/!\\[.*\\](\\(.*\\))/\\1/' > \"$TEMP_IMAGES\" 2>/dev/null || true\n \n # Extract reference-style links\n grep -oE '\\[([^\\]]+)\\]\\[([^\\]]+)\\]' \"$FILE_PATH\" | sed 's/\\[.*\\]\\[\\(.*\\)\\]/\\1/' >> \"$TEMP_LINKS\" 2>/dev/null || true\n \n # Count total links\n TOTAL_LINKS=$(cat \"$TEMP_LINKS\" \"$TEMP_IMAGES\" 2>/dev/null | wc -l || echo \"0\")\n \n if [ \"$TOTAL_LINKS\" -eq 0 ]; then\n echo \" 📋 No links found in markdown file\" >&2\n report_validation \"INFO\" \"No links to validate\"\n else\n echo \" 📊 Found $TOTAL_LINKS total links/images\" >&2\n fi\n \n # 2. Validate External Links\n echo \"🌐 Validating external links...\" >&2\n \n # Try using markdown-link-check if available\n if command -v npx &> /dev/null; then\n echo \" 🔍 Using markdown-link-check for comprehensive validation...\" >&2\n \n # Create a temporary config if none exists\n CONFIG_FILE=\".markdown-link-check.json\"\n TEMP_CONFIG=false\n \n if [ ! -f \"$CONFIG_FILE\" ]; then\n TEMP_CONFIG=true\n CONFIG_FILE=\"/tmp/markdown_link_config_$$\"\n cat > \"$CONFIG_FILE\" << 'EOF'\n{\n \"timeout\": \"30s\",\n \"retryOn429\": true,\n \"retryCount\": 3,\n \"fallbackProtocols\": [\"http\", \"https\"],\n \"ignorePatterns\": [\n { \"pattern\": \"^http://localhost\" },\n { \"pattern\": \"^https://localhost\" },\n { \"pattern\": \"^http://127.0.0.1\" },\n { \"pattern\": \"^#\" }\n ]\n}\nEOF\n fi\n \n MLC_OUTPUT_FILE=\"/tmp/mlc_output_$$\"\n if timeout 60s npx markdown-link-check \"$FILE_PATH\" --config \"$CONFIG_FILE\" > \"$MLC_OUTPUT_FILE\" 2>&1; then\n # Parse results\n DEAD_LINKS=$(grep -c '✖' \"$MLC_OUTPUT_FILE\" 2>/dev/null || echo \"0\")\n ALIVE_LINKS=$(grep -c '✓' \"$MLC_OUTPUT_FILE\" 2>/dev/null || echo \"0\")\n \n if [ \"$DEAD_LINKS\" -eq 0 ]; then\n report_validation \"PASS\" \"All external links are valid ($ALIVE_LINKS checked)\"\n else\n report_validation \"ERROR\" \"Found $DEAD_LINKS dead external links\"\n echo \" 📝 Dead links details:\" >&2\n grep '✖' \"$MLC_OUTPUT_FILE\" | head -5 | while read line; do\n echo \" $line\" >&2\n done\n fi\n else\n # Fallback to basic URL validation\n echo \" ⚠️ markdown-link-check failed, using basic validation...\" >&2\n \n # Extract HTTP/HTTPS URLs\n EXTERNAL_URLS=$(grep -oE 'https?://[^)]+' \"$TEMP_LINKS\" 2>/dev/null || true)\n \n if [ -n \"$EXTERNAL_URLS\" ]; then\n EXTERNAL_COUNT=$(echo \"$EXTERNAL_URLS\" | wc -l)\n echo \" 🌐 Found $EXTERNAL_COUNT external URLs to validate\" >&2\n \n # Basic URL validation using curl\n if command -v curl &> /dev/null; then\n EXTERNAL_ERRORS=0\n echo \"$EXTERNAL_URLS\" | head -10 | while read -r url; do\n if [ -n \"$url\" ]; then\n if curl -s --head --max-time 10 \"$url\" >/dev/null 2>&1; then\n echo \" ✅ Valid: $url\" >&2\n else\n echo \" ❌ Invalid: $url\" >&2\n EXTERNAL_ERRORS=$((EXTERNAL_ERRORS + 1))\n fi\n fi\n done\n else\n echo \" ⚠️ curl not available for URL validation\" >&2\n fi\n else\n echo \" 📋 No external URLs found\" >&2\n fi\n fi\n \n # Clean up temporary config if created\n [ \"$TEMP_CONFIG\" = true ] && rm -f \"$CONFIG_FILE\"\n rm -f \"$MLC_OUTPUT_FILE\"\n \n else\n echo \" ⚠️ npx not available, using basic link validation\" >&2\n fi\n \n # 3. Validate Internal Links\n echo \"📁 Validating internal links and references...\" >&2\n \n # Extract internal links (relative paths)\n INTERNAL_URLS=$(cat \"$TEMP_LINKS\" \"$TEMP_IMAGES\" 2>/dev/null | grep -E '^\\./|^\\.\\./' || true)\n ABSOLUTE_PATHS=$(cat \"$TEMP_LINKS\" \"$TEMP_IMAGES\" 2>/dev/null | grep -E '^/' || true)\n \n INTERNAL_ERRORS=0\n \n # Check relative path links\n if [ -n \"$INTERNAL_URLS\" ]; then\n echo \" 📂 Checking relative path links...\" >&2\n \n echo \"$INTERNAL_URLS\" | while read -r link; do\n if [ -n \"$link\" ]; then\n # Remove anchor if present\n FILE_PART=$(echo \"$link\" | cut -d'#' -f1)\n ANCHOR_PART=$(echo \"$link\" | cut -d'#' -f2)\n \n if [ -n \"$FILE_PART\" ]; then\n # Resolve relative path\n RESOLVED_PATH=\"$(cd \"$FILE_DIR\" && realpath \"$FILE_PART\" 2>/dev/null || echo \"$FILE_PART\")\"\n \n if [ -f \"$RESOLVED_PATH\" ] || [ -d \"$RESOLVED_PATH\" ]; then\n echo \" ✅ Valid: $link\" >&2\n else\n echo \" ❌ Broken: $link (resolved to: $RESOLVED_PATH)\" >&2\n INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))\n fi\n fi\n \n # Check anchor if present (simplified check)\n if [ \"$link\" != \"$FILE_PART\" ] && [ -n \"$ANCHOR_PART\" ]; then\n echo \" ℹ️ Anchor found: #$ANCHOR_PART\" >&2\n fi\n fi\n done\n fi\n \n # Check absolute path links\n if [ -n \"$ABSOLUTE_PATHS\" ]; then\n echo \" 📁 Checking absolute path links...\" >&2\n \n echo \"$ABSOLUTE_PATHS\" | while read -r link; do\n if [ -n \"$link\" ]; then\n FILE_PART=$(echo \"$link\" | cut -d'#' -f1)\n \n if [ -f \"$FILE_PART\" ] || [ -d \"$FILE_PART\" ]; then\n echo \" ✅ Valid: $link\" >&2\n else\n echo \" ❌ Broken: $link\" >&2\n INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))\n fi\n fi\n done\n fi\n \n if [ \"$INTERNAL_ERRORS\" -eq 0 ]; then\n report_validation \"PASS\" \"All internal links are valid\"\n else\n report_validation \"ERROR\" \"Found $INTERNAL_ERRORS broken internal links\"\n fi\n \n # 4. Validate Image Links\n echo \"🖼️ Validating image links...\" >&2\n \n IMAGE_COUNT=$(cat \"$TEMP_IMAGES\" 2>/dev/null | wc -l || echo \"0\")\n \n if [ \"$IMAGE_COUNT\" -gt 0 ]; then\n echo \" 📊 Found $IMAGE_COUNT image links\" >&2\n \n IMAGE_ERRORS=0\n \n cat \"$TEMP_IMAGES\" | while read -r img_link; do\n if [ -n \"$img_link\" ]; then\n if [[ \"$img_link\" == http* ]]; then\n echo \" 🌐 External image: $img_link\" >&2\n else\n # Local image file\n if [[ \"$img_link\" == /* ]]; then\n IMG_PATH=\"$img_link\"\n else\n IMG_PATH=\"$FILE_DIR/$img_link\"\n fi\n \n if [ -f \"$IMG_PATH\" ]; then\n echo \" ✅ Valid image: $img_link\" >&2\n else\n echo \" ❌ Missing image: $img_link\" >&2\n IMAGE_ERRORS=$((IMAGE_ERRORS + 1))\n fi\n fi\n fi\n done\n \n if [ \"$IMAGE_ERRORS\" -eq 0 ]; then\n report_validation \"PASS\" \"All local images found\"\n else\n report_validation \"ERROR\" \"$IMAGE_ERRORS local images missing\"\n fi\n else\n echo \" 📋 No image links found\" >&2\n fi\n \n # 5. Validate Internal Anchors\n echo \"⚓ Validating document anchors...\" >&2\n \n # Extract anchor-only links (starting with #)\n ANCHOR_LINKS=$(cat \"$TEMP_LINKS\" | grep '^#' 2>/dev/null || true)\n \n if [ -n \"$ANCHOR_LINKS\" ]; then\n echo \" 🔗 Found anchor links to validate\" >&2\n \n # Extract headers from markdown to validate anchors\n HEADERS_FILE=\"/tmp/markdown_headers_$$\"\n grep -E '^#{1,6} ' \"$FILE_PATH\" | sed 's/^#* *//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g' > \"$HEADERS_FILE\" 2>/dev/null || true\n \n ANCHOR_ERRORS=0\n \n echo \"$ANCHOR_LINKS\" | while read -r anchor; do\n if [ -n \"$anchor\" ]; then\n CLEAN_ANCHOR=$(echo \"$anchor\" | sed 's/^#//' | tr '[:upper:]' '[:lower:]')\n \n if grep -q \"^$CLEAN_ANCHOR$\" \"$HEADERS_FILE\" 2>/dev/null; then\n echo \" ✅ Valid anchor: $anchor\" >&2\n else\n echo \" ❌ Invalid anchor: $anchor\" >&2\n ANCHOR_ERRORS=$((ANCHOR_ERRORS + 1))\n fi\n fi\n done\n \n rm -f \"$HEADERS_FILE\"\n \n if [ \"$ANCHOR_ERRORS\" -eq 0 ]; then\n report_validation \"PASS\" \"All document anchors valid\"\n else\n report_validation \"ERROR\" \"$ANCHOR_ERRORS invalid document anchors\"\n fi\n else\n echo \" 📋 No document anchors found\" >&2\n fi\n \n # 6. Markdown Quality Checks\n echo \"📝 Markdown quality and accessibility checks...\" >&2\n \n # Check for alt text in images\n IMAGES_WITHOUT_ALT=$(grep -c '!\\[\\](' \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n \n if [ \"$IMAGES_WITHOUT_ALT\" -gt 0 ]; then\n report_validation \"WARNING\" \"$IMAGES_WITHOUT_ALT images missing alt text for accessibility\"\n else\n if [ \"$IMAGE_COUNT\" -gt 0 ]; then\n report_validation \"PASS\" \"All images have alt text\"\n fi\n fi\n \n # Check for bare URLs (not wrapped in markdown links)\n BARE_URLS=$(grep -oE 'https?://[^\\s\\)\\]]+' \"$FILE_PATH\" | grep -v '](http' | head -5 | wc -l || echo \"0\")\n \n if [ \"$BARE_URLS\" -gt 0 ]; then\n report_validation \"WARNING\" \"Found $BARE_URLS bare URLs - consider wrapping in markdown links\"\n fi\n \n # 7. Generate Validation Summary\n echo \"\" >&2\n echo \"📋 Markdown Link Validation Summary:\" >&2\n echo \"===================================\" >&2\n echo \" 📄 File: $FILE_NAME\" >&2\n echo \" 📏 Size: $(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines\" >&2\n echo \" 🔗 Total links: $TOTAL_LINKS\" >&2\n echo \" 🖼️ Images: $IMAGE_COUNT\" >&2\n echo \" ✅ Validations passed: $VALIDATIONS_PASSED\" >&2\n echo \" ⚠️ Warnings: $WARNINGS\" >&2\n echo \" ❌ Errors: $ERRORS\" >&2\n \n if [ \"$ERRORS\" -eq 0 ]; then\n if [ \"$WARNINGS\" -eq 0 ]; then\n echo \" 🎉 Status: EXCELLENT - All links are valid and accessible\" >&2\n else\n echo \" ✅ Status: GOOD - Links are valid with minor accessibility recommendations\" >&2\n fi\n else\n echo \" ❌ Status: ERRORS - Found broken links that must be fixed\" >&2\n fi\n \n echo \"\" >&2\n echo \"💡 Markdown Link Best Practices:\" >&2\n echo \" • Use descriptive link text instead of 'click here'\" >&2\n echo \" • Add alt text to all images for accessibility\" >&2\n echo \" • Use relative paths for internal documentation\" >&2\n echo \" • Validate external links regularly\" >&2\n echo \" • Keep anchor links synchronized with headers\" >&2\n echo \" • Consider using reference-style links for readability\" >&2\n \n # Clean up temporary files\n rm -f \"$TEMP_LINKS\" \"$TEMP_IMAGES\" \"$TEMP_ANCHORS\"\n \n # Exit with error if there are critical link issues\n if [ \"$ERRORS\" -gt 0 ]; then\n echo \"⚠️ Markdown link validation completed with errors\" >&2\n exit 1\n fi\n \nelse\n # Not a markdown file\n exit 0\nfi\n\nexit 0"
}.claude/hooks/~/.claude/hooks/{
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/markdown-link-checker.sh",
"matchers": [
"write",
"edit"
]
}
}
}#!/usr/bin/env bash
# Read the tool input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Check if this is a markdown file
if [[ "$FILE_PATH" == *.md ]] || [[ "$FILE_PATH" == *.mdx ]] || [[ "$FILE_PATH" == *.markdown ]]; then
echo "🔗 Markdown Link Validation for: $(basename "$FILE_PATH")" >&2
# Initialize validation counters
ERRORS=0
WARNINGS=0
VALIDATIONS_PASSED=0
TOTAL_LINKS=0
EXTERNAL_LINKS=0
INTERNAL_LINKS=0
# Function to report validation results
report_validation() {
local level="$1"
local message="$2"
case "$level" in
"ERROR")
echo "❌ ERROR: $message" >&2
ERRORS=$((ERRORS + 1))
;;
"WARNING")
echo "⚠️ WARNING: $message" >&2
WARNINGS=$((WARNINGS + 1))
;;
"PASS")
echo "✅ PASS: $message" >&2
VALIDATIONS_PASSED=$((VALIDATIONS_PASSED + 1))
;;
"INFO")
echo "ℹ️ INFO: $message" >&2
;;
esac
}
# Check if file exists and is readable
if [ ! -f "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file not found: $FILE_PATH"
exit 1
fi
if [ ! -r "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file is not readable: $FILE_PATH"
exit 1
fi
# Get file information
FILE_NAME="$(basename "$FILE_PATH")"
FILE_DIR="$(dirname "$FILE_PATH")"
FILE_SIZE=$(wc -c < "$FILE_PATH" 2>/dev/null || echo "0")
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo "0")
echo "📊 Markdown file: $FILE_NAME ($(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines)" >&2
# 1. Extract All Links from Markdown
echo "🔍 Extracting links from markdown..." >&2
# Create temporary files for link analysis
TEMP_LINKS="/tmp/markdown_links_$$"
TEMP_IMAGES="/tmp/markdown_images_$$"
TEMP_ANCHORS="/tmp/markdown_anchors_$$"
# Extract markdown links [text](url)
grep -oE '\[([^\]]+)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/\[.*\](\(.*\))/\1/' > "$TEMP_LINKS" 2>/dev/null || true
# Extract image links 
grep -oE '!\[([^\]]*)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/!\[.*\](\(.*\))/\1/' > "$TEMP_IMAGES" 2>/dev/null || true
# Extract reference-style links
grep -oE '\[([^\]]+)\]\[([^\]]+)\]' "$FILE_PATH" | sed 's/\[.*\]\[\(.*\)\]/\1/' >> "$TEMP_LINKS" 2>/dev/null || true
# Count total links
TOTAL_LINKS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$TOTAL_LINKS" -eq 0 ]; then
echo " 📋 No links found in markdown file" >&2
report_validation "INFO" "No links to validate"
else
echo " 📊 Found $TOTAL_LINKS total links/images" >&2
fi
# 2. Validate External Links
echo "🌐 Validating external links..." >&2
# Try using markdown-link-check if available
if command -v npx &> /dev/null; then
echo " 🔍 Using markdown-link-check for comprehensive validation..." >&2
# Create a temporary config if none exists
CONFIG_FILE=".markdown-link-check.json"
TEMP_CONFIG=false
if [ ! -f "$CONFIG_FILE" ]; then
TEMP_CONFIG=true
CONFIG_FILE="/tmp/markdown_link_config_$$"
cat > "$CONFIG_FILE" << 'EOF'
{
"timeout": "30s",
"retryOn429": true,
"retryCount": 3,
"fallbackProtocols": ["http", "https"],
"ignorePatterns": [
{ "pattern": "^http://localhost" },
{ "pattern": "^https://localhost" },
{ "pattern": "^http://127.0.0.1" },
{ "pattern": "^#" }
]
}
EOF
fi
MLC_OUTPUT_FILE="/tmp/mlc_output_$$"
if timeout 60s npx markdown-link-check "$FILE_PATH" --config "$CONFIG_FILE" > "$MLC_OUTPUT_FILE" 2>&1; then
# Parse results
DEAD_LINKS=$(grep -c '✖' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
ALIVE_LINKS=$(grep -c '✓' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
if [ "$DEAD_LINKS" -eq 0 ]; then
report_validation "PASS" "All external links are valid ($ALIVE_LINKS checked)"
else
report_validation "ERROR" "Found $DEAD_LINKS dead external links"
echo " 📝 Dead links details:" >&2
grep '✖' "$MLC_OUTPUT_FILE" | head -5 | while read line; do
echo " $line" >&2
done
fi
else
# Fallback to basic URL validation
echo " ⚠️ markdown-link-check failed, using basic validation..." >&2
# Extract HTTP/HTTPS URLs
EXTERNAL_URLS=$(grep -oE 'https?://[^)]+' "$TEMP_LINKS" 2>/dev/null || true)
if [ -n "$EXTERNAL_URLS" ]; then
EXTERNAL_COUNT=$(echo "$EXTERNAL_URLS" | wc -l)
echo " 🌐 Found $EXTERNAL_COUNT external URLs to validate" >&2
# Basic URL validation using curl
if command -v curl &> /dev/null; then
EXTERNAL_ERRORS=0
echo "$EXTERNAL_URLS" | head -10 | while read -r url; do
if [ -n "$url" ]; then
if curl -s --head --max-time 10 "$url" >/dev/null 2>&1; then
echo " ✅ Valid: $url" >&2
else
echo " ❌ Invalid: $url" >&2
EXTERNAL_ERRORS=$((EXTERNAL_ERRORS + 1))
fi
fi
done
else
echo " ⚠️ curl not available for URL validation" >&2
fi
else
echo " 📋 No external URLs found" >&2
fi
fi
# Clean up temporary config if created
[ "$TEMP_CONFIG" = true ] && rm -f "$CONFIG_FILE"
rm -f "$MLC_OUTPUT_FILE"
else
echo " ⚠️ npx not available, using basic link validation" >&2
fi
# 3. Validate Internal Links
echo "📁 Validating internal links and references..." >&2
# Extract internal links (relative paths)
INTERNAL_URLS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^\./|^\.\./' || true)
ABSOLUTE_PATHS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^/' || true)
INTERNAL_ERRORS=0
# Check relative path links
if [ -n "$INTERNAL_URLS" ]; then
echo " 📂 Checking relative path links..." >&2
echo "$INTERNAL_URLS" | while read -r link; do
if [ -n "$link" ]; then
# Remove anchor if present
FILE_PART=$(echo "$link" | cut -d'#' -f1)
ANCHOR_PART=$(echo "$link" | cut -d'#' -f2)
if [ -n "$FILE_PART" ]; then
# Resolve relative path
RESOLVED_PATH="$(cd "$FILE_DIR" && realpath "$FILE_PART" 2>/dev/null || echo "$FILE_PART")"
if [ -f "$RESOLVED_PATH" ] || [ -d "$RESOLVED_PATH" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link (resolved to: $RESOLVED_PATH)" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
# Check anchor if present (simplified check)
if [ "$link" != "$FILE_PART" ] && [ -n "$ANCHOR_PART" ]; then
echo " ℹ️ Anchor found: #$ANCHOR_PART" >&2
fi
fi
done
fi
# Check absolute path links
if [ -n "$ABSOLUTE_PATHS" ]; then
echo " 📁 Checking absolute path links..." >&2
echo "$ABSOLUTE_PATHS" | while read -r link; do
if [ -n "$link" ]; then
FILE_PART=$(echo "$link" | cut -d'#' -f1)
if [ -f "$FILE_PART" ] || [ -d "$FILE_PART" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
done
fi
if [ "$INTERNAL_ERRORS" -eq 0 ]; then
report_validation "PASS" "All internal links are valid"
else
report_validation "ERROR" "Found $INTERNAL_ERRORS broken internal links"
fi
# 4. Validate Image Links
echo "🖼️ Validating image links..." >&2
IMAGE_COUNT=$(cat "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$IMAGE_COUNT" -gt 0 ]; then
echo " 📊 Found $IMAGE_COUNT image links" >&2
IMAGE_ERRORS=0
cat "$TEMP_IMAGES" | while read -r img_link; do
if [ -n "$img_link" ]; then
if [[ "$img_link" == http* ]]; then
echo " 🌐 External image: $img_link" >&2
else
# Local image file
if [[ "$img_link" == /* ]]; then
IMG_PATH="$img_link"
else
IMG_PATH="$FILE_DIR/$img_link"
fi
if [ -f "$IMG_PATH" ]; then
echo " ✅ Valid image: $img_link" >&2
else
echo " ❌ Missing image: $img_link" >&2
IMAGE_ERRORS=$((IMAGE_ERRORS + 1))
fi
fi
fi
done
if [ "$IMAGE_ERRORS" -eq 0 ]; then
report_validation "PASS" "All local images found"
else
report_validation "ERROR" "$IMAGE_ERRORS local images missing"
fi
else
echo " 📋 No image links found" >&2
fi
# 5. Validate Internal Anchors
echo "⚓ Validating document anchors..." >&2
# Extract anchor-only links (starting with #)
ANCHOR_LINKS=$(cat "$TEMP_LINKS" | grep '^#' 2>/dev/null || true)
if [ -n "$ANCHOR_LINKS" ]; then
echo " 🔗 Found anchor links to validate" >&2
# Extract headers from markdown to validate anchors
HEADERS_FILE="/tmp/markdown_headers_$$"
grep -E '^#{1,6} ' "$FILE_PATH" | sed 's/^#* *//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g' > "$HEADERS_FILE" 2>/dev/null || true
ANCHOR_ERRORS=0
echo "$ANCHOR_LINKS" | while read -r anchor; do
if [ -n "$anchor" ]; then
CLEAN_ANCHOR=$(echo "$anchor" | sed 's/^#//' | tr '[:upper:]' '[:lower:]')
if grep -q "^$CLEAN_ANCHOR$" "$HEADERS_FILE" 2>/dev/null; then
echo " ✅ Valid anchor: $anchor" >&2
else
echo " ❌ Invalid anchor: $anchor" >&2
ANCHOR_ERRORS=$((ANCHOR_ERRORS + 1))
fi
fi
done
rm -f "$HEADERS_FILE"
if [ "$ANCHOR_ERRORS" -eq 0 ]; then
report_validation "PASS" "All document anchors valid"
else
report_validation "ERROR" "$ANCHOR_ERRORS invalid document anchors"
fi
else
echo " 📋 No document anchors found" >&2
fi
# 6. Markdown Quality Checks
echo "📝 Markdown quality and accessibility checks..." >&2
# Check for alt text in images
IMAGES_WITHOUT_ALT=$(grep -c '!\[\](' "$FILE_PATH" 2>/dev/null || echo "0")
if [ "$IMAGES_WITHOUT_ALT" -gt 0 ]; then
report_validation "WARNING" "$IMAGES_WITHOUT_ALT images missing alt text for accessibility"
else
if [ "$IMAGE_COUNT" -gt 0 ]; then
report_validation "PASS" "All images have alt text"
fi
fi
# Check for bare URLs (not wrapped in markdown links)
BARE_URLS=$(grep -oE 'https?://[^\s\)\]]+' "$FILE_PATH" | grep -v '](http' | head -5 | wc -l || echo "0")
if [ "$BARE_URLS" -gt 0 ]; then
report_validation "WARNING" "Found $BARE_URLS bare URLs - consider wrapping in markdown links"
fi
# 7. Generate Validation Summary
echo "" >&2
echo "📋 Markdown Link Validation Summary:" >&2
echo "===================================" >&2
echo " 📄 File: $FILE_NAME" >&2
echo " 📏 Size: $(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines" >&2
echo " 🔗 Total links: $TOTAL_LINKS" >&2
echo " 🖼️ Images: $IMAGE_COUNT" >&2
echo " ✅ Validations passed: $VALIDATIONS_PASSED" >&2
echo " ⚠️ Warnings: $WARNINGS" >&2
echo " ❌ Errors: $ERRORS" >&2
if [ "$ERRORS" -eq 0 ]; then
if [ "$WARNINGS" -eq 0 ]; then
echo " 🎉 Status: EXCELLENT - All links are valid and accessible" >&2
else
echo " ✅ Status: GOOD - Links are valid with minor accessibility recommendations" >&2
fi
else
echo " ❌ Status: ERRORS - Found broken links that must be fixed" >&2
fi
echo "" >&2
echo "💡 Markdown Link Best Practices:" >&2
echo " • Use descriptive link text instead of 'click here'" >&2
echo " • Add alt text to all images for accessibility" >&2
echo " • Use relative paths for internal documentation" >&2
echo " • Validate external links regularly" >&2
echo " • Keep anchor links synchronized with headers" >&2
echo " • Consider using reference-style links for readability" >&2
# Clean up temporary files
rm -f "$TEMP_LINKS" "$TEMP_IMAGES" "$TEMP_ANCHORS"
# Exit with error if there are critical link issues
if [ "$ERRORS" -gt 0 ]; then
echo "⚠️ Markdown link validation completed with errors" >&2
exit 1
fi
else
# Not a markdown file
exit 0
fi
exit 0Hook times out on markdown files with many links
Increase timeout in markdown-link-check config from 30s to 60s. Reduce retryCount from 3 to 1 for faster failures. Use --quiet flag or limit external link checking to critical links only.
False positives for localhost or development URLs
Add localhost patterns to ignorePatterns in .markdown-link-check.json config. Hook creates temp config with common exclusions but customize project config for development server URLs.
Anchor validation fails for generated heading IDs
Heading ID generation varies by markdown processor. Update anchor cleaning logic in hook to match your tool's slug generation (GitHub uses lowercase with dashes, Jekyll may differ).
Image links show as broken but files exist
Check path resolution relative to markdown file location. Hook resolves paths from file directory, not repo root. Use absolute paths from repo root with leading slash for consistency.
External link validation causes rate limiting errors
Enable retryOn429 in config and increase timeout. Add rate-limited domains to ignorePatterns temporarily. Consider running full external validation in CI only, not on every file edit.
Loading reviews...
Join our community of Claude power users. No spam, unsubscribe anytime.
Automated accessibility testing and compliance checking for web applications following WCAG guidelines
Automatically generates or updates API documentation when endpoint files are modified
Automatically formats code files after Claude writes or edits them using Prettier, Black, or other formatters