check-links.js
1 #!/usr/bin/env node 2 /** 3 * Link checker script for documentation 4 * Validates internal and external links 5 */ 6 7 import { glob } from 'glob' 8 import { readFile } from 'fs/promises' 9 import path from 'path' 10 import { fileURLToPath } from 'url' 11 12 const __dirname = path.dirname(fileURLToPath(import.meta.url)) 13 const rootDir = path.join(__dirname, '..') 14 15 const patterns = [ 16 'docs/**/*.md', 17 '.vitepress/**/*.{md,ts,js}', 18 '*.md' 19 ] 20 21 const internalLinkRegex = /\]\((\/[^)]+)\)/g 22 const externalLinkRegex = /\]\((https?:\/\/[^)]+)\)/g 23 24 async function checkLinks() { 25 console.log('Checking documentation links...\n') 26 27 const allFiles = await Promise.all( 28 patterns.map(pattern => glob(pattern, { cwd: rootDir, absolute: true })) 29 ) 30 31 const files = [...new Set(allFiles.flat())] 32 let totalLinks = 0 33 let errors = [] 34 35 for (const file of files) { 36 const content = await readFile(file, 'utf-8') 37 const relativePath = path.relative(rootDir, file) 38 39 // Check internal links 40 for (const match of content.matchAll(internalLinkRegex)) { 41 totalLinks++ 42 const link = match[1] 43 44 // Skip anchors 45 const [linkPath] = link.split('#') 46 if (!linkPath) continue 47 48 // Check if link resolves 49 const targetPath = path.join(rootDir, linkPath) 50 try { 51 await readFile(targetPath.endsWith('.md') ? targetPath : `${targetPath}.md`) 52 } catch { 53 errors.push({ 54 file: relativePath, 55 link, 56 type: 'internal' 57 }) 58 } 59 } 60 61 // Check external links (lightweight - just URL format) 62 for (const match of content.matchAll(externalLinkRegex)) { 63 totalLinks++ 64 const link = match[1] 65 66 if (link.includes('example.com') || link.includes('YOUR_')) { 67 errors.push({ 68 file: relativePath, 69 link, 70 type: 'placeholder' 71 }) 72 } 73 } 74 } 75 76 console.log(`Checked ${files.length} files with ${totalLinks} links\n`) 77 78 if (errors.length === 0) { 79 console.log('✅ All links are valid!') 80 return 0 81 } 82 83 console.log(`❌ Found ${errors.length} issues:\n`) 84 errors.forEach(({ file, link, type }) => { 85 console.log(` [${type}] ${file}`) 86 console.log(` → ${link}\n`) 87 }) 88 89 return 1 90 } 91 92 checkLinks().then(code => process.exit(code))